From c2a51d1661b2853cf9e60617f61c030cd4d6ccc0 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 19:48:18 +0100 Subject: [PATCH 01/35] refactor: replace ToastEventBus with injectable Toaster - Add ToastText sealed interface for type-safe messages - Create Toaster singleton with convenience APIs - Move ToastType to top-level with @Stable annotation - Migrate all ViewModels and Repos to use Toaster - Delete ToastEventBus in favor of DI pattern Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/Toast.kt | 8 +- .../main/java/to/bitkit/models/ToastText.kt | 21 +++ .../java/to/bitkit/repositories/BackupRepo.kt | 7 +- .../to/bitkit/repositories/CurrencyRepo.kt | 7 +- app/src/main/java/to/bitkit/ui/ContentView.kt | 4 +- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 4 +- .../bitkit/ui/components/IsOnlineTracker.kt | 6 +- .../java/to/bitkit/ui/components/ToastView.kt | 21 +-- .../recovery/RecoveryMnemonicViewModel.kt | 12 +- .../ui/screens/recovery/RecoveryViewModel.kt | 24 +-- .../ui/screens/scanner/QrScanningScreen.kt | 6 +- .../ui/screens/settings/DevSettingsScreen.kt | 24 +-- .../screens/transfer/SavingsProgressScreen.kt | 6 +- .../transfer/SpendingAdvancedScreen.kt | 4 +- .../external/ExternalFeeCustomScreen.kt | 16 +- .../external/ExternalNodeViewModel.kt | 30 ++-- .../external/LnurlChannelViewModel.kt | 14 +- .../wallets/activity/ActivityDetailScreen.kt | 12 +- .../wallets/activity/ActivityExploreScreen.kt | 4 +- .../screens/wallets/send/SendAmountScreen.kt | 4 +- .../send/SendCoinSelectionViewModel.kt | 5 +- .../screens/wallets/send/SendFeeViewModel.kt | 16 +- .../wallets/send/SendRecipientScreen.kt | 6 +- .../ui/settings/BlocktankRegtestScreen.kt | 16 +- .../to/bitkit/ui/settings/SettingsScreen.kt | 4 +- .../settings/advanced/AddressViewerScreen.kt | 4 +- .../settings/advanced/ElectrumConfigScreen.kt | 6 +- .../advanced/ElectrumConfigViewModel.kt | 10 +- .../ui/settings/advanced/RgsServerScreen.kt | 6 +- .../backups/BackupNavSheetViewModel.kt | 16 +- .../ui/settings/backups/ShowMnemonicScreen.kt | 12 +- .../settings/lightning/ChannelDetailScreen.kt | 4 +- .../LightningConnectionsViewModel.kt | 27 ++-- .../bitkit/ui/shared/toast/ToastEventBus.kt | 34 ----- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 144 ++++++++++++++++++ .../java/to/bitkit/ui/sheets/BackupSheet.kt | 1 + .../java/to/bitkit/viewmodels/AppViewModel.kt | 50 +++--- .../bitkit/viewmodels/DevSettingsViewModel.kt | 29 ++-- .../to/bitkit/viewmodels/LdkDebugViewModel.kt | 105 +++---------- .../to/bitkit/viewmodels/TransferViewModel.kt | 30 ++-- .../to/bitkit/viewmodels/WalletViewModel.kt | 35 ++--- .../bitkit/repositories/CurrencyRepoTest.kt | 3 + .../java/to/bitkit/ui/WalletViewModelTest.kt | 5 + .../wallets/send/SendFeeViewModelTest.kt | 4 +- .../viewmodels/AmountInputViewModelTest.kt | 4 + 45 files changed, 417 insertions(+), 393 deletions(-) create mode 100644 app/src/main/java/to/bitkit/models/ToastText.kt delete mode 100644 app/src/main/java/to/bitkit/ui/shared/toast/ToastEventBus.kt create mode 100644 app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index a4dc00d99..b9688108e 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -1,5 +1,11 @@ package to.bitkit.models +import androidx.compose.runtime.Stable + +@Stable +enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR } + +@Stable data class Toast( val type: ToastType, val title: String, @@ -8,8 +14,6 @@ data class Toast( val visibilityTime: Long = VISIBILITY_TIME_DEFAULT, val testTag: String? = null, ) { - enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR } - companion object { const val VISIBILITY_TIME_DEFAULT = 3000L } diff --git a/app/src/main/java/to/bitkit/models/ToastText.kt b/app/src/main/java/to/bitkit/models/ToastText.kt new file mode 100644 index 000000000..bf73e651e --- /dev/null +++ b/app/src/main/java/to/bitkit/models/ToastText.kt @@ -0,0 +1,21 @@ +package to.bitkit.models + +import android.content.Context +import androidx.annotation.StringRes +import androidx.compose.runtime.Stable + +@Stable +sealed interface ToastText { + @JvmInline + @Stable + value class Resource(@StringRes val resId: Int) : ToastText + + @JvmInline + @Stable + value class Literal(val value: String) : ToastText +} + +fun ToastText.asString(context: Context): String = when (this) { + is ToastText.Resource -> context.getString(resId) + is ToastText.Literal -> value +} diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index 9088a0e88..e53ddfcde 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -42,11 +42,10 @@ 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.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 @@ -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()) @@ -373,8 +373,7 @@ class BackupRepo @Inject constructor( lastNotificationTime = currentTime scope.launch { - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.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)) diff --git a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt index 57d347abc..1456fcd11 100644 --- a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt @@ -29,11 +29,10 @@ 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.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 @@ -53,6 +52,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()) @@ -92,8 +92,7 @@ class CurrencyRepo @Inject constructor( .distinctUntilChanged() .collect { isStale -> if (isStale) { - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = "Rates currently unavailable", description = "An error has occurred. Please try again later." ) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 96188dbf9..762bb8ee8 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -46,7 +46,7 @@ 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.ToastType import to.bitkit.models.WidgetType import to.bitkit.ui.Routes.ExternalConnection import to.bitkit.ui.components.AuthCheckScreen @@ -628,7 +628,7 @@ private fun RootNavHost( toastException = { appViewModel.toast(it) }, toast = { title, description -> appViewModel.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = title, description = description ) diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 5e81df27d..55096ff34 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -46,7 +46,7 @@ import to.bitkit.ext.createChannelDetails import to.bitkit.ext.formatToString import to.bitkit.ext.uri import to.bitkit.models.NodeLifecycleState -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay import to.bitkit.repositories.LightningState import to.bitkit.ui.components.BodyM @@ -92,7 +92,7 @@ fun NodeInfoScreen( onDisconnectPeer = { wallet.disconnectPeer(it) }, onCopy = { text -> app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), description = text ) diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index 0ef916d3f..ae21265d2 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -8,7 +8,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.repositories.ConnectivityState import to.bitkit.viewmodels.AppViewModel @@ -31,7 +31,7 @@ fun IsOnlineTracker( when (connectivityState) { ConnectivityState.CONNECTED -> { app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.other__connection_back_title), description = context.getString(R.string.other__connection_back_msg), ) @@ -39,7 +39,7 @@ fun IsOnlineTracker( ConnectivityState.DISCONNECTED -> { app.toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.other__connection_issue), description = context.getString(R.string.other__connection_issue_explain), ) diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index aef3abf9d..4632001bc 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -53,6 +53,7 @@ import dev.chrisbanes.haze.rememberHazeState import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -322,7 +323,7 @@ private fun ToastViewPreview() { ) { ToastView( toast = Toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = "You're still offline", description = "Check your connection to keep using Bitkit.", autoHide = true, @@ -331,7 +332,7 @@ private fun ToastViewPreview() { ) ToastView( toast = Toast( - type = Toast.ToastType.LIGHTNING, + type = ToastType.LIGHTNING, title = "Instant Payments Ready", description = "You can now pay anyone, anywhere, instantly.", autoHide = true, @@ -340,7 +341,7 @@ private fun ToastViewPreview() { ) ToastView( toast = Toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = "You're Back Online!", description = "Successfully reconnected to the Internet.", autoHide = true, @@ -349,7 +350,7 @@ private fun ToastViewPreview() { ) ToastView( toast = Toast( - type = Toast.ToastType.INFO, + type = ToastType.INFO, title = "General Message", description = "Used for neutral content to inform the user.", autoHide = false, @@ -358,7 +359,7 @@ private fun ToastViewPreview() { ) ToastView( toast = Toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = "Error Toast", description = "This is a toast message.", autoHide = true, @@ -372,9 +373,9 @@ private fun ToastViewPreview() { @ReadOnlyComposable @Composable private fun Toast.tintColor(): Color = when (type) { - Toast.ToastType.SUCCESS -> Colors.Green - Toast.ToastType.INFO -> Colors.Blue - Toast.ToastType.LIGHTNING -> Colors.Purple - Toast.ToastType.WARNING -> Colors.Brand - Toast.ToastType.ERROR -> Colors.Red + ToastType.SUCCESS -> Colors.Green + ToastType.INFO -> Colors.Blue + ToastType.LIGHTNING -> Colors.Purple + ToastType.WARNING -> Colors.Brand + ToastType.ERROR -> Colors.Red } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt index fd53d23a9..a9785d623 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt @@ -12,8 +12,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.data.keychain.Keychain -import to.bitkit.models.Toast -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -21,6 +20,7 @@ import javax.inject.Inject class RecoveryMnemonicViewModel @Inject constructor( @ApplicationContext private val context: Context, private val keychain: Keychain, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(RecoveryMnemonicUiState()) @@ -42,11 +42,7 @@ class RecoveryMnemonicViewModel @Inject constructor( isLoading = false, ) } - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.security__mnemonic_load_error), - description = context.getString(R.string.security__mnemonic_load_error), - ) + toaster.error(R.string.security__mnemonic_load_error) return@launch } @@ -66,7 +62,7 @@ class RecoveryMnemonicViewModel @Inject constructor( isLoading = false, ) } - ToastEventBus.send(e) + toaster.error(e) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index c0f19f378..98061ef81 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -17,11 +17,10 @@ import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.env.Env -import to.bitkit.models.Toast import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LogsRepo import to.bitkit.repositories.WalletRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -32,6 +31,7 @@ class RecoveryViewModel @Inject constructor( private val lightningRepo: LightningRepo, private val walletRepo: WalletRepo, private val settingsStore: SettingsStore, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(RecoveryUiState()) @@ -73,11 +73,7 @@ class RecoveryViewModel @Inject constructor( isExportingLogs = false, ) } - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.common__error), - description = context.getString(R.string.other__logs_export_error), - ) + toaster.error(R.string.common__error, R.string.other__logs_export_error) } ) } @@ -98,11 +94,7 @@ class RecoveryViewModel @Inject constructor( }.onFailure { fallbackError -> Logger.error("Failed to open support links", fallbackError, context = TAG) viewModelScope.launch { - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.common__error), - description = context.getString(R.string.settings__support__link_error), - ) + toaster.error(R.string.common__error, R.string.settings__support__link_error) } } } @@ -119,13 +111,9 @@ class RecoveryViewModel @Inject constructor( fun wipeWallet() { viewModelScope.launch { walletRepo.wipeWallet().onFailure { error -> - ToastEventBus.send(error) + toaster.error(error) }.onSuccess { - ToastEventBus.send( - type = Toast.ToastType.SUCCESS, - title = context.getString(R.string.security__wiped_title), - description = context.getString(R.string.security__wiped_message), - ) + toaster.success(R.string.security__wiped_title, R.string.security__wiped_message) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 84d7c76fd..7aad87e56 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -65,7 +65,7 @@ import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ext.getClipboardText import to.bitkit.ext.startActivityAppSettings -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton @@ -150,7 +150,7 @@ fun QrScanningScreen( val error = requireNotNull(result.exceptionOrNull()) Logger.error("Failed to scan QR code", error) app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), description = context.getString(R.string.other__qr_error_text), ) @@ -256,7 +256,7 @@ private fun handlePaste( val clipboard = context.getClipboardText()?.trim() if (clipboard.isNullOrBlank()) { app.toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.wallet__send_clipboard_empty_title), description = context.getString(R.string.wallet__send_clipboard_empty_text), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt index ff033c2ce..c93bc2338 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt @@ -14,7 +14,7 @@ import androidx.navigation.NavController import org.lightningdevkit.ldknode.Network import to.bitkit.R import to.bitkit.env.Env -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.activityListViewModel import to.bitkit.ui.appViewModel @@ -78,63 +78,63 @@ fun DevSettingsScreen( title = "Reset Settings State", onClick = { settings.reset() - app.toast(type = Toast.ToastType.SUCCESS, title = "Settings state reset") + app.toast(type = ToastType.SUCCESS, title = "Settings state reset") } ) SettingsTextButtonRow( title = "Reset All Activities", onClick = { activity.removeAllActivities() - app.toast(type = Toast.ToastType.SUCCESS, title = "Activities removed") + app.toast(type = ToastType.SUCCESS, title = "Activities removed") } ) SettingsTextButtonRow( title = "Reset Backup State", onClick = { viewModel.resetBackupState() - app.toast(type = Toast.ToastType.SUCCESS, title = "Backup state reset") + app.toast(type = ToastType.SUCCESS, title = "Backup state reset") } ) SettingsTextButtonRow( title = "Reset Widgets State", onClick = { viewModel.resetWidgetsState() - app.toast(type = Toast.ToastType.SUCCESS, title = "Widgets state reset") + app.toast(type = ToastType.SUCCESS, title = "Widgets state reset") } ) SettingsTextButtonRow( title = "Refresh Currency Rates", onClick = { viewModel.refreshCurrencyRates() - app.toast(type = Toast.ToastType.SUCCESS, title = "Currency rates refreshed") + app.toast(type = ToastType.SUCCESS, title = "Currency rates refreshed") } ) SettingsTextButtonRow( title = "Reset App Database", onClick = { viewModel.resetDatabase() - app.toast(type = Toast.ToastType.SUCCESS, title = "Database state reset") + app.toast(type = ToastType.SUCCESS, title = "Database state reset") } ) SettingsTextButtonRow( title = "Reset Blocktank State", onClick = { viewModel.resetBlocktankState() - app.toast(type = Toast.ToastType.SUCCESS, title = "Blocktank state reset") + app.toast(type = ToastType.SUCCESS, title = "Blocktank state reset") } ) SettingsTextButtonRow( title = "Reset Cache Store", onClick = { viewModel.resetCacheStore() - app.toast(type = Toast.ToastType.SUCCESS, title = "Cache store reset") + app.toast(type = ToastType.SUCCESS, title = "Cache store reset") } ) SettingsTextButtonRow( title = "Wipe App", onClick = { viewModel.wipeWallet() - app.toast(type = Toast.ToastType.SUCCESS, title = "Wallet wiped") + app.toast(type = ToastType.SUCCESS, title = "Wallet wiped") } ) @@ -145,14 +145,14 @@ fun DevSettingsScreen( onClick = { val count = 100 activity.generateRandomTestData(count) - app.toast(type = Toast.ToastType.SUCCESS, title = "Generated $count test activities") + app.toast(type = ToastType.SUCCESS, title = "Generated $count test activities") } ) SettingsTextButtonRow( "Fake New BG Receive", onClick = { viewModel.fakeBgReceive() - app.toast(type = Toast.ToastType.INFO, title = "Restart app to see the payment received sheet") + app.toast(type = ToastType.INFO, title = "Restart app to see the payment received sheet") } ) SettingsTextButtonRow( diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 3b3521c2e..1b2269d82 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import to.bitkit.R -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton @@ -71,7 +71,7 @@ fun SavingsProgressScreen( if (nonTrustedChannels.isEmpty()) { // All channels are trusted peers - show error and navigate back immediately app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.lightning__close_error), description = context.getString(R.string.lightning__close_error_msg), ) @@ -82,7 +82,7 @@ fun SavingsProgressScreen( onGiveUp = { app.showSheet(Sheet.ForceTransfer) }, onTransferUnavailable = { app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.lightning__close_error), description = context.getString(R.string.lightning__close_error_msg), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt index 7e92fad64..1511ef44a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt @@ -26,7 +26,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ext.mockOrder -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.appViewModel @@ -91,7 +91,7 @@ fun SpendingAdvancedScreen( is TransferEffect.ToastError -> { isLoading = false app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = effect.title, description = effect.description, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt index f2cd73f25..4981a7958 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalFeeCustomScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.BITCOIN_SYMBOL -import to.bitkit.models.Toast import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display @@ -39,7 +38,6 @@ import to.bitkit.ui.currencyViewModel import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -52,7 +50,6 @@ fun ExternalFeeCustomScreen( val uiState by viewModel.uiState.collectAsState() val currency = currencyViewModel ?: return val scope = rememberCoroutineScope() - val context = LocalContext.current var input by remember { @@ -88,18 +85,11 @@ fun ExternalFeeCustomScreen( } }, onContinue = { - val feeRate = input.toUIntOrNull() ?: 0u - if (feeRate == 0u) { - scope.launch { - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = context.getString(R.string.wallet__min_possible_fee_rate), - description = context.getString(R.string.wallet__min_possible_fee_rate_msg), - ) + scope.launch { + if (viewModel.validateCustomFeeRate()) { + onBack() } - return@Content } - onBack() }, onBack = onBack, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index efd087df3..63859b1da 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -20,7 +20,6 @@ import to.bitkit.data.SettingsStore import to.bitkit.ext.WatchResult import to.bitkit.ext.of import to.bitkit.ext.watchUntil -import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed import to.bitkit.models.TransferType import to.bitkit.models.formatToModernDisplay @@ -28,7 +27,7 @@ import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.SideEffect import to.bitkit.ui.screens.transfer.external.ExternalNodeContract.UiState -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -40,6 +39,7 @@ class ExternalNodeViewModel @Inject constructor( private val lightningRepo: LightningRepo, private val settingsStore: SettingsStore, private val transferRepo: to.bitkit.repositories.TransferRepo, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(UiState()) val uiState = _uiState.asStateFlow() @@ -73,11 +73,7 @@ class ExternalNodeViewModel @Inject constructor( _uiState.update { it.copy(peer = peer) } setEffect(SideEffect.ConnectionSuccess) } else { - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.lightning__error_add_title), - description = context.getString(R.string.lightning__error_add), - ) + toaster.error(R.string.lightning__error_add_title, R.string.lightning__error_add) } } } @@ -89,10 +85,7 @@ class ExternalNodeViewModel @Inject constructor( if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } } else { - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.lightning__error_add_uri), - ) + toaster.error(R.string.lightning__error_add_uri) } } } @@ -102,8 +95,7 @@ class ExternalNodeViewModel @Inject constructor( if (sats > maxAmount) { viewModelScope.launch { - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = context.getString(R.string.lightning__spending_amount__error_max__title), description = context.getString(R.string.lightning__spending_amount__error_max__description) .replace("{amount}", maxAmount.formatToModernDisplay()), @@ -134,6 +126,15 @@ class ExternalNodeViewModel @Inject constructor( updateNetworkFee() } + suspend fun validateCustomFeeRate(): Boolean { + val feeRate = _uiState.value.customFeeRate ?: 0u + if (feeRate == 0u) { + toaster.info(R.string.wallet__min_possible_fee_rate, R.string.wallet__min_possible_fee_rate_msg) + return false + } + return true + } + private fun updateNetworkFee() { viewModelScope.launch { val amountSats = _uiState.value.amount.sats @@ -180,8 +181,7 @@ class ExternalNodeViewModel @Inject constructor( }.onFailure { e -> val error = e.message.orEmpty() Logger.warn("Error opening channel with peer: '${_uiState.value.peer}': '$error'") - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = context.getString(R.string.lightning__error_channel_purchase), description = context.getString(R.string.lightning__error_channel_setup_msg) .replace("{raw}", error), diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index 52401cc90..9a38a8edb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -12,16 +12,16 @@ import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R import to.bitkit.ext.of -import to.bitkit.models.Toast import to.bitkit.repositories.LightningRepo import to.bitkit.ui.Routes -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import javax.inject.Inject @HiltViewModel class LnurlChannelViewModel @Inject constructor( @ApplicationContext private val context: Context, private val lightningRepo: LightningRepo, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(LnurlChannelUiState()) @@ -68,10 +68,9 @@ class LnurlChannelViewModel @Inject constructor( lightningRepo.requestLnurlChannel(callback = params.callback, k1 = params.k1, nodeId = nodeId) .onSuccess { - ToastEventBus.send( - type = Toast.ToastType.SUCCESS, - title = context.getString(R.string.other__lnurl_channel_success_title), - description = context.getString(R.string.other__lnurl_channel_success_msg_no_peer), + toaster.success( + R.string.other__lnurl_channel_success_title, + R.string.other__lnurl_channel_success_msg_no_peer, ) _uiState.update { it.copy(isConnected = true) } }.onFailure { error -> @@ -83,8 +82,7 @@ class LnurlChannelViewModel @Inject constructor( } suspend fun errorToast(error: Throwable) { - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = context.getString(R.string.other__lnurl_channel_error), description = error.message ?: "Unknown error", ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index 6bc8520b6..b0ca70f3e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -58,7 +58,7 @@ import to.bitkit.ext.toActivityItemDate import to.bitkit.ext.toActivityItemTime import to.bitkit.ext.totalValue import to.bitkit.models.FeeRate.Companion.getFeeShortDescription -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.blocktankViewModel @@ -228,7 +228,7 @@ fun ActivityDetailScreen( boostTxDoesExist = boostTxDoesExist, onCopy = { text -> app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = copyToastTitle, description = text.ellipsisMiddle(40) ) @@ -251,7 +251,7 @@ fun ActivityDetailScreen( item = it, onSuccess = { app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.wallet__boost_success_title), description = context.getString(R.string.wallet__boost_success_msg), testTag = "BoostSuccessToast" @@ -261,7 +261,7 @@ fun ActivityDetailScreen( }, onFailure = { app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__boost_error_title), description = context.getString(R.string.wallet__boost_error_msg), testTag = "BoostFailureToast" @@ -270,14 +270,14 @@ fun ActivityDetailScreen( }, onMaxFee = { app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__send_fee_error), description = context.getString(R.string.wallet__send_fee_error_max) ) }, onMinFee = { app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__send_fee_error), description = context.getString(R.string.wallet__send_fee_error_min) ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index f6b8e16f0..130a11fc2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -45,7 +45,7 @@ import to.bitkit.ext.create import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.isSent import to.bitkit.ext.totalValue -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BalanceHeaderView @@ -167,7 +167,7 @@ fun ActivityExploreScreen( boostTxDoesExist = boostTxDoesExist, onCopy = { text -> app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = toastMessage, description = text.ellipsisMiddle(40), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index 292aeaca3..60a356589 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -32,7 +32,7 @@ import to.bitkit.ext.maxWithdrawableSat import to.bitkit.models.BalanceState import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.NodeLifecycleState -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.models.safe import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalBalances @@ -109,7 +109,7 @@ fun SendAmountScreen( onClickMax = { maxSats -> if (uiState.lnurl == null) { app?.toast( - type = Toast.ToastType.INFO, + type = ToastType.INFO, title = context.getString(R.string.wallet__send_max_spending__title), description = context.getString(R.string.wallet__send_max_spending__description) ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt index 851027e26..b3cfcb9b6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt @@ -16,7 +16,7 @@ import to.bitkit.env.Defaults import to.bitkit.ext.rawId import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.LightningRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -25,6 +25,7 @@ class SendCoinSelectionViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningRepo: LightningRepo, private val activityRepo: ActivityRepo, + private val toaster: Toaster, ) : ViewModel() { companion object { private const val TAG = "SendCoinSelectionViewModel" @@ -67,7 +68,7 @@ class SendCoinSelectionViewModel @Inject constructor( } }.onFailure { Logger.error("Failed to load UTXOs for coin selection", it, context = TAG) - ToastEventBus.send(Exception("Failed to load UTXOs: ${it.message}")) + toaster.error("Failed to load UTXOs: ${it.message}") } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index cdd4fac8f..8e12bda98 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -12,13 +12,12 @@ import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.models.FeeRate -import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo import to.bitkit.ui.components.KEY_DELETE -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.viewmodels.SendUiState import javax.inject.Inject @@ -32,6 +31,7 @@ class SendFeeViewModel @Inject constructor( private val currencyRepo: CurrencyRepo, private val walletRepo: WalletRepo, @ApplicationContext private val context: Context, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(SendFeeUiState()) val uiState = _uiState.asStateFlow() @@ -105,20 +105,12 @@ class SendFeeViewModel @Inject constructor( // TODO update to use minimum instead of slow when using mempool api val minSatsPerVByte = sendUiState.feeRates?.slow ?: 1u if (satsPerVByte < minSatsPerVByte) { - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = context.getString(R.string.wallet__min_possible_fee_rate), - description = context.getString(R.string.wallet__min_possible_fee_rate_msg) - ) + toaster.info(R.string.wallet__min_possible_fee_rate, R.string.wallet__min_possible_fee_rate_msg) return false } if (satsPerVByte > maxSatsPerVByte) { - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = context.getString(R.string.wallet__max_possible_fee_rate), - description = context.getString(R.string.wallet__max_possible_fee_rate_msg) - ) + toaster.info(R.string.wallet__max_possible_fee_rate, R.string.wallet__max_possible_fee_rate_msg) return false } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index 26e487a9d..3ff163ade 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -59,7 +59,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import to.bitkit.R import to.bitkit.ext.startActivityAppSettings -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB @@ -140,7 +140,7 @@ fun SendRecipientScreen( val error = requireNotNull(result.exceptionOrNull()) Logger.error("Scan failed", error, context = TAG) app?.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), description = context.getString(R.string.other__qr_error_text), ) @@ -174,7 +174,7 @@ fun SendRecipientScreen( }.onFailure { Logger.error("Camera initialization failed", it, context = TAG) app?.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), description = context.getString(R.string.other__camera_init_error) .replace("{message}", it.message.orEmpty()) diff --git a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt index fcc6ff45c..4965889e5 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt @@ -28,7 +28,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import kotlinx.coroutines.launch import to.bitkit.R -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption @@ -113,14 +113,14 @@ fun BlocktankRegtestScreen( val txId = viewModel.regtestDeposit(depositAddress, sats) Logger.debug("Deposit successful with txId: $txId") app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = "Success", description = "Deposit successful. TxID: $txId", ) }.onFailure { Logger.error("Deposit failed", it) app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = "Failed to deposit", description = it.message.orEmpty(), ) @@ -159,14 +159,14 @@ fun BlocktankRegtestScreen( viewModel.regtestMine(count) Logger.debug("Successfully mined $count blocks") app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = "Success", description = "Successfully mined $count blocks", ) }.onFailure { Logger.error("Mining failed", it) app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = "Failed to mine", description = it.message.orEmpty(), ) @@ -211,14 +211,14 @@ fun BlocktankRegtestScreen( val paymentId = viewModel.regtestPay(paymentInvoice, amount) Logger.debug("Payment successful with ID: $paymentId") app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = "Success", description = "Payment successful. ID: $paymentId", ) }.onFailure { Logger.error("Payment failed", it) app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = "Failed to pay invoice from LND", description = it.message.orEmpty(), ) @@ -278,7 +278,7 @@ fun BlocktankRegtestScreen( ) Logger.debug("Channel closed successfully with txId: $closingTxId") app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = "Success", description = "Channel closed. Closing TxID: $closingTxId" ) diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index f08ac96af..1893dc944 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.settings.SettingsButtonRow @@ -76,7 +76,7 @@ fun SettingsScreen( haptic.performHapticFeedback(HapticFeedbackType.LongPress) app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString( if (newValue) { R.string.settings__dev_enabled_title diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index 09353a86c..e65a58bd9 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -30,7 +30,7 @@ import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.models.AddressModel -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyS @@ -78,7 +78,7 @@ fun AddressViewerScreen( onCopy = { text -> context.setClipboardText(text) app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), description = text, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 72531e42f..1a46b44db 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -31,7 +31,7 @@ import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServerPeer -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up @@ -75,7 +75,7 @@ fun ElectrumConfigScreen( uiState.connectionResult?.let { result -> if (result.isSuccess) { app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.settings__es__server_updated_title), description = context.getString(R.string.settings__es__server_updated_message) .replace("{host}", uiState.host) @@ -84,7 +84,7 @@ fun ElectrumConfigScreen( ) } else { app.toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.settings__es__server_error), description = context.getString(R.string.settings__es__server_error_description), testTag = "ElectrumErrorToast", diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt index ed41850f1..a7c25036d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt @@ -23,10 +23,9 @@ import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServer import to.bitkit.models.ElectrumServerPeer import to.bitkit.models.MAX_VALID_PORT -import to.bitkit.models.Toast import to.bitkit.models.getDefaultPort import to.bitkit.repositories.LightningRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import javax.inject.Inject @HiltViewModel @@ -35,6 +34,7 @@ class ElectrumConfigViewModel @Inject constructor( @ApplicationContext private val context: Context, private val settingsStore: SettingsStore, private val lightningRepo: LightningRepo, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(ElectrumConfigUiState()) @@ -246,8 +246,7 @@ class ElectrumConfigViewModel @Inject constructor( viewModelScope.launch { val validationError = validateInput() if (validationError != null) { - ToastEventBus.send( - type = Toast.ToastType.WARNING, + toaster.warning( title = context.getString(R.string.settings__es__error_peer), description = validationError, ) @@ -268,8 +267,7 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput(host, port) if (validationError != null) { - ToastEventBus.send( - type = Toast.ToastType.WARNING, + toaster.warning( title = context.getString(R.string.settings__es__error_peer), description = validationError, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index cf74847db..28d9d08d0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -26,7 +26,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up @@ -68,14 +68,14 @@ fun RgsServerScreen( uiState.connectionResult?.let { result -> if (result.isSuccess) { app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.settings__rgs__update_success_title), description = context.getString(R.string.settings__rgs__update_success_description), testTag = "RgsUpdatedToast", ) } else { app.toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__ldk_start_error_title), description = result.exceptionOrNull()?.message ?: "Unknown error", testTag = "RgsErrorToast", diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index 3c41f83b5..385d8dd30 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -19,11 +19,10 @@ import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain import to.bitkit.models.BackupCategory import to.bitkit.models.HealthState -import to.bitkit.models.Toast import to.bitkit.repositories.HealthRepo import to.bitkit.ui.settings.backups.BackupContract.SideEffect import to.bitkit.ui.settings.backups.BackupContract.UiState -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -35,6 +34,7 @@ class BackupNavSheetViewModel @Inject constructor( private val keychain: Keychain, private val healthRepo: HealthRepo, private val cacheStore: CacheStore, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(UiState()) @@ -86,11 +86,7 @@ class BackupNavSheetViewModel @Inject constructor( } }.onFailure { Logger.error("Error loading mnemonic", it, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = context.getString(R.string.security__mnemonic_error), - description = context.getString(R.string.security__mnemonic_error_description), - ) + toaster.warning(R.string.security__mnemonic_error, R.string.security__mnemonic_error_description) } } @@ -154,6 +150,12 @@ class BackupNavSheetViewModel @Inject constructor( fun resetState() { _uiState.update { UiState() } } + + fun onMnemonicCopied() { + viewModelScope.launch { + toaster.success(R.string.common__copied, R.string.security__mnemonic_copied) + } + } } interface BackupContract { diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt index 0468834aa..843441ce0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/ShowMnemonicScreen.kt @@ -41,7 +41,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.ext.setClipboardText -import to.bitkit.models.Toast import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.BottomSheetPreview @@ -51,7 +50,6 @@ import to.bitkit.ui.components.SheetSize import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.effects.BlockScreenshots import to.bitkit.ui.shared.modifiers.sheetHeight -import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -63,24 +61,18 @@ fun ShowMnemonicScreen( uiState: BackupContract.UiState, onRevealClick: () -> Unit, onContinueClick: () -> Unit, + onMnemonicCopied: () -> Unit, ) { BlockScreenshots() val context = LocalContext.current - val scope = rememberCoroutineScope() ShowMnemonicContent( mnemonic = uiState.bip39Mnemonic, showMnemonic = uiState.showMnemonic, onRevealClick = onRevealClick, onCopyClick = { context.setClipboardText(uiState.bip39Mnemonic) - scope.launch { - ToastEventBus.send( - type = Toast.ToastType.SUCCESS, - title = context.getString(R.string.common__copied), - description = context.getString(R.string.security__mnemonic_copied), - ) - } + onMnemonicCopied() }, onContinueClick = onContinueClick, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 04795bd4d..fffa115db 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -55,7 +55,7 @@ import to.bitkit.ext.DatePattern import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.setClipboardText -import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Caption13Up @@ -129,7 +129,7 @@ fun ChannelDetailScreen( onCopyText = { text -> context.setClipboardText(text) app.toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), description = text, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index 94174c2d6..a1324dd09 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -31,13 +31,12 @@ import to.bitkit.ext.calculateRemoteBalance import to.bitkit.ext.createChannelDetails import to.bitkit.ext.filterOpen import to.bitkit.ext.filterPending -import to.bitkit.models.Toast import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LogsRepo import to.bitkit.repositories.WalletRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -51,6 +50,7 @@ class LightningConnectionsViewModel @Inject constructor( private val logsRepo: LogsRepo, private val walletRepo: WalletRepo, private val activityRepo: ActivityRepo, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(LightningConnectionsUiState()) @@ -338,11 +338,10 @@ class LightningConnectionsViewModel @Inject constructor( viewModelScope.launch { logsRepo.zipLogsForSharing() .onSuccess { uri -> onReady(uri) } - .onFailure { err -> - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = context.getString(R.string.lightning__error_logs), - description = context.getString(R.string.lightning__error_logs_description), + .onFailure { + toaster.warning( + R.string.lightning__error_logs, + R.string.lightning__error_logs_description, ) } } @@ -454,10 +453,9 @@ class LightningConnectionsViewModel @Inject constructor( onSuccess = { walletRepo.syncNodeAndWallet() - ToastEventBus.send( - type = Toast.ToastType.SUCCESS, - title = context.getString(R.string.lightning__close_success_title), - description = context.getString(R.string.lightning__close_success_msg), + toaster.success( + R.string.lightning__close_success_title, + R.string.lightning__close_success_msg, ) _closeConnectionUiState.update { @@ -470,10 +468,9 @@ class LightningConnectionsViewModel @Inject constructor( onFailure = { error -> Logger.error("Failed to close channel", e = error, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = context.getString(R.string.lightning__close_error), - description = context.getString(R.string.lightning__close_error_msg), + toaster.warning( + R.string.lightning__close_error, + R.string.lightning__close_error_msg, ) _closeConnectionUiState.update { it.copy(isLoading = false) } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastEventBus.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastEventBus.kt deleted file mode 100644 index 5613a4265..000000000 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastEventBus.kt +++ /dev/null @@ -1,34 +0,0 @@ -package to.bitkit.ui.shared.toast - -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import to.bitkit.models.Toast - -object ToastEventBus { - private val _events = MutableSharedFlow(extraBufferCapacity = 1) - val events = _events.asSharedFlow() - - suspend fun send( - type: Toast.ToastType, - title: String, - description: String? = null, - autoHide: Boolean = true, - visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, - ) { - _events.emit( - Toast(type, title, description, autoHide, visibilityTime) - ) - } - - suspend fun send(error: Throwable) { - _events.emit( - Toast( - type = Toast.ToastType.ERROR, - title = "Error", - description = error.message ?: "Unknown error", - autoHide = true, - visibilityTime = Toast.VISIBILITY_TIME_DEFAULT, - ) - ) - } -} diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt new file mode 100644 index 000000000..096b381f6 --- /dev/null +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -0,0 +1,144 @@ +package to.bitkit.ui.shared.toast + +import android.content.Context +import androidx.annotation.StringRes +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import to.bitkit.R +import to.bitkit.models.Toast +import to.bitkit.models.ToastType +import javax.inject.Inject +import javax.inject.Singleton + +@Suppress("TooManyFunctions") +@Singleton +class Toaster @Inject constructor( + @ApplicationContext private val context: Context, +) { + private val _events = MutableSharedFlow(extraBufferCapacity = 1) + val events: SharedFlow = _events.asSharedFlow() + + @Suppress("LongParameterList") + private suspend fun emit( + type: ToastType, + title: String, + description: String? = null, + autoHide: Boolean = true, + visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, + testTag: String? = null, + ) { + _events.emit( + Toast( + type = type, + title = title, + description = description, + autoHide = autoHide, + visibilityTime = visibilityTime, + testTag = testTag, + ) + ) + } + + // region Success + suspend fun success( + title: String, + description: String? = null, + testTag: String? = null, + ) = emit(ToastType.SUCCESS, title, description, testTag = testTag) + + suspend fun success( + @StringRes titleRes: Int, + @StringRes descriptionRes: Int? = null, + testTag: String? = null, + ) = emit( + type = ToastType.SUCCESS, + title = context.getString(titleRes), + description = descriptionRes?.let { context.getString(it) }, + testTag = testTag, + ) + // endregion + + // region Info + suspend fun info( + title: String, + description: String? = null, + testTag: String? = null, + ) = emit(ToastType.INFO, title, description, testTag = testTag) + + suspend fun info( + @StringRes titleRes: Int, + @StringRes descriptionRes: Int? = null, + testTag: String? = null, + ) = emit( + type = ToastType.INFO, + title = context.getString(titleRes), + description = descriptionRes?.let { context.getString(it) }, + testTag = testTag, + ) + // endregion + + // region Lightning + suspend fun lightning( + title: String, + description: String? = null, + testTag: String? = null, + ) = emit(ToastType.LIGHTNING, title, description, testTag = testTag) + + suspend fun lightning( + @StringRes titleRes: Int, + @StringRes descriptionRes: Int? = null, + testTag: String? = null, + ) = emit( + type = ToastType.LIGHTNING, + title = context.getString(titleRes), + description = descriptionRes?.let { context.getString(it) }, + testTag = testTag, + ) + // endregion + + // region Warning + suspend fun warning( + title: String, + description: String? = null, + testTag: String? = null, + ) = emit(ToastType.WARNING, title, description, testTag = testTag) + + suspend fun warning( + @StringRes titleRes: Int, + @StringRes descriptionRes: Int? = null, + testTag: String? = null, + ) = emit( + type = ToastType.WARNING, + title = context.getString(titleRes), + description = descriptionRes?.let { context.getString(it) }, + testTag = testTag, + ) + // endregion + + // region Error + suspend fun error( + title: String, + description: String? = null, + testTag: String? = null, + ) = emit(ToastType.ERROR, title, description, testTag = testTag) + + suspend fun error( + @StringRes titleRes: Int, + @StringRes descriptionRes: Int? = null, + testTag: String? = null, + ) = emit( + type = ToastType.ERROR, + title = context.getString(titleRes), + description = descriptionRes?.let { context.getString(it) }, + testTag = testTag, + ) + + suspend fun error(throwable: Throwable) = emit( + type = ToastType.ERROR, + title = context.getString(R.string.common__error), + description = throwable.message ?: context.getString(R.string.common__error_body), + ) + // endregion +} diff --git a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt index 0ccaf924b..4d774bf30 100644 --- a/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt +++ b/app/src/main/java/to/bitkit/ui/sheets/BackupSheet.kt @@ -97,6 +97,7 @@ fun BackupSheet( uiState = uiState, onRevealClick = viewModel::onRevealMnemonic, onContinueClick = viewModel::onShowMnemonicContinue, + onMnemonicCopied = viewModel::onMnemonicCopied, ) } composableWithDefaultTransitions { diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 94c9a8cb2..d148c609b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -82,6 +82,7 @@ import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.Suggestion import to.bitkit.models.Toast +import to.bitkit.models.ToastType import to.bitkit.models.TransactionSpeed import to.bitkit.models.safe import to.bitkit.models.toActivityFilter @@ -102,8 +103,8 @@ import to.bitkit.services.AppUpdaterService import to.bitkit.services.MigrationService import to.bitkit.ui.Routes import to.bitkit.ui.components.Sheet -import to.bitkit.ui.shared.toast.ToastEventBus import to.bitkit.ui.shared.toast.ToastQueueManager +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.theme.TRANSITION_SCREEN_MS import to.bitkit.utils.Logger @@ -127,6 +128,7 @@ class AppViewModel @Inject constructor( healthRepo: HealthRepo, toastManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueueManager, timedSheetManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> TimedSheetManager, + toaster: Toaster, @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val keychain: Keychain, @@ -230,7 +232,7 @@ class AppViewModel @Inject constructor( init { viewModelScope.launch { - ToastEventBus.events.collect { + toaster.events.collect { toast(it.type, it.title, it.description, it.autoHide, it.visibilityTime) } } @@ -449,7 +451,7 @@ class AppViewModel @Inject constructor( delay(MIGRATION_AUTH_RESET_DELAY_MS) resetIsAuthenticatedStateInternal() toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = "Migration Warning", description = "Migration completed but node restart failed. Please restart the app." ) @@ -533,7 +535,7 @@ class AppViewModel @Inject constructor( return } toast( - type = Toast.ToastType.LIGHTNING, + type = ToastType.LIGHTNING, title = context.getString(R.string.lightning__channel_opened_title), description = context.getString(R.string.lightning__channel_opened_msg), testTag = "SpendingBalanceReadyToast", @@ -543,7 +545,7 @@ class AppViewModel @Inject constructor( private suspend fun notifyTransactionRemoved(event: Event.OnchainTransactionEvicted) { if (activityRepo.wasTransactionReplaced(event.txid)) return toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.wallet__toast_transaction_removed_title), description = context.getString(R.string.wallet__toast_transaction_removed_description), testTag = "TransactionRemovedToast", @@ -558,7 +560,7 @@ class AppViewModel @Inject constructor( } private fun notifyTransactionUnconfirmed() = toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.wallet__toast_transaction_unconfirmed_title), description = context.getString(R.string.wallet__toast_transaction_unconfirmed_description), testTag = "TransactionUnconfirmedToast", @@ -567,7 +569,7 @@ class AppViewModel @Inject constructor( private suspend fun notifyTransactionReplaced(event: Event.OnchainTransactionReplaced) { val isReceive = activityRepo.isReceivedTransaction(event.txid) toast( - type = Toast.ToastType.INFO, + type = ToastType.INFO, title = when (isReceive) { true -> R.string.wallet__toast_received_transaction_replaced_title else -> R.string.wallet__toast_transaction_replaced_title @@ -584,7 +586,7 @@ class AppViewModel @Inject constructor( } private fun notifyPaymentFailed() = toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__toast_payment_failed_title), description = context.getString(R.string.wallet__toast_payment_failed_description), testTag = "PaymentFailedToast", @@ -775,7 +777,7 @@ class AppViewModel @Inject constructor( val minSendable = lnurl.data.minSendableSat() if (_sendUiState.value.amount < minSendable) { toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__lnurl_pay__error_min__title), description = context.getString(R.string.wallet__lnurl_pay__error_min__description) .replace("{amount}", minSendable.toString()), @@ -830,7 +832,7 @@ class AppViewModel @Inject constructor( val data = context.getClipboardText()?.trim() if (data.isNullOrBlank()) { toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.wallet__send_clipboard_empty_title), description = context.getString(R.string.wallet__send_clipboard_empty_text), ) @@ -875,7 +877,7 @@ class AppViewModel @Inject constructor( else -> { Logger.warn("Unhandled scan data: $scan", context = TAG) toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.other__scan_err_decoding), description = context.getString(R.string.other__scan_err_interpret_title), ) @@ -892,7 +894,7 @@ class AppViewModel @Inject constructor( ?.takeIf { invoice -> if (invoice.isExpired) { toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.other__scan_err_decoding), description = context.getString(R.string.other__scan__error__expired), ) @@ -961,7 +963,7 @@ class AppViewModel @Inject constructor( private suspend fun onScanLightning(invoice: LightningInvoice, scanResult: String) { if (invoice.isExpired) { toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.other__scan_err_decoding), description = context.getString(R.string.other__scan__error__expired), ) @@ -973,7 +975,7 @@ class AppViewModel @Inject constructor( if (!lightningRepo.canSend(invoice.amountSatoshis)) { toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__error_insufficient_funds_title), description = context.getString(R.string.wallet__error_insufficient_funds_msg) ) @@ -1017,7 +1019,7 @@ class AppViewModel @Inject constructor( if (!lightningRepo.canSend(minSendable)) { toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_pay_error), description = context.getString(R.string.other__lnurl_pay_error_no_capacity), ) @@ -1065,7 +1067,7 @@ class AppViewModel @Inject constructor( if (minWithdrawable > maxWithdrawable) { toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_withdr_error), description = context.getString(R.string.other__lnurl_withdr_error_minmax) ) @@ -1114,14 +1116,14 @@ class AppViewModel @Inject constructor( domain = domain, ).onFailure { toast( - type = Toast.ToastType.WARNING, + type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_auth_error), description = context.getString(R.string.other__lnurl_auth_error_msg) .replace("{raw}", it.message?.takeIf { m -> m.isNotBlank() } ?: it.javaClass.simpleName), ) }.onSuccess { toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.other__lnurl_auth_success_title), description = when (domain.isNotBlank()) { true -> context.getString(R.string.other__lnurl_auth_success_msg_domain) @@ -1153,7 +1155,7 @@ class AppViewModel @Inject constructor( // val appNetwork = Env.network.toCoreNetworkType() // if (network != appNetwork) { // toast( - // type = Toast.ToastType.WARNING, + // type = ToastType.WARNING, // title = context.getString(R.string.other__qr_error_network_header), // description = context.getString(R.string.other__qr_error_network_text) // .replace("{selectedNetwork}", appNetwork.name) @@ -1358,7 +1360,7 @@ class AppViewModel @Inject constructor( }.onFailure { e -> Logger.error(msg = "Error sending onchain payment", e = e, context = TAG) toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.wallet__error_sending_title), description = e.message ?: context.getString(R.string.common__error_body) ) @@ -1450,7 +1452,7 @@ class AppViewModel @Inject constructor( paymentRequest = invoice ).onSuccess { toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.other__lnurl_withdr_success_title), description = context.getString(R.string.other__lnurl_withdr_success_msg), ) @@ -1793,7 +1795,7 @@ class AppViewModel @Inject constructor( val currentToast: StateFlow = toastManager.currentToast fun toast( - type: Toast.ToastType, + type: ToastType, title: String, description: String? = null, autoHide: Boolean = true, @@ -1814,7 +1816,7 @@ class AppViewModel @Inject constructor( fun toast(error: Throwable) { toast( - type = Toast.ToastType.ERROR, + type = ToastType.ERROR, title = context.getString(R.string.common__error), description = error.message ?: context.getString(R.string.common__error_body) ) @@ -1867,7 +1869,7 @@ class AppViewModel @Inject constructor( if (newAttempts <= 0) { toast( - type = Toast.ToastType.SUCCESS, + type = ToastType.SUCCESS, title = context.getString(R.string.security__wiped_title), description = context.getString(R.string.security__wiped_message), ) diff --git a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt index 30a5bd849..11dae996b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt @@ -18,13 +18,12 @@ import to.bitkit.env.Env import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType -import to.bitkit.models.Toast import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LogsRepo import to.bitkit.repositories.WalletRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -41,29 +40,26 @@ class DevSettingsViewModel @Inject constructor( private val cacheStore: CacheStore, private val blocktankRepo: BlocktankRepo, private val appDb: AppDb, + private val toaster: Toaster, ) : ViewModel() { fun openChannel() = viewModelScope.launch { val peer = lightningRepo.getPeers()?.firstOrNull() if (peer == null) { - ToastEventBus.send(type = Toast.ToastType.WARNING, title = "No peer connected") + toaster.warning("No peer connected") return@launch } lightningRepo.openChannel(peer, 50_000u, 25_000u) - .onSuccess { - ToastEventBus.send(type = Toast.ToastType.INFO, title = "Channel pending") - } - .onFailure { ToastEventBus.send(it) } + .onSuccess { toaster.info("Channel pending") } + .onFailure { toaster.error(it) } } fun registerForNotifications() = viewModelScope.launch { lightningRepo.registerForNotifications() - .onSuccess { - ToastEventBus.send(type = Toast.ToastType.INFO, title = "Registered for notifications") - } - .onFailure { ToastEventBus.send(it) } + .onSuccess { toaster.info("Registered for notifications") } + .onFailure { toaster.error(it) } } fun testLspNotification() = viewModelScope.launch { @@ -74,9 +70,9 @@ class DevSettingsViewModel @Inject constructor( notificationType = "incomingHtlc", customUrl = Env.blocktankNotificationApiUrl, ) - ToastEventBus.send(type = Toast.ToastType.INFO, title = "LSP notification sent to this device") + toaster.info("LSP notification sent to this device") }.onFailure { - ToastEventBus.send(type = Toast.ToastType.WARNING, title = "Error testing LSP notification") + toaster.warning("Error testing LSP notification") } } @@ -103,10 +99,9 @@ class DevSettingsViewModel @Inject constructor( logsRepo.zipLogsForSharing() .onSuccess { uri -> onReady(uri) } .onFailure { - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = context.getString(R.string.lightning__error_logs), - description = context.getString(R.string.lightning__error_logs_description), + toaster.warning( + R.string.lightning__error_logs, + R.string.lightning__error_logs_description, ) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt index d8aced251..1ade94f06 100644 --- a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt @@ -17,10 +17,9 @@ import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.data.backup.VssBackupClient import to.bitkit.di.BgDispatcher import to.bitkit.ext.of -import to.bitkit.models.Toast import to.bitkit.repositories.LightningRepo import to.bitkit.services.NetworkGraphInfo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import java.io.File import javax.inject.Inject @@ -31,6 +30,7 @@ class LdkDebugViewModel @Inject constructor( @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningRepo: LightningRepo, private val vssBackupClient: VssBackupClient, + private val toaster: Toaster, ) : ViewModel() { private val _uiState = MutableStateFlow(LdkDebugUiState()) @@ -43,12 +43,7 @@ class LdkDebugViewModel @Inject constructor( fun addPeer() { val uri = _uiState.value.nodeUri.trim() if (uri.isEmpty()) { - viewModelScope.launch { - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = "Please enter a node URI", - ) - } + viewModelScope.launch { toaster.warning("Please enter a node URI") } return } connectPeer(uri) @@ -60,12 +55,7 @@ class LdkDebugViewModel @Inject constructor( val pastedUri = clipData?.getItemAt(0)?.text?.toString()?.trim() if (pastedUri.isNullOrEmpty()) { - viewModelScope.launch { - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = "Clipboard is empty", - ) - } + viewModelScope.launch { toaster.warning("Clipboard is empty") } return } @@ -81,26 +71,15 @@ class LdkDebugViewModel @Inject constructor( lightningRepo.connectPeer(peer) }.onSuccess { result -> result.onSuccess { - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Peer connected", - ) + toaster.info("Peer connected") _uiState.update { it.copy(nodeUri = "") } }.onFailure { e -> Logger.error("Failed to connect peer", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to connect peer", - description = e.message, - ) + toaster.error("Failed to connect peer", e.message) } }.onFailure { e -> Logger.error("Failed to parse peer URI", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Invalid node URI format", - description = e.message, - ) + toaster.error("Invalid node URI format", e.message) } _uiState.update { it.copy(isLoading = false) } } @@ -118,15 +97,9 @@ class LdkDebugViewModel @Inject constructor( context = TAG ) _uiState.update { it.copy(networkGraphInfo = info) } - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Network graph info logged", - ) + toaster.info("Network graph info logged") } else { - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = "Failed to get network graph info", - ) + toaster.warning("Failed to get network graph info") } } } @@ -137,18 +110,11 @@ class LdkDebugViewModel @Inject constructor( val outputDir = context.cacheDir.absolutePath lightningRepo.exportNetworkGraphToFile(outputDir).onSuccess { file -> Logger.info("Network graph exported to: ${file.absolutePath}", context = TAG) - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Network graph exported", - ) + toaster.info("Network graph exported") onFileReady(file) }.onFailure { e -> Logger.error("Failed to export network graph", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to export network graph", - description = e.message, - ) + toaster.error("Failed to export network graph", e.message) } _uiState.update { it.copy(isLoading = false) } } @@ -160,17 +126,10 @@ class LdkDebugViewModel @Inject constructor( vssBackupClient.listKeys().onSuccess { keys -> Logger.info("VSS keys: ${keys.size}", context = TAG) _uiState.update { it.copy(vssKeys = keys) } - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Found ${keys.size} VSS key(s)", - ) + toaster.info("Found ${keys.size} VSS key(s)") }.onFailure { e -> Logger.error("Failed to list VSS keys", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to list VSS keys", - description = e.message, - ) + toaster.error("Failed to list VSS keys", e.message) } _uiState.update { it.copy(isLoading = false) } } @@ -182,17 +141,10 @@ class LdkDebugViewModel @Inject constructor( vssBackupClient.deleteAllKeys().onSuccess { deletedCount -> Logger.info("Deleted $deletedCount VSS keys", context = TAG) _uiState.update { it.copy(vssKeys = emptyList()) } - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Deleted $deletedCount VSS key(s)", - ) + toaster.info("Deleted $deletedCount VSS key(s)") }.onFailure { e -> Logger.error("Failed to delete VSS keys", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to delete VSS keys", - description = e.message, - ) + toaster.error("Failed to delete VSS keys", e.message) } _uiState.update { it.copy(isLoading = false) } } @@ -208,24 +160,14 @@ class LdkDebugViewModel @Inject constructor( _uiState.update { state -> state.copy(vssKeys = state.vssKeys.filter { it.key != key }) } - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Deleted key: $key", - ) + toaster.info("Deleted key: $key") } else { - ToastEventBus.send( - type = Toast.ToastType.WARNING, - title = "Key not found: $key", - ) + toaster.warning("Key not found: $key") } } .onFailure { e -> Logger.error("Failed to delete VSS key: $key", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to delete key", - description = e.message, - ) + toaster.error("Failed to delete key", e.message) } _uiState.update { it.copy(isLoading = false) } } @@ -237,18 +179,11 @@ class LdkDebugViewModel @Inject constructor( lightningRepo.restartNode() .onSuccess { Logger.info("Node restarted successfully", context = TAG) - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = "Node restarted", - ) + toaster.info("Node restarted") } .onFailure { e -> Logger.error("Failed to restart node", e, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = "Failed to restart node", - description = e.message, - ) + toaster.error("Failed to restart node", e.message) } _uiState.update { it.copy(isLoading = false) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 9a83e0472..9c0e72d41 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -29,7 +29,6 @@ import to.bitkit.R import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.ext.amountOnClose -import to.bitkit.models.Toast import to.bitkit.models.TransactionSpeed import to.bitkit.models.TransferType import to.bitkit.models.safe @@ -37,7 +36,7 @@ import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.TransferRepo import to.bitkit.repositories.WalletRepo -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject import kotlin.math.min @@ -62,6 +61,7 @@ class TransferViewModel @Inject constructor( private val cacheStore: CacheStore, private val transferRepo: TransferRepo, private val clock: Clock, + private val toaster: Toaster, ) : ViewModel() { private val _spendingUiState = MutableStateFlow(TransferToSpendingUiState()) val spendingUiState = _spendingUiState.asStateFlow() @@ -213,7 +213,7 @@ class TransferViewModel @Inject constructor( launch { watchOrder(order.id) } } .onFailure { error -> - ToastEventBus.send(error) + toaster.error(error) } } } @@ -458,10 +458,9 @@ class TransferViewModel @Inject constructor( if (nonTrustedChannels.isEmpty()) { channelsToClose = emptyList() Logger.error("Cannot force close channels with trusted peer", context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.lightning__force_failed_title), - description = context.getString(R.string.lightning__force_failed_msg) + toaster.error( + R.string.lightning__force_failed_title, + R.string.lightning__force_failed_msg ) return@runCatching } @@ -483,25 +482,22 @@ class TransferViewModel @Inject constructor( val initMsg = context.getString(R.string.lightning__force_init_msg) val skippedMsg = context.getString(R.string.lightning__force_channels_skipped) val description = if (trustedChannels.isNotEmpty()) "$initMsg $skippedMsg" else initMsg - ToastEventBus.send( - type = Toast.ToastType.LIGHTNING, + toaster.lightning( title = context.getString(R.string.lightning__force_init_title), description = description, ) } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.lightning__force_failed_title), - description = context.getString(R.string.lightning__force_failed_msg) + toaster.error( + R.string.lightning__force_failed_title, + R.string.lightning__force_failed_msg ) } }.onFailure { Logger.error("Force close failed", e = it, context = TAG) - ToastEventBus.send( - type = Toast.ToastType.ERROR, - title = context.getString(R.string.lightning__force_failed_title), - description = context.getString(R.string.lightning__force_failed_msg) + toaster.error( + R.string.lightning__force_failed_title, + R.string.lightning__force_failed_msg ) } _isForceTransferLoading.value = false diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 471fce9d5..04436af6e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -26,7 +26,6 @@ import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.di.BgDispatcher -import to.bitkit.models.Toast import to.bitkit.repositories.BackupRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo @@ -35,7 +34,7 @@ import to.bitkit.repositories.SyncSource import to.bitkit.repositories.WalletRepo import to.bitkit.services.MigrationService import to.bitkit.ui.onboarding.LOADING_MS -import to.bitkit.ui.shared.toast.ToastEventBus +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import to.bitkit.utils.isTxSyncTimeout import javax.inject.Inject @@ -54,6 +53,7 @@ class WalletViewModel @Inject constructor( private val backupRepo: BackupRepo, private val blocktankRepo: BlocktankRepo, private val migrationService: MigrationService, + private val toaster: Toaster, ) : ViewModel() { companion object { private const val TAG = "WalletViewModel" @@ -126,8 +126,7 @@ class WalletViewModel @Inject constructor( Logger.error("RN migration failed", it, context = TAG) migrationService.markMigrationChecked() migrationService.setShowingMigrationLoading(false) - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = "Migration Failed", description = "Please restore your wallet manually using your recovery phrase" ) @@ -255,7 +254,7 @@ class WalletViewModel @Inject constructor( .onFailure { Logger.error("Node startup error", it, context = TAG) if (it !is RecoveryModeError) { - ToastEventBus.send(it) + toaster.error(it) } } } @@ -267,7 +266,7 @@ class WalletViewModel @Inject constructor( lightningRepo.stop() .onFailure { Logger.error("Node stop error", it) - ToastEventBus.send(it) + toaster.error(it) } } } @@ -277,7 +276,7 @@ class WalletViewModel @Inject constructor( .onFailure { Logger.error("Failed to refresh state: ${it.message}", it) if (it is CancellationException || it.isTxSyncTimeout()) return@onFailure - ToastEventBus.send(it) + toaster.error(it) } } @@ -301,15 +300,10 @@ class WalletViewModel @Inject constructor( viewModelScope.launch { lightningRepo.disconnectPeer(peer) .onSuccess { - ToastEventBus.send( - type = Toast.ToastType.INFO, - title = context.getString(R.string.common__success), - description = context.getString(R.string.wallet__peer_disconnected) - ) + toaster.info(R.string.common__success, R.string.wallet__peer_disconnected) } .onFailure { - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = context.getString(R.string.common__error), description = it.message ?: context.getString(R.string.common__error_body) ) @@ -319,8 +313,7 @@ class WalletViewModel @Inject constructor( fun updateBip21Invoice(amountSats: ULong? = walletState.value.bip21AmountSats) = viewModelScope.launch { walletRepo.updateBip21Invoice(amountSats).onFailure { error -> - ToastEventBus.send( - type = Toast.ToastType.ERROR, + toaster.error( title = context.getString(R.string.wallet__error_invoice_update), description = error.message ?: context.getString(R.string.common__error_body) ) @@ -335,7 +328,7 @@ class WalletViewModel @Inject constructor( fun wipeWallet() = viewModelScope.launch(bgDispatcher) { walletRepo.wipeWallet().onFailure { - ToastEventBus.send(it) + toaster.error(it) } } @@ -346,7 +339,7 @@ class WalletViewModel @Inject constructor( backupRepo.scheduleFullBackup() } .onFailure { - ToastEventBus.send(it) + toaster.error(it) } } @@ -358,7 +351,7 @@ class WalletViewModel @Inject constructor( mnemonic = mnemonic, bip39Passphrase = bip39Passphrase, ).onFailure { - ToastEventBus.send(it) + toaster.error(it) } } @@ -366,13 +359,13 @@ class WalletViewModel @Inject constructor( fun addTagToSelected(newTag: String) = viewModelScope.launch { walletRepo.addTagToSelected(newTag).onFailure { - ToastEventBus.send(it) + toaster.error(it) } } fun removeTag(tag: String) = viewModelScope.launch { walletRepo.removeTag(tag).onFailure { - ToastEventBus.send(it) + toaster.error(it) } } diff --git a/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt b/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt index 8cbdcdf19..3a247f567 100644 --- a/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt +++ b/app/src/test/java/to/bitkit/repositories/CurrencyRepoTest.kt @@ -17,6 +17,7 @@ import to.bitkit.models.FxRate import to.bitkit.models.PrimaryDisplay import to.bitkit.services.CurrencyService import to.bitkit.test.BaseUnitTest +import to.bitkit.ui.shared.toast.Toaster import java.math.BigDecimal import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -33,6 +34,7 @@ class CurrencyRepoTest : BaseUnitTest() { private val settingsStore = mock() private val cacheStore = mock() private val clock = mock() + private val toaster = mock() private lateinit var sut: CurrencyRepo @@ -75,6 +77,7 @@ class CurrencyRepoTest : BaseUnitTest() { currencyService = currencyService, settingsStore = settingsStore, cacheStore = cacheStore, + toaster = toaster, enablePolling = false, clock = clock ) diff --git a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt index e80b3f74b..5b6c73a5b 100644 --- a/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/WalletViewModelTest.kt @@ -27,6 +27,7 @@ import to.bitkit.repositories.WalletRepo import to.bitkit.repositories.WalletState import to.bitkit.services.MigrationService import to.bitkit.test.BaseUnitTest +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.viewmodels.RestoreState import to.bitkit.viewmodels.WalletViewModel @@ -41,6 +42,7 @@ class WalletViewModelTest : BaseUnitTest() { private val backupRepo = mock() private val blocktankRepo = mock() private val migrationService = mock() + private val toaster = mock() private val lightningState = MutableStateFlow(LightningState()) private val walletState = MutableStateFlow(WalletState()) @@ -63,6 +65,7 @@ class WalletViewModelTest : BaseUnitTest() { backupRepo = backupRepo, blocktankRepo = blocktankRepo, migrationService = migrationService, + toaster = toaster, ) } @@ -247,6 +250,7 @@ class WalletViewModelTest : BaseUnitTest() { backupRepo = backupRepo, blocktankRepo = blocktankRepo, migrationService = migrationService, + toaster = toaster, ) assertEquals(RestoreState.Initial, testSut.restoreState.value) @@ -287,6 +291,7 @@ class WalletViewModelTest : BaseUnitTest() { backupRepo = backupRepo, blocktankRepo = blocktankRepo, migrationService = migrationService, + toaster = toaster, ) // Trigger restore to put state in non-idle diff --git a/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt b/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt index 703926a78..36597e925 100644 --- a/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt +++ b/app/src/test/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModelTest.kt @@ -17,6 +17,7 @@ import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo import to.bitkit.test.BaseUnitTest import to.bitkit.ui.components.KEY_DELETE +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.viewmodels.SendUiState import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -28,6 +29,7 @@ class SendFeeViewModelTest : BaseUnitTest() { private val currencyRepo: CurrencyRepo = mock() private val walletRepo: WalletRepo = mock() private val context: Context = mock() + private val toaster: Toaster = mock() private val balance = 100_000uL private val fee = 1_000uL @@ -42,7 +44,7 @@ class SendFeeViewModelTest : BaseUnitTest() { whenever(walletRepo.balanceState) .thenReturn(MutableStateFlow(BalanceState(totalOnchainSats = balance))) - sut = SendFeeViewModel(lightningRepo, currencyRepo, walletRepo, context) + sut = SendFeeViewModel(lightningRepo, currencyRepo, walletRepo, context, toaster) } @Test diff --git a/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt b/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt index e36008926..04b7d9502 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AmountInputViewModelTest.kt @@ -28,6 +28,7 @@ import to.bitkit.ui.components.KEY_000 import to.bitkit.ui.components.KEY_DECIMAL import to.bitkit.ui.components.KEY_DELETE import to.bitkit.ui.components.NumberPadType +import to.bitkit.ui.shared.toast.Toaster import kotlin.time.Clock import kotlin.time.Duration.Companion.milliseconds import kotlin.time.ExperimentalTime @@ -42,6 +43,7 @@ class AmountInputViewModelTest : BaseUnitTest() { private val settingsStore = mock() private val cacheStore = mock() private val clock = mock() + private val toaster = mock() @Suppress("SpellCheckingInspection") private val testRates = listOf( @@ -68,6 +70,7 @@ class AmountInputViewModelTest : BaseUnitTest() { currencyService = currencyService, settingsStore = settingsStore, cacheStore = cacheStore, + toaster = toaster, enablePolling = false, clock = clock, ) @@ -819,6 +822,7 @@ class AmountInputViewModelTest : BaseUnitTest() { currencyService = currencyService, settingsStore = settingsStore, cacheStore = cacheStore, + toaster = toaster, enablePolling = false, clock = clock, ) From 2557f16085bc4f43594894127b24c823fea5d2bf Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:33:46 +0100 Subject: [PATCH 02/35] refactor: rename toast description param to body Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/Toast.kt | 8 +-- .../java/to/bitkit/repositories/BackupRepo.kt | 2 +- .../to/bitkit/repositories/CurrencyRepo.kt | 2 +- app/src/main/java/to/bitkit/ui/ContentView.kt | 4 +- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 2 +- .../bitkit/ui/components/IsOnlineTracker.kt | 4 +- .../java/to/bitkit/ui/components/ToastView.kt | 14 +++--- .../ui/screens/scanner/QrScanningScreen.kt | 4 +- .../screens/transfer/SavingsProgressScreen.kt | 4 +- .../transfer/SpendingAdvancedScreen.kt | 2 +- .../screens/transfer/SpendingAmountScreen.kt | 2 +- .../external/ExternalNodeViewModel.kt | 4 +- .../external/LnurlChannelViewModel.kt | 2 +- .../wallets/activity/ActivityDetailScreen.kt | 10 ++-- .../wallets/activity/ActivityExploreScreen.kt | 2 +- .../screens/wallets/send/SendAmountScreen.kt | 2 +- .../wallets/send/SendRecipientScreen.kt | 4 +- .../ui/settings/BlocktankRegtestScreen.kt | 14 +++--- .../to/bitkit/ui/settings/SettingsScreen.kt | 2 +- .../settings/advanced/AddressViewerScreen.kt | 2 +- .../settings/advanced/ElectrumConfigScreen.kt | 4 +- .../advanced/ElectrumConfigViewModel.kt | 4 +- .../ui/settings/advanced/RgsServerScreen.kt | 4 +- .../settings/lightning/ChannelDetailScreen.kt | 2 +- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 46 ++++++++--------- .../java/to/bitkit/viewmodels/AppViewModel.kt | 50 +++++++++---------- .../to/bitkit/viewmodels/TransferViewModel.kt | 8 +-- .../to/bitkit/viewmodels/WalletViewModel.kt | 6 +-- 28 files changed, 107 insertions(+), 107 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index b9688108e..968695d49 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -2,14 +2,11 @@ package to.bitkit.models import androidx.compose.runtime.Stable -@Stable -enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR } - @Stable data class Toast( val type: ToastType, val title: String, - val description: String? = null, + val body: String? = null, val autoHide: Boolean, val visibilityTime: Long = VISIBILITY_TIME_DEFAULT, val testTag: String? = null, @@ -18,3 +15,6 @@ data class Toast( const val VISIBILITY_TIME_DEFAULT = 3000L } } + +@Stable +enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR } diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index e53ddfcde..cb1476ecf 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -375,7 +375,7 @@ class BackupRepo @Inject constructor( scope.launch { toaster.error( title = context.getString(R.string.settings__backup__failed_title), - description = context.getString(R.string.settings__backup__failed_message).formatPlural( + body = context.getString(R.string.settings__backup__failed_message).formatPlural( mapOf("interval" to (BACKUP_CHECK_INTERVAL / MINUTE_IN_MS)) ), ) diff --git a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt index 1456fcd11..68db7f2c2 100644 --- a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt @@ -94,7 +94,7 @@ class CurrencyRepo @Inject constructor( if (isStale) { toaster.error( title = "Rates currently unavailable", - description = "An error has occurred. Please try again later." + body = "An error has occurred. Please try again later." ) } } diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 762bb8ee8..c65396113 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -626,11 +626,11 @@ private fun RootNavHost( onBackClick = { navController.popBackStack() }, onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, toastException = { appViewModel.toast(it) }, - toast = { title, description -> + toast = { title, body -> appViewModel.toast( type = ToastType.ERROR, title = title, - description = description + body = body ) }, ) diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 55096ff34..8c394873b 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -94,7 +94,7 @@ fun NodeInfoScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), - description = text + body = text ) }, ) diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index ae21265d2..83264d790 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -33,7 +33,7 @@ fun IsOnlineTracker( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.other__connection_back_title), - description = context.getString(R.string.other__connection_back_msg), + body = context.getString(R.string.other__connection_back_msg), ) } @@ -41,7 +41,7 @@ fun IsOnlineTracker( app.toast( type = ToastType.WARNING, title = context.getString(R.string.other__connection_issue), - description = context.getString(R.string.other__connection_issue_explain), + body = context.getString(R.string.other__connection_issue_explain), ) } diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index 4632001bc..bdf2cf38c 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -226,9 +226,9 @@ fun ToastView( text = toast.title, color = tintColor, ) - toast.description?.let { description -> + toast.body?.let { body -> Caption( - text = description, + text = body, color = Colors.White ) } @@ -325,7 +325,7 @@ private fun ToastViewPreview() { toast = Toast( type = ToastType.WARNING, title = "You're still offline", - description = "Check your connection to keep using Bitkit.", + body = "Check your connection to keep using Bitkit.", autoHide = true, ), onDismiss = {}, @@ -334,7 +334,7 @@ private fun ToastViewPreview() { toast = Toast( type = ToastType.LIGHTNING, title = "Instant Payments Ready", - description = "You can now pay anyone, anywhere, instantly.", + body = "You can now pay anyone, anywhere, instantly.", autoHide = true, ), onDismiss = {}, @@ -343,7 +343,7 @@ private fun ToastViewPreview() { toast = Toast( type = ToastType.SUCCESS, title = "You're Back Online!", - description = "Successfully reconnected to the Internet.", + body = "Successfully reconnected to the Internet.", autoHide = true, ), onDismiss = {}, @@ -352,7 +352,7 @@ private fun ToastViewPreview() { toast = Toast( type = ToastType.INFO, title = "General Message", - description = "Used for neutral content to inform the user.", + body = "Used for neutral content to inform the user.", autoHide = false, ), onDismiss = {}, @@ -361,7 +361,7 @@ private fun ToastViewPreview() { toast = Toast( type = ToastType.ERROR, title = "Error Toast", - description = "This is a toast message.", + body = "This is a toast message.", autoHide = true, ), onDismiss = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 7aad87e56..264ee2353 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -152,7 +152,7 @@ fun QrScanningScreen( app.toast( type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), - description = context.getString(R.string.other__qr_error_text), + body = context.getString(R.string.other__qr_error_text), ) } } @@ -258,7 +258,7 @@ private fun handlePaste( app.toast( type = ToastType.WARNING, title = context.getString(R.string.wallet__send_clipboard_empty_title), - description = context.getString(R.string.wallet__send_clipboard_empty_text), + body = context.getString(R.string.wallet__send_clipboard_empty_text), ) } setScanResult(clipboard) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 1b2269d82..320066d63 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -73,7 +73,7 @@ fun SavingsProgressScreen( app.toast( type = ToastType.ERROR, title = context.getString(R.string.lightning__close_error), - description = context.getString(R.string.lightning__close_error_msg), + body = context.getString(R.string.lightning__close_error_msg), ) onTransferUnavailable() } else { @@ -84,7 +84,7 @@ fun SavingsProgressScreen( app.toast( type = ToastType.ERROR, title = context.getString(R.string.lightning__close_error), - description = context.getString(R.string.lightning__close_error_msg), + body = context.getString(R.string.lightning__close_error_msg), ) onTransferUnavailable() }, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt index 1511ef44a..5d76b4d1f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt @@ -93,7 +93,7 @@ fun SpendingAdvancedScreen( app.toast( type = ToastType.ERROR, title = effect.title, - description = effect.description, + body = effect.body, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt index d313421c2..7e015b9cc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt @@ -72,7 +72,7 @@ fun SpendingAmountScreen( viewModel.transferEffects.collect { effect -> when (effect) { TransferEffect.OnOrderCreated -> onOrderCreated() - is TransferEffect.ToastError -> toast(effect.title, effect.description) + is TransferEffect.ToastError -> toast(effect.title, effect.body) is TransferEffect.ToastException -> toastException(effect.e) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index 63859b1da..71134034c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -97,7 +97,7 @@ class ExternalNodeViewModel @Inject constructor( viewModelScope.launch { toaster.error( title = context.getString(R.string.lightning__spending_amount__error_max__title), - description = context.getString(R.string.lightning__spending_amount__error_max__description) + body = context.getString(R.string.lightning__spending_amount__error_max__description) .replace("{amount}", maxAmount.formatToModernDisplay()), ) } @@ -183,7 +183,7 @@ class ExternalNodeViewModel @Inject constructor( Logger.warn("Error opening channel with peer: '${_uiState.value.peer}': '$error'") toaster.error( title = context.getString(R.string.lightning__error_channel_purchase), - description = context.getString(R.string.lightning__error_channel_setup_msg) + body = context.getString(R.string.lightning__error_channel_setup_msg) .replace("{raw}", error), ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index 9a38a8edb..19b5e6576 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -84,7 +84,7 @@ class LnurlChannelViewModel @Inject constructor( suspend fun errorToast(error: Throwable) { toaster.error( title = context.getString(R.string.other__lnurl_channel_error), - description = error.message ?: "Unknown error", + body = error.message ?: "Unknown error", ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index b0ca70f3e..9ef1f4663 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -230,7 +230,7 @@ fun ActivityDetailScreen( app.toast( type = ToastType.SUCCESS, title = copyToastTitle, - description = text.ellipsisMiddle(40) + body = text.ellipsisMiddle(40) ) }, feeRates = feeRates, @@ -253,7 +253,7 @@ fun ActivityDetailScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.wallet__boost_success_title), - description = context.getString(R.string.wallet__boost_success_msg), + body = context.getString(R.string.wallet__boost_success_msg), testTag = "BoostSuccessToast" ) listViewModel.resync() @@ -263,7 +263,7 @@ fun ActivityDetailScreen( app.toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__boost_error_title), - description = context.getString(R.string.wallet__boost_error_msg), + body = context.getString(R.string.wallet__boost_error_msg), testTag = "BoostFailureToast" ) detailViewModel.onDismissBoostSheet() @@ -272,14 +272,14 @@ fun ActivityDetailScreen( app.toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__send_fee_error), - description = context.getString(R.string.wallet__send_fee_error_max) + body = context.getString(R.string.wallet__send_fee_error_max) ) }, onMinFee = { app.toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__send_fee_error), - description = context.getString(R.string.wallet__send_fee_error_min) + body = context.getString(R.string.wallet__send_fee_error_min) ) } ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index 130a11fc2..7de2fbf5a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -169,7 +169,7 @@ fun ActivityExploreScreen( app.toast( type = ToastType.SUCCESS, title = toastMessage, - description = text.ellipsisMiddle(40), + body = text.ellipsisMiddle(40), ) }, onClickExplore = { txid -> diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index 60a356589..a857fac93 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -111,7 +111,7 @@ fun SendAmountScreen( app?.toast( type = ToastType.INFO, title = context.getString(R.string.wallet__send_max_spending__title), - description = context.getString(R.string.wallet__send_max_spending__description) + body = context.getString(R.string.wallet__send_max_spending__description) ) } amountInputViewModel.setSats(maxSats, currencies) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index 3ff163ade..dcec61ea4 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -142,7 +142,7 @@ fun SendRecipientScreen( app?.toast( type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), - description = context.getString(R.string.other__qr_error_text), + body = context.getString(R.string.other__qr_error_text), ) } } @@ -176,7 +176,7 @@ fun SendRecipientScreen( app?.toast( type = ToastType.ERROR, title = context.getString(R.string.other__qr_error_header), - description = context.getString(R.string.other__camera_init_error) + body = context.getString(R.string.other__camera_init_error) .replace("{message}", it.message.orEmpty()) ) isCameraInitialized = false diff --git a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt index 4965889e5..b0d3406d4 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt @@ -115,14 +115,14 @@ fun BlocktankRegtestScreen( app.toast( type = ToastType.SUCCESS, title = "Success", - description = "Deposit successful. TxID: $txId", + body = "Deposit successful. TxID: $txId", ) }.onFailure { Logger.error("Deposit failed", it) app.toast( type = ToastType.ERROR, title = "Failed to deposit", - description = it.message.orEmpty(), + body = it.message.orEmpty(), ) } @@ -161,14 +161,14 @@ fun BlocktankRegtestScreen( app.toast( type = ToastType.SUCCESS, title = "Success", - description = "Successfully mined $count blocks", + body = "Successfully mined $count blocks", ) }.onFailure { Logger.error("Mining failed", it) app.toast( type = ToastType.ERROR, title = "Failed to mine", - description = it.message.orEmpty(), + body = it.message.orEmpty(), ) } isMining = false @@ -213,14 +213,14 @@ fun BlocktankRegtestScreen( app.toast( type = ToastType.SUCCESS, title = "Success", - description = "Payment successful. ID: $paymentId", + body = "Payment successful. ID: $paymentId", ) }.onFailure { Logger.error("Payment failed", it) app.toast( type = ToastType.ERROR, title = "Failed to pay invoice from LND", - description = it.message.orEmpty(), + body = it.message.orEmpty(), ) } } @@ -280,7 +280,7 @@ fun BlocktankRegtestScreen( app.toast( type = ToastType.SUCCESS, title = "Success", - description = "Channel closed. Closing TxID: $closingTxId" + body = "Channel closed. Closing TxID: $closingTxId" ) }.onFailure { Logger.error("Channel close failed", it) diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index 1893dc944..e57e4e180 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -84,7 +84,7 @@ fun SettingsScreen( R.string.settings__dev_disabled_title } ), - description = context.getString( + body = context.getString( if (newValue) { R.string.settings__dev_enabled_message } else { diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index e65a58bd9..6754fea54 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -80,7 +80,7 @@ fun AddressViewerScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), - description = text, + body = text, ) } ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 1a46b44db..1a06e5556 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -77,7 +77,7 @@ fun ElectrumConfigScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.settings__es__server_updated_title), - description = context.getString(R.string.settings__es__server_updated_message) + body = context.getString(R.string.settings__es__server_updated_message) .replace("{host}", uiState.host) .replace("{port}", uiState.port), testTag = "ElectrumUpdatedToast", @@ -86,7 +86,7 @@ fun ElectrumConfigScreen( app.toast( type = ToastType.WARNING, title = context.getString(R.string.settings__es__server_error), - description = context.getString(R.string.settings__es__server_error_description), + body = context.getString(R.string.settings__es__server_error_description), testTag = "ElectrumErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt index a7c25036d..70b456f10 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt @@ -248,7 +248,7 @@ class ElectrumConfigViewModel @Inject constructor( if (validationError != null) { toaster.warning( title = context.getString(R.string.settings__es__error_peer), - description = validationError, + body = validationError, ) } else { connectToServer() @@ -269,7 +269,7 @@ class ElectrumConfigViewModel @Inject constructor( if (validationError != null) { toaster.warning( title = context.getString(R.string.settings__es__error_peer), - description = validationError, + body = validationError, ) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 28d9d08d0..5072b5f15 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -70,14 +70,14 @@ fun RgsServerScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.settings__rgs__update_success_title), - description = context.getString(R.string.settings__rgs__update_success_description), + body = context.getString(R.string.settings__rgs__update_success_description), testTag = "RgsUpdatedToast", ) } else { app.toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__ldk_start_error_title), - description = result.exceptionOrNull()?.message ?: "Unknown error", + body = result.exceptionOrNull()?.message ?: "Unknown error", testTag = "RgsErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index fffa115db..2cd13a0fb 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -131,7 +131,7 @@ fun ChannelDetailScreen( app.toast( type = ToastType.SUCCESS, title = context.getString(R.string.common__copied), - description = text, + body = text, ) }, onOpenUrl = { txId -> diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 096b381f6..08029289a 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -24,7 +24,7 @@ class Toaster @Inject constructor( private suspend fun emit( type: ToastType, title: String, - description: String? = null, + body: String? = null, autoHide: Boolean = true, visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, testTag: String? = null, @@ -33,7 +33,7 @@ class Toaster @Inject constructor( Toast( type = type, title = title, - description = description, + body = body, autoHide = autoHide, visibilityTime = visibilityTime, testTag = testTag, @@ -44,18 +44,18 @@ class Toaster @Inject constructor( // region Success suspend fun success( title: String, - description: String? = null, + body: String? = null, testTag: String? = null, - ) = emit(ToastType.SUCCESS, title, description, testTag = testTag) + ) = emit(ToastType.SUCCESS, title, body, testTag = testTag) suspend fun success( @StringRes titleRes: Int, - @StringRes descriptionRes: Int? = null, + @StringRes bodyRes: Int? = null, testTag: String? = null, ) = emit( type = ToastType.SUCCESS, title = context.getString(titleRes), - description = descriptionRes?.let { context.getString(it) }, + body = bodyRes?.let { context.getString(it) }, testTag = testTag, ) // endregion @@ -63,18 +63,18 @@ class Toaster @Inject constructor( // region Info suspend fun info( title: String, - description: String? = null, + body: String? = null, testTag: String? = null, - ) = emit(ToastType.INFO, title, description, testTag = testTag) + ) = emit(ToastType.INFO, title, body, testTag = testTag) suspend fun info( @StringRes titleRes: Int, - @StringRes descriptionRes: Int? = null, + @StringRes bodyRes: Int? = null, testTag: String? = null, ) = emit( type = ToastType.INFO, title = context.getString(titleRes), - description = descriptionRes?.let { context.getString(it) }, + body = bodyRes?.let { context.getString(it) }, testTag = testTag, ) // endregion @@ -82,18 +82,18 @@ class Toaster @Inject constructor( // region Lightning suspend fun lightning( title: String, - description: String? = null, + body: String? = null, testTag: String? = null, - ) = emit(ToastType.LIGHTNING, title, description, testTag = testTag) + ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) suspend fun lightning( @StringRes titleRes: Int, - @StringRes descriptionRes: Int? = null, + @StringRes bodyRes: Int? = null, testTag: String? = null, ) = emit( type = ToastType.LIGHTNING, title = context.getString(titleRes), - description = descriptionRes?.let { context.getString(it) }, + body = bodyRes?.let { context.getString(it) }, testTag = testTag, ) // endregion @@ -101,18 +101,18 @@ class Toaster @Inject constructor( // region Warning suspend fun warning( title: String, - description: String? = null, + body: String? = null, testTag: String? = null, - ) = emit(ToastType.WARNING, title, description, testTag = testTag) + ) = emit(ToastType.WARNING, title, body, testTag = testTag) suspend fun warning( @StringRes titleRes: Int, - @StringRes descriptionRes: Int? = null, + @StringRes bodyRes: Int? = null, testTag: String? = null, ) = emit( type = ToastType.WARNING, title = context.getString(titleRes), - description = descriptionRes?.let { context.getString(it) }, + body = bodyRes?.let { context.getString(it) }, testTag = testTag, ) // endregion @@ -120,25 +120,25 @@ class Toaster @Inject constructor( // region Error suspend fun error( title: String, - description: String? = null, + body: String? = null, testTag: String? = null, - ) = emit(ToastType.ERROR, title, description, testTag = testTag) + ) = emit(ToastType.ERROR, title, body, testTag = testTag) suspend fun error( @StringRes titleRes: Int, - @StringRes descriptionRes: Int? = null, + @StringRes bodyRes: Int? = null, testTag: String? = null, ) = emit( type = ToastType.ERROR, title = context.getString(titleRes), - description = descriptionRes?.let { context.getString(it) }, + body = bodyRes?.let { context.getString(it) }, testTag = testTag, ) suspend fun error(throwable: Throwable) = emit( type = ToastType.ERROR, title = context.getString(R.string.common__error), - description = throwable.message ?: context.getString(R.string.common__error_body), + body = throwable.message ?: context.getString(R.string.common__error_body), ) // endregion } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index d148c609b..8324db0c3 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -233,7 +233,7 @@ class AppViewModel @Inject constructor( init { viewModelScope.launch { toaster.events.collect { - toast(it.type, it.title, it.description, it.autoHide, it.visibilityTime) + toast(it.type, it.title, it.body, it.autoHide, it.visibilityTime) } } viewModelScope.launch { @@ -453,7 +453,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = "Migration Warning", - description = "Migration completed but node restart failed. Please restart the app." + body = "Migration completed but node restart failed. Please restart the app." ) } @@ -537,7 +537,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.LIGHTNING, title = context.getString(R.string.lightning__channel_opened_title), - description = context.getString(R.string.lightning__channel_opened_msg), + body = context.getString(R.string.lightning__channel_opened_msg), testTag = "SpendingBalanceReadyToast", ) } @@ -547,7 +547,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.wallet__toast_transaction_removed_title), - description = context.getString(R.string.wallet__toast_transaction_removed_description), + body = context.getString(R.string.wallet__toast_transaction_removed_description), testTag = "TransactionRemovedToast", ) } @@ -562,7 +562,7 @@ class AppViewModel @Inject constructor( private fun notifyTransactionUnconfirmed() = toast( type = ToastType.WARNING, title = context.getString(R.string.wallet__toast_transaction_unconfirmed_title), - description = context.getString(R.string.wallet__toast_transaction_unconfirmed_description), + body = context.getString(R.string.wallet__toast_transaction_unconfirmed_description), testTag = "TransactionUnconfirmedToast", ) @@ -574,7 +574,7 @@ class AppViewModel @Inject constructor( true -> R.string.wallet__toast_received_transaction_replaced_title else -> R.string.wallet__toast_transaction_replaced_title }.let { context.getString(it) }, - description = when (isReceive) { + body = when (isReceive) { true -> R.string.wallet__toast_received_transaction_replaced_description else -> R.string.wallet__toast_transaction_replaced_description }.let { context.getString(it) }, @@ -588,7 +588,7 @@ class AppViewModel @Inject constructor( private fun notifyPaymentFailed() = toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__toast_payment_failed_title), - description = context.getString(R.string.wallet__toast_payment_failed_description), + body = context.getString(R.string.wallet__toast_payment_failed_description), testTag = "PaymentFailedToast", ) @@ -779,7 +779,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__lnurl_pay__error_min__title), - description = context.getString(R.string.wallet__lnurl_pay__error_min__description) + body = context.getString(R.string.wallet__lnurl_pay__error_min__description) .replace("{amount}", minSendable.toString()), testTag = "LnurlPayAmountTooLowToast", ) @@ -834,7 +834,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.wallet__send_clipboard_empty_title), - description = context.getString(R.string.wallet__send_clipboard_empty_text), + body = context.getString(R.string.wallet__send_clipboard_empty_text), ) return } @@ -879,7 +879,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.other__scan_err_decoding), - description = context.getString(R.string.other__scan_err_interpret_title), + body = context.getString(R.string.other__scan_err_interpret_title), ) } } @@ -896,7 +896,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.other__scan_err_decoding), - description = context.getString(R.string.other__scan__error__expired), + body = context.getString(R.string.other__scan__error__expired), ) Logger.debug( @@ -965,7 +965,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.other__scan_err_decoding), - description = context.getString(R.string.other__scan__error__expired), + body = context.getString(R.string.other__scan__error__expired), ) return } @@ -977,7 +977,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__error_insufficient_funds_title), - description = context.getString(R.string.wallet__error_insufficient_funds_msg) + body = context.getString(R.string.wallet__error_insufficient_funds_msg) ) return } @@ -1021,7 +1021,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_pay_error), - description = context.getString(R.string.other__lnurl_pay_error_no_capacity), + body = context.getString(R.string.other__lnurl_pay_error_no_capacity), ) return } @@ -1069,7 +1069,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_withdr_error), - description = context.getString(R.string.other__lnurl_withdr_error_minmax) + body = context.getString(R.string.other__lnurl_withdr_error_minmax) ) return } @@ -1118,14 +1118,14 @@ class AppViewModel @Inject constructor( toast( type = ToastType.WARNING, title = context.getString(R.string.other__lnurl_auth_error), - description = context.getString(R.string.other__lnurl_auth_error_msg) + body = context.getString(R.string.other__lnurl_auth_error_msg) .replace("{raw}", it.message?.takeIf { m -> m.isNotBlank() } ?: it.javaClass.simpleName), ) }.onSuccess { toast( type = ToastType.SUCCESS, title = context.getString(R.string.other__lnurl_auth_success_title), - description = when (domain.isNotBlank()) { + body = when (domain.isNotBlank()) { true -> context.getString(R.string.other__lnurl_auth_success_msg_domain) .replace("{domain}", domain) @@ -1157,7 +1157,7 @@ class AppViewModel @Inject constructor( // toast( // type = ToastType.WARNING, // title = context.getString(R.string.other__qr_error_network_header), - // description = context.getString(R.string.other__qr_error_network_text) + // body = context.getString(R.string.other__qr_error_network_text) // .replace("{selectedNetwork}", appNetwork.name) // .replace("{dataNetwork}", network.name), // ) @@ -1362,7 +1362,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.wallet__error_sending_title), - description = e.message ?: context.getString(R.string.common__error_body) + body = e.message ?: context.getString(R.string.common__error_body) ) hideSheet() } @@ -1454,7 +1454,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.SUCCESS, title = context.getString(R.string.other__lnurl_withdr_success_title), - description = context.getString(R.string.other__lnurl_withdr_success_msg), + body = context.getString(R.string.other__lnurl_withdr_success_msg), ) hideSheet() _sendUiState.update { it.copy(isLoading = false) } @@ -1797,7 +1797,7 @@ class AppViewModel @Inject constructor( fun toast( type: ToastType, title: String, - description: String? = null, + body: String? = null, autoHide: Boolean = true, visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, testTag: String? = null, @@ -1806,7 +1806,7 @@ class AppViewModel @Inject constructor( Toast( type = type, title = title, - description = description, + body = body, autoHide = autoHide, visibilityTime = visibilityTime, testTag = testTag, @@ -1818,7 +1818,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.ERROR, title = context.getString(R.string.common__error), - description = error.message ?: context.getString(R.string.common__error_body) + body = error.message ?: context.getString(R.string.common__error_body) ) } @@ -1826,7 +1826,7 @@ class AppViewModel @Inject constructor( toast( type = toast.type, title = toast.title, - description = toast.description, + body = toast.body, autoHide = toast.autoHide, visibilityTime = toast.visibilityTime ) @@ -1871,7 +1871,7 @@ class AppViewModel @Inject constructor( toast( type = ToastType.SUCCESS, title = context.getString(R.string.security__wiped_title), - description = context.getString(R.string.security__wiped_message), + body = context.getString(R.string.security__wiped_message), ) delay(250) // small delay for UI feedback mainScreenEffect(MainScreenEffect.WipeWallet) diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 9c0e72d41..215821433 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -93,7 +93,7 @@ class TransferViewModel @Inject constructor( setTransferEffect( TransferEffect.ToastError( title = context.getString(R.string.lightning__spending_amount__error_max__title), - description = context.getString( + body = context.getString( R.string.lightning__spending_amount__error_max__description_zero ), ) @@ -481,10 +481,10 @@ class TransferViewModel @Inject constructor( Logger.info("Force close initiated successfully for all channels", context = TAG) val initMsg = context.getString(R.string.lightning__force_init_msg) val skippedMsg = context.getString(R.string.lightning__force_channels_skipped) - val description = if (trustedChannels.isNotEmpty()) "$initMsg $skippedMsg" else initMsg + val bodyText = if (trustedChannels.isNotEmpty()) "$initMsg $skippedMsg" else initMsg toaster.lightning( title = context.getString(R.string.lightning__force_init_title), - description = description, + body = bodyText, ) } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) @@ -566,6 +566,6 @@ data class TransferValues( sealed interface TransferEffect { data object OnOrderCreated : TransferEffect data class ToastException(val e: Throwable) : TransferEffect - data class ToastError(val title: String, val description: String) : TransferEffect + data class ToastError(val title: String, val body: String) : TransferEffect } // endregion diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 04436af6e..ffa54790d 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -128,7 +128,7 @@ class WalletViewModel @Inject constructor( migrationService.setShowingMigrationLoading(false) toaster.error( title = "Migration Failed", - description = "Please restore your wallet manually using your recovery phrase" + body = "Please restore your wallet manually using your recovery phrase" ) } } @@ -305,7 +305,7 @@ class WalletViewModel @Inject constructor( .onFailure { toaster.error( title = context.getString(R.string.common__error), - description = it.message ?: context.getString(R.string.common__error_body) + body = it.message ?: context.getString(R.string.common__error_body) ) } } @@ -315,7 +315,7 @@ class WalletViewModel @Inject constructor( walletRepo.updateBip21Invoice(amountSats).onFailure { error -> toaster.error( title = context.getString(R.string.wallet__error_invoice_update), - description = error.message ?: context.getString(R.string.common__error_body) + body = error.message ?: context.getString(R.string.common__error_body) ) } } From 26092e3ebe1ffa7abd468756abfda87f5390ac62 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:36:13 +0100 Subject: [PATCH 03/35] refactor: use Duration for toast visibility Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/Toast.kt | 6 ++++-- .../java/to/bitkit/ui/shared/toast/ToastQueueManager.kt | 7 ++++--- app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt | 5 +++-- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 9 +++++---- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index 968695d49..ca9c7eb1e 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -1,6 +1,8 @@ package to.bitkit.models import androidx.compose.runtime.Stable +import kotlin.time.Duration +import kotlin.time.Duration.Companion.seconds @Stable data class Toast( @@ -8,11 +10,11 @@ data class Toast( val title: String, val body: String? = null, val autoHide: Boolean, - val visibilityTime: Long = VISIBILITY_TIME_DEFAULT, + val duration: Duration = DURATION_DEFAULT, val testTag: String? = null, ) { companion object { - const val VISIBILITY_TIME_DEFAULT = 3000L + val DURATION_DEFAULT: Duration = 3.seconds } } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt index 94af44aa3..f8beb6b29 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.models.Toast +import kotlin.time.Duration private const val MAX_QUEUE_SIZE = 5 @@ -81,7 +82,7 @@ class ToastQueueManager(private val scope: CoroutineScope) { if (isPaused && toast != null) { isPaused = false if (toast.autoHide) { - startTimer(toast.visibilityTime) + startTimer(toast.duration) } } } @@ -108,11 +109,11 @@ class ToastQueueManager(private val scope: CoroutineScope) { // Start auto-hide timer if enabled if (nextToast.autoHide) { - startTimer(nextToast.visibilityTime) + startTimer(nextToast.duration) } } - private fun startTimer(duration: Long) { + private fun startTimer(duration: Duration) { cancelTimer() timerJob = scope.launch { delay(duration) diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 08029289a..16fe33773 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -11,6 +11,7 @@ import to.bitkit.models.Toast import to.bitkit.models.ToastType import javax.inject.Inject import javax.inject.Singleton +import kotlin.time.Duration @Suppress("TooManyFunctions") @Singleton @@ -26,7 +27,7 @@ class Toaster @Inject constructor( title: String, body: String? = null, autoHide: Boolean = true, - visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, + duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, ) { _events.emit( @@ -35,7 +36,7 @@ class Toaster @Inject constructor( title = title, body = body, autoHide = autoHide, - visibilityTime = visibilityTime, + duration = duration, testTag = testTag, ) ) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 8324db0c3..b5acc7735 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -46,6 +46,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout +import kotlin.time.Duration import org.lightningdevkit.ldknode.ChannelDataMigration import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.PaymentId @@ -233,7 +234,7 @@ class AppViewModel @Inject constructor( init { viewModelScope.launch { toaster.events.collect { - toast(it.type, it.title, it.body, it.autoHide, it.visibilityTime) + toast(it.type, it.title, it.body, it.autoHide, it.duration) } } viewModelScope.launch { @@ -1799,7 +1800,7 @@ class AppViewModel @Inject constructor( title: String, body: String? = null, autoHide: Boolean = true, - visibilityTime: Long = Toast.VISIBILITY_TIME_DEFAULT, + duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, ) { toastManager.enqueue( @@ -1808,7 +1809,7 @@ class AppViewModel @Inject constructor( title = title, body = body, autoHide = autoHide, - visibilityTime = visibilityTime, + duration = duration, testTag = testTag, ) ) @@ -1828,7 +1829,7 @@ class AppViewModel @Inject constructor( title = toast.title, body = toast.body, autoHide = toast.autoHide, - visibilityTime = toast.visibilityTime + duration = toast.duration ) } From e3679ad6ad4ad81dd8245c548da2fbf6f04e0cc9 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:41:44 +0100 Subject: [PATCH 04/35] refactor: remove Context from Toaster Co-Authored-By: Claude Opus 4.5 --- .../recovery/RecoveryMnemonicViewModel.kt | 2 +- .../ui/screens/recovery/RecoveryViewModel.kt | 15 +++- .../external/ExternalNodeViewModel.kt | 12 ++- .../external/LnurlChannelViewModel.kt | 4 +- .../screens/wallets/send/SendFeeViewModel.kt | 10 ++- .../backups/BackupNavSheetViewModel.kt | 10 ++- .../LightningConnectionsViewModel.kt | 12 +-- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 78 +------------------ .../bitkit/viewmodels/DevSettingsViewModel.kt | 4 +- .../to/bitkit/viewmodels/TransferViewModel.kt | 12 +-- .../to/bitkit/viewmodels/WalletViewModel.kt | 5 +- 11 files changed, 61 insertions(+), 103 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt index a9785d623..48fcd3816 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt @@ -42,7 +42,7 @@ class RecoveryMnemonicViewModel @Inject constructor( isLoading = false, ) } - toaster.error(R.string.security__mnemonic_load_error) + toaster.error(context.getString(R.string.security__mnemonic_load_error)) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index 98061ef81..d229d64d3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -73,7 +73,10 @@ class RecoveryViewModel @Inject constructor( isExportingLogs = false, ) } - toaster.error(R.string.common__error, R.string.other__logs_export_error) + toaster.error( + context.getString(R.string.common__error), + context.getString(R.string.other__logs_export_error), + ) } ) } @@ -94,7 +97,10 @@ class RecoveryViewModel @Inject constructor( }.onFailure { fallbackError -> Logger.error("Failed to open support links", fallbackError, context = TAG) viewModelScope.launch { - toaster.error(R.string.common__error, R.string.settings__support__link_error) + toaster.error( + context.getString(R.string.common__error), + context.getString(R.string.settings__support__link_error), + ) } } } @@ -113,7 +119,10 @@ class RecoveryViewModel @Inject constructor( walletRepo.wipeWallet().onFailure { error -> toaster.error(error) }.onSuccess { - toaster.success(R.string.security__wiped_title, R.string.security__wiped_message) + toaster.success( + context.getString(R.string.security__wiped_title), + context.getString(R.string.security__wiped_message), + ) } } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index 71134034c..ed1fcea4e 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -73,7 +73,10 @@ class ExternalNodeViewModel @Inject constructor( _uiState.update { it.copy(peer = peer) } setEffect(SideEffect.ConnectionSuccess) } else { - toaster.error(R.string.lightning__error_add_title, R.string.lightning__error_add) + toaster.error( + context.getString(R.string.lightning__error_add_title), + context.getString(R.string.lightning__error_add), + ) } } } @@ -85,7 +88,7 @@ class ExternalNodeViewModel @Inject constructor( if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } } else { - toaster.error(R.string.lightning__error_add_uri) + toaster.error(context.getString(R.string.lightning__error_add_uri)) } } } @@ -129,7 +132,10 @@ class ExternalNodeViewModel @Inject constructor( suspend fun validateCustomFeeRate(): Boolean { val feeRate = _uiState.value.customFeeRate ?: 0u if (feeRate == 0u) { - toaster.info(R.string.wallet__min_possible_fee_rate, R.string.wallet__min_possible_fee_rate_msg) + toaster.info( + context.getString(R.string.wallet__min_possible_fee_rate), + context.getString(R.string.wallet__min_possible_fee_rate_msg), + ) return false } return true diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index 19b5e6576..f60d580fb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -69,8 +69,8 @@ class LnurlChannelViewModel @Inject constructor( lightningRepo.requestLnurlChannel(callback = params.callback, k1 = params.k1, nodeId = nodeId) .onSuccess { toaster.success( - R.string.other__lnurl_channel_success_title, - R.string.other__lnurl_channel_success_msg_no_peer, + context.getString(R.string.other__lnurl_channel_success_title), + context.getString(R.string.other__lnurl_channel_success_msg_no_peer), ) _uiState.update { it.copy(isConnected = true) } }.onFailure { error -> diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index 8e12bda98..417a8eef9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -105,12 +105,18 @@ class SendFeeViewModel @Inject constructor( // TODO update to use minimum instead of slow when using mempool api val minSatsPerVByte = sendUiState.feeRates?.slow ?: 1u if (satsPerVByte < minSatsPerVByte) { - toaster.info(R.string.wallet__min_possible_fee_rate, R.string.wallet__min_possible_fee_rate_msg) + toaster.info( + context.getString(R.string.wallet__min_possible_fee_rate), + context.getString(R.string.wallet__min_possible_fee_rate_msg), + ) return false } if (satsPerVByte > maxSatsPerVByte) { - toaster.info(R.string.wallet__max_possible_fee_rate, R.string.wallet__max_possible_fee_rate_msg) + toaster.info( + context.getString(R.string.wallet__max_possible_fee_rate), + context.getString(R.string.wallet__max_possible_fee_rate_msg), + ) return false } diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index 385d8dd30..8bb0947f3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -86,7 +86,10 @@ class BackupNavSheetViewModel @Inject constructor( } }.onFailure { Logger.error("Error loading mnemonic", it, context = TAG) - toaster.warning(R.string.security__mnemonic_error, R.string.security__mnemonic_error_description) + toaster.warning( + context.getString(R.string.security__mnemonic_error), + context.getString(R.string.security__mnemonic_error_description), + ) } } @@ -153,7 +156,10 @@ class BackupNavSheetViewModel @Inject constructor( fun onMnemonicCopied() { viewModelScope.launch { - toaster.success(R.string.common__copied, R.string.security__mnemonic_copied) + toaster.success( + context.getString(R.string.common__copied), + context.getString(R.string.security__mnemonic_copied), + ) } } } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index a1324dd09..0126fbf6b 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -340,8 +340,8 @@ class LightningConnectionsViewModel @Inject constructor( .onSuccess { uri -> onReady(uri) } .onFailure { toaster.warning( - R.string.lightning__error_logs, - R.string.lightning__error_logs_description, + context.getString(R.string.lightning__error_logs), + context.getString(R.string.lightning__error_logs_description), ) } } @@ -454,8 +454,8 @@ class LightningConnectionsViewModel @Inject constructor( walletRepo.syncNodeAndWallet() toaster.success( - R.string.lightning__close_success_title, - R.string.lightning__close_success_msg, + context.getString(R.string.lightning__close_success_title), + context.getString(R.string.lightning__close_success_msg), ) _closeConnectionUiState.update { @@ -469,8 +469,8 @@ class LightningConnectionsViewModel @Inject constructor( Logger.error("Failed to close channel", e = error, context = TAG) toaster.warning( - R.string.lightning__close_error, - R.string.lightning__close_error_msg, + context.getString(R.string.lightning__close_error), + context.getString(R.string.lightning__close_error_msg), ) _closeConnectionUiState.update { it.copy(isLoading = false) } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 16fe33773..62db3dce7 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -1,23 +1,16 @@ package to.bitkit.ui.shared.toast -import android.content.Context -import androidx.annotation.StringRes -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow -import to.bitkit.R import to.bitkit.models.Toast import to.bitkit.models.ToastType import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration -@Suppress("TooManyFunctions") @Singleton -class Toaster @Inject constructor( - @ApplicationContext private val context: Context, -) { +class Toaster @Inject constructor() { private val _events = MutableSharedFlow(extraBufferCapacity = 1) val events: SharedFlow = _events.asSharedFlow() @@ -42,104 +35,39 @@ class Toaster @Inject constructor( ) } - // region Success suspend fun success( title: String, body: String? = null, testTag: String? = null, ) = emit(ToastType.SUCCESS, title, body, testTag = testTag) - suspend fun success( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, - testTag: String? = null, - ) = emit( - type = ToastType.SUCCESS, - title = context.getString(titleRes), - body = bodyRes?.let { context.getString(it) }, - testTag = testTag, - ) - // endregion - - // region Info suspend fun info( title: String, body: String? = null, testTag: String? = null, ) = emit(ToastType.INFO, title, body, testTag = testTag) - suspend fun info( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, - testTag: String? = null, - ) = emit( - type = ToastType.INFO, - title = context.getString(titleRes), - body = bodyRes?.let { context.getString(it) }, - testTag = testTag, - ) - // endregion - - // region Lightning suspend fun lightning( title: String, body: String? = null, testTag: String? = null, ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) - suspend fun lightning( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, - testTag: String? = null, - ) = emit( - type = ToastType.LIGHTNING, - title = context.getString(titleRes), - body = bodyRes?.let { context.getString(it) }, - testTag = testTag, - ) - // endregion - - // region Warning suspend fun warning( title: String, body: String? = null, testTag: String? = null, ) = emit(ToastType.WARNING, title, body, testTag = testTag) - suspend fun warning( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, - testTag: String? = null, - ) = emit( - type = ToastType.WARNING, - title = context.getString(titleRes), - body = bodyRes?.let { context.getString(it) }, - testTag = testTag, - ) - // endregion - - // region Error suspend fun error( title: String, body: String? = null, testTag: String? = null, ) = emit(ToastType.ERROR, title, body, testTag = testTag) - suspend fun error( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, - testTag: String? = null, - ) = emit( - type = ToastType.ERROR, - title = context.getString(titleRes), - body = bodyRes?.let { context.getString(it) }, - testTag = testTag, - ) - suspend fun error(throwable: Throwable) = emit( type = ToastType.ERROR, - title = context.getString(R.string.common__error), - body = throwable.message ?: context.getString(R.string.common__error_body), + title = "Error", + body = throwable.message ?: "An unknown error occurred", ) - // endregion } diff --git a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt index 11dae996b..c5e0a4ad2 100644 --- a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt @@ -100,8 +100,8 @@ class DevSettingsViewModel @Inject constructor( .onSuccess { uri -> onReady(uri) } .onFailure { toaster.warning( - R.string.lightning__error_logs, - R.string.lightning__error_logs_description, + context.getString(R.string.lightning__error_logs), + context.getString(R.string.lightning__error_logs_description), ) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 215821433..02af58076 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -459,8 +459,8 @@ class TransferViewModel @Inject constructor( channelsToClose = emptyList() Logger.error("Cannot force close channels with trusted peer", context = TAG) toaster.error( - R.string.lightning__force_failed_title, - R.string.lightning__force_failed_msg + context.getString(R.string.lightning__force_failed_title), + context.getString(R.string.lightning__force_failed_msg), ) return@runCatching } @@ -489,15 +489,15 @@ class TransferViewModel @Inject constructor( } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) toaster.error( - R.string.lightning__force_failed_title, - R.string.lightning__force_failed_msg + context.getString(R.string.lightning__force_failed_title), + context.getString(R.string.lightning__force_failed_msg), ) } }.onFailure { Logger.error("Force close failed", e = it, context = TAG) toaster.error( - R.string.lightning__force_failed_title, - R.string.lightning__force_failed_msg + context.getString(R.string.lightning__force_failed_title), + context.getString(R.string.lightning__force_failed_msg), ) } _isForceTransferLoading.value = false diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index ffa54790d..9fc2a6d38 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -300,7 +300,10 @@ class WalletViewModel @Inject constructor( viewModelScope.launch { lightningRepo.disconnectPeer(peer) .onSuccess { - toaster.info(R.string.common__success, R.string.wallet__peer_disconnected) + toaster.info( + context.getString(R.string.common__success), + context.getString(R.string.wallet__peer_disconnected), + ) } .onFailure { toaster.error( From 32ad8c590e51d31cbbd27ca143ddd88712559435 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:43:59 +0100 Subject: [PATCH 05/35] refactor: rename ToastQueueManager to ToastQueue Co-Authored-By: Claude Opus 4.5 --- .../main/java/to/bitkit/di/ViewModelModule.kt | 6 +++--- .../{ToastQueueManager.kt => ToastQueue.kt} | 8 ++++---- .../java/to/bitkit/viewmodels/AppViewModel.kt | 16 ++++++++-------- 3 files changed, 15 insertions(+), 15 deletions(-) rename app/src/main/java/to/bitkit/ui/shared/toast/{ToastQueueManager.kt => ToastQueue.kt} (92%) diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt index d0f531515..f4517100d 100644 --- a/app/src/main/java/to/bitkit/di/ViewModelModule.kt +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -6,7 +6,7 @@ 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 @@ -19,7 +19,7 @@ object ViewModelModule { } @Provides - fun provideToastManagerProvider(): (CoroutineScope) -> ToastQueueManager { - return ::ToastQueueManager + fun provideToastQueueProvider(): (CoroutineScope) -> ToastQueue { + return ::ToastQueue } } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt similarity index 92% rename from app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt rename to app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt index f8beb6b29..673a413ed 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueueManager.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt @@ -14,10 +14,10 @@ import kotlin.time.Duration private const val MAX_QUEUE_SIZE = 5 /** - * Manages a queue of toasts to display sequentially. + * A queue for displaying toasts sequentially. * - * This ensures that toasts are shown one at a time without premature cancellation. - * When a toast is displayed, it waits for its full visibility duration before + * Ensures toasts are shown one at a time without premature cancellation. + * When a toast is displayed, it waits for its full duration before * showing the next toast in the queue. * * Features: @@ -27,7 +27,7 @@ private const val MAX_QUEUE_SIZE = 5 * - Auto-advance to next toast on completion * - Max queue size with FIFO overflow handling */ -class ToastQueueManager(private val scope: CoroutineScope) { +class ToastQueue(private val scope: CoroutineScope) { // Public state exposed to UI private val _currentToast = MutableStateFlow(null) val currentToast: StateFlow = _currentToast.asStateFlow() diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index b5acc7735..e50ac8d26 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -104,7 +104,7 @@ import to.bitkit.services.AppUpdaterService import to.bitkit.services.MigrationService import to.bitkit.ui.Routes import to.bitkit.ui.components.Sheet -import to.bitkit.ui.shared.toast.ToastQueueManager +import to.bitkit.ui.shared.toast.ToastQueue import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.theme.TRANSITION_SCREEN_MS @@ -127,7 +127,7 @@ import kotlin.time.ExperimentalTime class AppViewModel @Inject constructor( connectivityRepo: ConnectivityRepo, healthRepo: HealthRepo, - toastManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueueManager, + toastQueueProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueue, timedSheetManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> TimedSheetManager, toaster: Toaster, @ApplicationContext private val context: Context, @@ -1792,8 +1792,8 @@ class AppViewModel @Inject constructor( // endregion // region Toasts - private val toastManager = toastManagerProvider(viewModelScope) - val currentToast: StateFlow = toastManager.currentToast + private val toastQueue = toastQueueProvider(viewModelScope) + val currentToast: StateFlow = toastQueue.currentToast fun toast( type: ToastType, @@ -1803,7 +1803,7 @@ class AppViewModel @Inject constructor( duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, ) { - toastManager.enqueue( + toastQueue.enqueue( Toast( type = type, title = title, @@ -1833,11 +1833,11 @@ class AppViewModel @Inject constructor( ) } - fun hideToast() = toastManager.dismissCurrentToast() + fun hideToast() = toastQueue.dismissCurrentToast() - fun pauseToast() = toastManager.pauseCurrentToast() + fun pauseToast() = toastQueue.pauseCurrentToast() - fun resumeToast() = toastManager.resumeCurrentToast() + fun resumeToast() = toastQueue.resumeCurrentToast() // endregion // region security From 301e073215667525c8176019474cf5eb6e72bcbe Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:45:37 +0100 Subject: [PATCH 06/35] refactor: make ToastQueue inherit BaseCoroutineScope Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/di/ViewModelModule.kt | 4 ++-- app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt | 7 ++++--- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 5 +++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt index f4517100d..a7d6da319 100644 --- a/app/src/main/java/to/bitkit/di/ViewModelModule.kt +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -5,7 +5,7 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineDispatcher import to.bitkit.ui.shared.toast.ToastQueue import javax.inject.Singleton @@ -19,7 +19,7 @@ object ViewModelModule { } @Provides - fun provideToastQueueProvider(): (CoroutineScope) -> ToastQueue { + fun provideToastQueueProvider(): (CoroutineDispatcher) -> ToastQueue { return ::ToastQueue } } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt index 673a413ed..499721a52 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt @@ -1,6 +1,6 @@ package to.bitkit.ui.shared.toast -import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import to.bitkit.async.BaseCoroutineScope import to.bitkit.models.Toast import kotlin.time.Duration @@ -27,7 +28,7 @@ private const val MAX_QUEUE_SIZE = 5 * - Auto-advance to next toast on completion * - Max queue size with FIFO overflow handling */ -class ToastQueue(private val scope: CoroutineScope) { +class ToastQueue(dispatcher: CoroutineDispatcher) : BaseCoroutineScope(dispatcher) { // Public state exposed to UI private val _currentToast = MutableStateFlow(null) val currentToast: StateFlow = _currentToast.asStateFlow() @@ -115,7 +116,7 @@ class ToastQueue(private val scope: CoroutineScope) { private fun startTimer(duration: Duration) { cancelTimer() - timerJob = scope.launch { + timerJob = launch { delay(duration) if (!isPaused) { _currentToast.value = null diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index e50ac8d26..4d672f47c 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -28,6 +28,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -127,7 +128,7 @@ import kotlin.time.ExperimentalTime class AppViewModel @Inject constructor( connectivityRepo: ConnectivityRepo, healthRepo: HealthRepo, - toastQueueProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueue, + toastQueueProvider: @JvmSuppressWildcards (CoroutineDispatcher) -> ToastQueue, timedSheetManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> TimedSheetManager, toaster: Toaster, @ApplicationContext private val context: Context, @@ -1792,7 +1793,7 @@ class AppViewModel @Inject constructor( // endregion // region Toasts - private val toastQueue = toastQueueProvider(viewModelScope) + private val toastQueue = toastQueueProvider(Dispatchers.Main.immediate) val currentToast: StateFlow = toastQueue.currentToast fun toast( From 0e3e7f5cc6e9d7223e8f08ea72da01c10a5af866 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:46:01 +0100 Subject: [PATCH 07/35] docs: add *Manager anti-pattern rule Co-Authored-By: Claude Opus 4.5 --- AGENTS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/AGENTS.md b/AGENTS.md index e7a0b0da0..20c73500c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -210,6 +210,7 @@ suspend fun getData(): Result = withContext(Dispatchers.IO) { - 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 From 96ec53c91000aadf21d2ef3c1a5021b4df69ea0b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:48:46 +0100 Subject: [PATCH 08/35] refactor: expose Toaster via CompositionLocal Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/ui/ContentView.kt | 1 + app/src/main/java/to/bitkit/ui/Locals.kt | 5 +++++ app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index c65396113..9d9b77290 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -360,6 +360,7 @@ fun ContentView( LocalTransferViewModel provides transferViewModel, LocalSettingsViewModel provides settingsViewModel, LocalBackupsViewModel provides backupsViewModel, + LocalToaster provides appViewModel.toaster, LocalDrawerState provides drawerState, LocalBalances provides balance, LocalCurrencies provides currencies, diff --git a/app/src/main/java/to/bitkit/ui/Locals.kt b/app/src/main/java/to/bitkit/ui/Locals.kt index 2843e38c4..dd1f02f7d 100644 --- a/app/src/main/java/to/bitkit/ui/Locals.kt +++ b/app/src/main/java/to/bitkit/ui/Locals.kt @@ -14,6 +14,7 @@ import to.bitkit.viewmodels.CurrencyViewModel import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel +import to.bitkit.ui.shared.toast.Toaster // Locals val LocalBalances = compositionLocalOf { BalanceState() } @@ -29,6 +30,7 @@ val LocalActivityListViewModel = staticCompositionLocalOf { null } val LocalSettingsViewModel = staticCompositionLocalOf { null } val LocalBackupsViewModel = staticCompositionLocalOf { null } +val LocalToaster = staticCompositionLocalOf { null } val appViewModel: AppViewModel? @Composable get() = LocalAppViewModel.current @@ -56,3 +58,6 @@ val backupsViewModel: BackupsViewModel? val drawerState: DrawerState? @Composable get() = LocalDrawerState.current + +val toaster: Toaster? + @Composable get() = LocalToaster.current diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 4d672f47c..001563bc3 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -130,7 +130,7 @@ class AppViewModel @Inject constructor( healthRepo: HealthRepo, toastQueueProvider: @JvmSuppressWildcards (CoroutineDispatcher) -> ToastQueue, timedSheetManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> TimedSheetManager, - toaster: Toaster, + val toaster: Toaster, @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val keychain: Keychain, From f644b0a54cc0b5b98f5c68f4526d6b06db613a42 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:50:57 +0100 Subject: [PATCH 09/35] refactor: use LocalToaster in composables Co-Authored-By: Claude Opus 4.5 --- .../main/java/to/bitkit/ui/components/IsOnlineTracker.kt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index 83264d790..9e208724c 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -8,8 +8,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R -import to.bitkit.models.ToastType import to.bitkit.repositories.ConnectivityState +import to.bitkit.ui.toaster import to.bitkit.viewmodels.AppViewModel @Composable @@ -17,6 +17,7 @@ fun IsOnlineTracker( app: AppViewModel, ) { val context = LocalContext.current + val toaster = toaster ?: return val connectivityState by app.isOnline.collectAsStateWithLifecycle(initialValue = ConnectivityState.CONNECTED) val (isFirstEmission, setIsFirstEmission) = remember { mutableStateOf(true) } @@ -30,16 +31,14 @@ fun IsOnlineTracker( when (connectivityState) { ConnectivityState.CONNECTED -> { - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = context.getString(R.string.other__connection_back_title), body = context.getString(R.string.other__connection_back_msg), ) } ConnectivityState.DISCONNECTED -> { - app.toast( - type = ToastType.WARNING, + toaster.warning( title = context.getString(R.string.other__connection_issue), body = context.getString(R.string.other__connection_issue_explain), ) From 20be904ab23b23cf1205ed839b62f115f918bb0b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:52:27 +0100 Subject: [PATCH 10/35] refactor: rename ToastView to ToastContent Co-Authored-By: Claude Opus 4.5 --- .../main/java/to/bitkit/ui/MainActivity.kt | 4 +- .../java/to/bitkit/ui/components/ToastView.kt | 103 ++++++++---------- 2 files changed, 45 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index db4b23dee..52bd312a1 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -36,7 +36,7 @@ import to.bitkit.androidServices.LightningNodeService.Companion.CHANNEL_ID_NODE import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.ui.components.AuthCheckView import to.bitkit.ui.components.IsOnlineTracker -import to.bitkit.ui.components.ToastOverlay +import to.bitkit.ui.components.ToastHost import to.bitkit.ui.onboarding.CreateWalletWithPassphraseScreen import to.bitkit.ui.onboarding.IntroScreen import to.bitkit.ui.onboarding.OnboardingSlidesScreen @@ -173,7 +173,7 @@ class MainActivity : FragmentActivity() { } val currentToast by appViewModel.currentToast.collectAsStateWithLifecycle() - ToastOverlay( + ToastHost( toast = currentToast, hazeState = hazeState, onDismiss = { appViewModel.hideToast() }, diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index bdf2cf38c..c08b837d1 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -67,9 +67,45 @@ private const val TINT_ALPHA = 0.32f private const val SHADOW_ALPHA = 0.4f private const val ELEVATION_DP = 10 +@Composable +fun ToastHost( + toast: Toast?, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + hazeState: HazeState = rememberHazeState(blurEnabled = true), + onDragStart: () -> Unit = {}, + onDragEnd: () -> Unit = {}, +) { + Box( + contentAlignment = Alignment.TopCenter, + modifier = modifier.fillMaxSize(), + ) { + AnimatedContent( + targetState = toast, + transitionSpec = { + (fadeIn() + slideInVertically { -it }) + .togetherWith(fadeOut() + slideOutVertically { -it }) + .using(SizeTransform(clip = false)) + }, + contentAlignment = Alignment.TopCenter, + label = "toastAnimation", + ) { + if (it != null) { + ToastContent( + toast = it, + onDismiss = onDismiss, + hazeState = hazeState, + onDragStart = onDragStart, + onDragEnd = onDragEnd + ) + } + } + } +} + @OptIn(ExperimentalHazeMaterialsApi::class) @Composable -fun ToastView( +private fun ToastContent( toast: Toast, onDismiss: () -> Unit, modifier: Modifier = Modifier, @@ -261,67 +297,14 @@ fun ToastView( } } -@Composable -private fun ToastHost( - toast: Toast?, - hazeState: HazeState, - onDismiss: () -> Unit, - onDragStart: () -> Unit = {}, - onDragEnd: () -> Unit = {}, -) { - AnimatedContent( - targetState = toast, - transitionSpec = { - (fadeIn() + slideInVertically { -it }) - .togetherWith(fadeOut() + slideOutVertically { -it }) - .using(SizeTransform(clip = false)) - }, - contentAlignment = Alignment.TopCenter, - label = "toastAnimation", - ) { - if (it != null) { - ToastView( - toast = it, - onDismiss = onDismiss, - hazeState = hazeState, - onDragStart = onDragStart, - onDragEnd = onDragEnd - ) - } - } -} - -@Composable -fun ToastOverlay( - toast: Toast?, - onDismiss: () -> Unit, - modifier: Modifier = Modifier, - hazeState: HazeState = rememberHazeState(blurEnabled = true), - onDragStart: () -> Unit = {}, - onDragEnd: () -> Unit = {}, -) { - Box( - contentAlignment = Alignment.TopCenter, - modifier = modifier.fillMaxSize(), - ) { - ToastHost( - toast = toast, - hazeState = hazeState, - onDismiss = onDismiss, - onDragStart = onDragStart, - onDragEnd = onDragEnd - ) - } -} - @Preview(showSystemUi = true) @Composable -private fun ToastViewPreview() { +private fun ToastContentPreview() { AppThemeSurface { ScreenColumn( verticalArrangement = Arrangement.spacedBy(16.dp), ) { - ToastView( + ToastContent( toast = Toast( type = ToastType.WARNING, title = "You're still offline", @@ -330,7 +313,7 @@ private fun ToastViewPreview() { ), onDismiss = {}, ) - ToastView( + ToastContent( toast = Toast( type = ToastType.LIGHTNING, title = "Instant Payments Ready", @@ -339,7 +322,7 @@ private fun ToastViewPreview() { ), onDismiss = {}, ) - ToastView( + ToastContent( toast = Toast( type = ToastType.SUCCESS, title = "You're Back Online!", @@ -348,7 +331,7 @@ private fun ToastViewPreview() { ), onDismiss = {}, ) - ToastView( + ToastContent( toast = Toast( type = ToastType.INFO, title = "General Message", @@ -357,7 +340,7 @@ private fun ToastViewPreview() { ), onDismiss = {}, ) - ToastView( + ToastContent( toast = Toast( type = ToastType.ERROR, title = "Error Toast", From 3a3b96df3f1ab1bfc795f571280632479bf81fd1 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 20:55:13 +0100 Subject: [PATCH 11/35] test: add ToastQueue behavior tests Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/ui/Locals.kt | 2 +- .../java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- .../bitkit/ui/shared/toast/ToastQueueTest.kt | 149 ++++++++++++++++++ 3 files changed, 151 insertions(+), 2 deletions(-) create mode 100644 app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt diff --git a/app/src/main/java/to/bitkit/ui/Locals.kt b/app/src/main/java/to/bitkit/ui/Locals.kt index dd1f02f7d..89688e9eb 100644 --- a/app/src/main/java/to/bitkit/ui/Locals.kt +++ b/app/src/main/java/to/bitkit/ui/Locals.kt @@ -6,6 +6,7 @@ import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf import to.bitkit.models.BalanceState import to.bitkit.repositories.CurrencyState +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.viewmodels.ActivityListViewModel import to.bitkit.viewmodels.AppViewModel import to.bitkit.viewmodels.BackupsViewModel @@ -14,7 +15,6 @@ import to.bitkit.viewmodels.CurrencyViewModel import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel -import to.bitkit.ui.shared.toast.Toaster // Locals val LocalBalances = compositionLocalOf { BalanceState() } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 001563bc3..1e24dce3f 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -47,7 +47,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeout -import kotlin.time.Duration import org.lightningdevkit.ldknode.ChannelDataMigration import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.PaymentId @@ -120,6 +119,7 @@ import to.bitkit.utils.timedsheets.sheets.QuickPayTimedSheet import java.math.BigDecimal import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException +import kotlin.time.Duration import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) diff --git a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt new file mode 100644 index 000000000..bdf646c76 --- /dev/null +++ b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt @@ -0,0 +1,149 @@ +package to.bitkit.ui.shared.toast + +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import to.bitkit.models.Toast +import to.bitkit.models.ToastType +import to.bitkit.test.BaseUnitTest +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalCoroutinesApi::class) +class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { + private lateinit var sut: ToastQueue + + @Before + fun setUp() { + sut = ToastQueue(testDispatcher) + } + + @Test + fun `enqueue shows toast immediately when queue empty`() = test { + val toast = createToast() + + sut.enqueue(toast) + + assertEquals(toast, sut.currentToast.value) + } + + @Test + fun `enqueue queues toast when another is displayed`() = test { + val toast1 = createToast(title = "First") + val toast2 = createToast(title = "Second") + + sut.enqueue(toast1) + sut.enqueue(toast2) + + assertEquals("Second", sut.currentToast.value?.title) + } + + @Test + fun `dismiss advances to next toast in queue`() = test { + val toast1 = createToast(title = "First", autoHide = false) + val toast2 = createToast(title = "Second", autoHide = false) + + sut.enqueue(toast1) + sut.enqueue(toast2) + + assertEquals("Second", sut.currentToast.value?.title) + + sut.dismissCurrentToast() + + assertNull(sut.currentToast.value) + } + + @Test + fun `auto-hide timer dismisses toast after duration`() = test { + val toast = createToast(autoHide = true) + + sut.enqueue(toast) + + assertEquals(toast, sut.currentToast.value) + + advanceTimeBy(3001) + + assertNull(sut.currentToast.value) + } + + @Test + fun `pause stops auto-hide timer`() = test { + val toast = createToast(autoHide = true) + + sut.enqueue(toast) + advanceTimeBy(1000) + sut.pauseCurrentToast() + advanceTimeBy(5000) + + assertEquals(toast, sut.currentToast.value) + } + + @Test + fun `resume restarts auto-hide timer`() = test { + val toast = createToast(autoHide = true) + + sut.enqueue(toast) + advanceTimeBy(1000) + sut.pauseCurrentToast() + advanceTimeBy(5000) + sut.resumeCurrentToast() + advanceTimeBy(2000) + + assertEquals(toast, sut.currentToast.value) + + advanceTimeBy(1001) + + assertNull(sut.currentToast.value) + } + + @Test + fun `max queue size drops oldest when exceeded`() = test { + val toasts = (1..6).map { createToast(title = "Toast $it") } + + toasts.forEach { sut.enqueue(it) } + + assertEquals("Toast 6", sut.currentToast.value?.title) + } + + @Test + fun `clear removes all toasts and hides current`() = test { + val toast1 = createToast(title = "First", autoHide = false) + val toast2 = createToast(title = "Second", autoHide = false) + + sut.enqueue(toast1) + sut.enqueue(toast2) + sut.clear() + + assertNull(sut.currentToast.value) + } + + @Test + fun `non-auto-hide toast stays until dismissed`() = test { + val toast = createToast(autoHide = false) + + sut.enqueue(toast) + advanceTimeBy(10_000) + + assertEquals(toast, sut.currentToast.value) + + sut.dismissCurrentToast() + + assertNull(sut.currentToast.value) + } + + private fun createToast( + title: String = "Test Toast", + body: String? = null, + type: ToastType = ToastType.INFO, + autoHide: Boolean = true, + ) = Toast( + type = type, + title = title, + body = body, + autoHide = autoHide, + duration = 3.seconds, + ) +} From 6feaede3ee6880c548f1fa8eb5772b79821db7cf Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 22:58:26 +0100 Subject: [PATCH 12/35] refactor: add @StringRes overloads to Toaster Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/Toast.kt | 4 +- .../main/java/to/bitkit/models/ToastText.kt | 8 ++ .../java/to/bitkit/ui/components/ToastView.kt | 28 ++--- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 105 ++++++++++++++++-- .../java/to/bitkit/viewmodels/AppViewModel.kt | 56 +++++++--- .../bitkit/ui/shared/toast/ToastQueueTest.kt | 11 +- 6 files changed, 167 insertions(+), 45 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index ca9c7eb1e..8dbc764ab 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -7,8 +7,8 @@ import kotlin.time.Duration.Companion.seconds @Stable data class Toast( val type: ToastType, - val title: String, - val body: String? = null, + val title: ToastText, + val body: ToastText? = null, val autoHide: Boolean, val duration: Duration = DURATION_DEFAULT, val testTag: String? = null, diff --git a/app/src/main/java/to/bitkit/models/ToastText.kt b/app/src/main/java/to/bitkit/models/ToastText.kt index bf73e651e..bb9335387 100644 --- a/app/src/main/java/to/bitkit/models/ToastText.kt +++ b/app/src/main/java/to/bitkit/models/ToastText.kt @@ -2,7 +2,9 @@ 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 { @@ -15,6 +17,12 @@ sealed interface ToastText { value class Literal(val value: String) : ToastText } +@Composable +fun ToastText.asString(): String = when (this) { + is ToastText.Resource -> stringResource(resId) + is ToastText.Literal -> value +} + fun ToastText.asString(context: Context): String = when (this) { is ToastText.Resource -> context.getString(resId) is ToastText.Literal -> value diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index c08b837d1..98f89fc0d 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -53,7 +53,9 @@ import dev.chrisbanes.haze.rememberHazeState import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.Toast +import to.bitkit.models.ToastText import to.bitkit.models.ToastType +import to.bitkit.models.asString import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors @@ -259,12 +261,12 @@ private fun ToastContent( .padding(16.dp) ) { BodyMSB( - text = toast.title, + text = toast.title.asString(), color = tintColor, ) toast.body?.let { body -> Caption( - text = body, + text = body.asString(), color = Colors.White ) } @@ -299,7 +301,7 @@ private fun ToastContent( @Preview(showSystemUi = true) @Composable -private fun ToastContentPreview() { +private fun Preview() { AppThemeSurface { ScreenColumn( verticalArrangement = Arrangement.spacedBy(16.dp), @@ -307,8 +309,8 @@ private fun ToastContentPreview() { ToastContent( toast = Toast( type = ToastType.WARNING, - title = "You're still offline", - body = "Check your connection to keep using Bitkit.", + title = ToastText.Literal("You're still offline"), + body = ToastText.Literal("Check your connection to keep using Bitkit."), autoHide = true, ), onDismiss = {}, @@ -316,8 +318,8 @@ private fun ToastContentPreview() { ToastContent( toast = Toast( type = ToastType.LIGHTNING, - title = "Instant Payments Ready", - body = "You can now pay anyone, anywhere, instantly.", + title = ToastText.Literal("Instant Payments Ready"), + body = ToastText.Literal("You can now pay anyone, anywhere, instantly."), autoHide = true, ), onDismiss = {}, @@ -325,8 +327,8 @@ private fun ToastContentPreview() { ToastContent( toast = Toast( type = ToastType.SUCCESS, - title = "You're Back Online!", - body = "Successfully reconnected to the Internet.", + title = ToastText.Literal("You're Back Online!"), + body = ToastText.Literal("Successfully reconnected to the Internet."), autoHide = true, ), onDismiss = {}, @@ -334,8 +336,8 @@ private fun ToastContentPreview() { ToastContent( toast = Toast( type = ToastType.INFO, - title = "General Message", - body = "Used for neutral content to inform the user.", + title = ToastText.Literal("General Message"), + body = ToastText.Literal("Used for neutral content to inform the user."), autoHide = false, ), onDismiss = {}, @@ -343,8 +345,8 @@ private fun ToastContentPreview() { ToastContent( toast = Toast( type = ToastType.ERROR, - title = "Error Toast", - body = "This is a toast message.", + title = ToastText.Literal("Error Toast"), + body = ToastText.Literal("This is a toast message."), autoHide = true, ), onDismiss = {}, diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 62db3dce7..cae6b7b37 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -1,14 +1,17 @@ package to.bitkit.ui.shared.toast +import androidx.annotation.StringRes import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow import to.bitkit.models.Toast +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import javax.inject.Inject import javax.inject.Singleton import kotlin.time.Duration +@Suppress("TooManyFunctions") @Singleton class Toaster @Inject constructor() { private val _events = MutableSharedFlow(extraBufferCapacity = 1) @@ -17,8 +20,8 @@ class Toaster @Inject constructor() { @Suppress("LongParameterList") private suspend fun emit( type: ToastType, - title: String, - body: String? = null, + title: ToastText, + body: ToastText? = null, autoHide: Boolean = true, duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, @@ -35,39 +38,123 @@ class Toaster @Inject constructor() { ) } + // region @StringRes overloads + suspend fun success( + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + testTag: String? = null, + ) = emit( + ToastType.SUCCESS, + ToastText.Resource(titleRes), + bodyRes?.let { ToastText.Resource(it) }, + testTag = testTag + ) + + suspend fun info( + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + testTag: String? = null, + ) = emit( + ToastType.INFO, + ToastText.Resource(titleRes), + bodyRes?.let { ToastText.Resource(it) }, + testTag = testTag, + ) + + suspend fun lightning( + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + testTag: String? = null, + ) = emit( + ToastType.LIGHTNING, + ToastText.Resource(titleRes), + bodyRes?.let { ToastText.Resource(it) }, + testTag = testTag + ) + + suspend fun warning( + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + testTag: String? = null, + ) = emit( + ToastType.WARNING, + ToastText.Resource(titleRes), + bodyRes?.let { ToastText.Resource(it) }, + testTag = testTag + ) + + suspend fun error( + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + testTag: String? = null, + ) = emit( + ToastType.ERROR, + ToastText.Resource(titleRes), + bodyRes?.let { ToastText.Resource(it) }, + testTag = testTag, + ) + // endregion + + // region String literal overloads suspend fun success( title: String, body: String? = null, testTag: String? = null, - ) = emit(ToastType.SUCCESS, title, body, testTag = testTag) + ) = emit( + ToastType.SUCCESS, + ToastText.Literal(title), + body?.let { ToastText.Literal(it) }, + testTag = testTag, + ) suspend fun info( title: String, body: String? = null, testTag: String? = null, - ) = emit(ToastType.INFO, title, body, testTag = testTag) + ) = emit( + ToastType.INFO, + ToastText.Literal(title), + body?.let { ToastText.Literal(it) }, + testTag = testTag, + ) suspend fun lightning( title: String, body: String? = null, testTag: String? = null, - ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) + ) = emit( + ToastType.LIGHTNING, + ToastText.Literal(title), + body?.let { ToastText.Literal(it) }, + testTag = testTag, + ) suspend fun warning( title: String, body: String? = null, testTag: String? = null, - ) = emit(ToastType.WARNING, title, body, testTag = testTag) + ) = emit( + ToastType.WARNING, + ToastText.Literal(title), + body?.let { ToastText.Literal(it) }, + testTag = testTag, + ) suspend fun error( title: String, body: String? = null, testTag: String? = null, - ) = emit(ToastType.ERROR, title, body, testTag = testTag) + ) = emit( + ToastType.ERROR, + ToastText.Literal(title), + body?.let { ToastText.Literal(it) }, + testTag = testTag, + ) suspend fun error(throwable: Throwable) = emit( type = ToastType.ERROR, - title = "Error", - body = throwable.message ?: "An unknown error occurred", + title = ToastText.Literal("Error"), + body = ToastText.Literal(throwable.message ?: "An unknown error occurred"), ) + // endregion } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 1e24dce3f..3e3c8d9e4 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -83,6 +83,7 @@ import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.Suggestion import to.bitkit.models.Toast +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.models.TransactionSpeed import to.bitkit.models.safe @@ -234,9 +235,7 @@ class AppViewModel @Inject constructor( init { viewModelScope.launch { - toaster.events.collect { - toast(it.type, it.title, it.body, it.autoHide, it.duration) - } + toaster.events.collect { toastQueue.enqueue(it) } } viewModelScope.launch { // Delays are required for auth check on launch functionality @@ -1798,8 +1797,8 @@ class AppViewModel @Inject constructor( fun toast( type: ToastType, - title: String, - body: String? = null, + title: ToastText, + body: ToastText? = null, autoHide: Boolean = true, duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, @@ -1816,23 +1815,48 @@ class AppViewModel @Inject constructor( ) } + fun toast( + type: ToastType, + @StringRes titleRes: Int, + @StringRes bodyRes: Int? = null, + autoHide: Boolean = true, + duration: Duration = Toast.DURATION_DEFAULT, + testTag: String? = null, + ) = toast( + type = type, + title = ToastText.Resource(titleRes), + body = bodyRes?.let { ToastText.Resource(it) }, + autoHide = autoHide, + duration = duration, + testTag = testTag, + ) + + fun toast( + type: ToastType, + title: String, + body: String? = null, + autoHide: Boolean = true, + duration: Duration = Toast.DURATION_DEFAULT, + testTag: String? = null, + ) = toast( + type = type, + title = ToastText.Literal(title), + body = body?.let { ToastText.Literal(it) }, + autoHide = autoHide, + duration = duration, + testTag = testTag, + ) + fun toast(error: Throwable) { toast( type = ToastType.ERROR, - title = context.getString(R.string.common__error), - body = error.message ?: context.getString(R.string.common__error_body) + title = ToastText.Resource(R.string.common__error), + body = error.message?.let { ToastText.Literal(it) } + ?: ToastText.Resource(R.string.common__error_body) ) } - fun toast(toast: Toast) { - toast( - type = toast.type, - title = toast.title, - body = toast.body, - autoHide = toast.autoHide, - duration = toast.duration - ) - } + fun toast(toast: Toast) = toastQueue.enqueue(toast) fun hideToast() = toastQueue.dismissCurrentToast() diff --git a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt index bdf646c76..f341814ec 100644 --- a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt +++ b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt @@ -8,6 +8,7 @@ import org.junit.Assert.assertNull import org.junit.Before import org.junit.Test import to.bitkit.models.Toast +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.test.BaseUnitTest import kotlin.time.Duration.Companion.seconds @@ -38,7 +39,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { sut.enqueue(toast1) sut.enqueue(toast2) - assertEquals("Second", sut.currentToast.value?.title) + assertEquals(ToastText.Literal("Second"), sut.currentToast.value?.title) } @Test @@ -49,7 +50,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { sut.enqueue(toast1) sut.enqueue(toast2) - assertEquals("Second", sut.currentToast.value?.title) + assertEquals(ToastText.Literal("Second"), sut.currentToast.value?.title) sut.dismissCurrentToast() @@ -105,7 +106,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { toasts.forEach { sut.enqueue(it) } - assertEquals("Toast 6", sut.currentToast.value?.title) + assertEquals(ToastText.Literal("Toast 6"), sut.currentToast.value?.title) } @Test @@ -141,8 +142,8 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { autoHide: Boolean = true, ) = Toast( type = type, - title = title, - body = body, + title = ToastText.Literal(title), + body = body?.let { ToastText.Literal(it) }, autoHide = autoHide, duration = 3.seconds, ) From 1ad36fa80546b8730bd260bfcf70623f69dfba04 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 23:06:14 +0100 Subject: [PATCH 13/35] refactor: use @StringRes in composable toasts Co-Authored-By: Claude Opus 4.5 --- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 6 ++-- .../bitkit/ui/components/IsOnlineTracker.kt | 10 +++---- .../ui/screens/scanner/QrScanningScreen.kt | 8 +++--- .../screens/transfer/SavingsProgressScreen.kt | 10 +++---- .../wallets/activity/ActivityDetailScreen.kt | 17 ++++++----- .../to/bitkit/ui/settings/SettingsScreen.kt | 28 ++++++++----------- .../settings/advanced/AddressViewerScreen.kt | 5 ++-- .../settings/advanced/ElectrumConfigScreen.kt | 15 ++++++---- .../ui/settings/advanced/RgsServerScreen.kt | 11 ++++---- .../settings/lightning/ChannelDetailScreen.kt | 5 ++-- 10 files changed, 55 insertions(+), 60 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 8c394873b..2317fe37a 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -46,6 +46,7 @@ import to.bitkit.ext.createChannelDetails import to.bitkit.ext.formatToString import to.bitkit.ext.uri import to.bitkit.models.NodeLifecycleState +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay import to.bitkit.repositories.LightningState @@ -77,7 +78,6 @@ fun NodeInfoScreen( val wallet = walletViewModel ?: return val app = appViewModel ?: return val settings = settingsViewModel ?: return - val context = LocalContext.current val isRefreshing by wallet.isRefreshing.collectAsStateWithLifecycle() val isDevModeEnabled by settings.isDevModeEnabled.collectAsStateWithLifecycle() @@ -93,8 +93,8 @@ fun NodeInfoScreen( onCopy = { text -> app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.common__copied), - body = text + title = ToastText.Resource(R.string.common__copied), + body = ToastText.Literal(text), ) }, ) diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index 9e208724c..2b0bcd07c 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -5,7 +5,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.repositories.ConnectivityState @@ -16,7 +15,6 @@ import to.bitkit.viewmodels.AppViewModel fun IsOnlineTracker( app: AppViewModel, ) { - val context = LocalContext.current val toaster = toaster ?: return val connectivityState by app.isOnline.collectAsStateWithLifecycle(initialValue = ConnectivityState.CONNECTED) @@ -32,15 +30,15 @@ fun IsOnlineTracker( when (connectivityState) { ConnectivityState.CONNECTED -> { toaster.success( - title = context.getString(R.string.other__connection_back_title), - body = context.getString(R.string.other__connection_back_msg), + titleRes = R.string.other__connection_back_title, + bodyRes = R.string.other__connection_back_msg, ) } ConnectivityState.DISCONNECTED -> { toaster.warning( - title = context.getString(R.string.other__connection_issue), - body = context.getString(R.string.other__connection_issue_explain), + titleRes = R.string.other__connection_issue, + bodyRes = R.string.other__connection_issue_explain, ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 264ee2353..0490be66d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -151,8 +151,8 @@ fun QrScanningScreen( Logger.error("Failed to scan QR code", error) app.toast( type = ToastType.ERROR, - title = context.getString(R.string.other__qr_error_header), - body = context.getString(R.string.other__qr_error_text), + titleRes = R.string.other__qr_error_header, + bodyRes = R.string.other__qr_error_text, ) } } @@ -257,8 +257,8 @@ private fun handlePaste( if (clipboard.isNullOrBlank()) { app.toast( type = ToastType.WARNING, - title = context.getString(R.string.wallet__send_clipboard_empty_title), - body = context.getString(R.string.wallet__send_clipboard_empty_text), + titleRes = R.string.wallet__send_clipboard_empty_title, + bodyRes = R.string.wallet__send_clipboard_empty_text, ) } setScanResult(clipboard) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 320066d63..089241e90 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.keepScreenOn import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -52,7 +51,6 @@ fun SavingsProgressScreen( onContinueClick: () -> Unit = {}, onTransferUnavailable: () -> Unit = {}, ) { - val context = LocalContext.current var progressState by remember { mutableStateOf(SavingsProgressState.PROGRESS) } // Effect to close channels & update UI @@ -72,8 +70,8 @@ fun SavingsProgressScreen( // All channels are trusted peers - show error and navigate back immediately app.toast( type = ToastType.ERROR, - title = context.getString(R.string.lightning__close_error), - body = context.getString(R.string.lightning__close_error_msg), + titleRes = R.string.lightning__close_error, + bodyRes = R.string.lightning__close_error_msg, ) onTransferUnavailable() } else { @@ -83,8 +81,8 @@ fun SavingsProgressScreen( onTransferUnavailable = { app.toast( type = ToastType.ERROR, - title = context.getString(R.string.lightning__close_error), - body = context.getString(R.string.lightning__close_error_msg), + titleRes = R.string.lightning__close_error, + bodyRes = R.string.lightning__close_error_msg, ) onTransferUnavailable() }, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index 9ef1f4663..e0e4abd82 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -194,7 +194,6 @@ fun ActivityDetailScreen( } } - val context = LocalContext.current val blocktankInfo by blocktankViewModel?.info?.collectAsStateWithLifecycle() ?: remember { mutableStateOf(null) } @@ -252,8 +251,8 @@ fun ActivityDetailScreen( onSuccess = { app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.wallet__boost_success_title), - body = context.getString(R.string.wallet__boost_success_msg), + titleRes = R.string.wallet__boost_success_title, + bodyRes = R.string.wallet__boost_success_msg, testTag = "BoostSuccessToast" ) listViewModel.resync() @@ -262,8 +261,8 @@ fun ActivityDetailScreen( onFailure = { app.toast( type = ToastType.ERROR, - title = context.getString(R.string.wallet__boost_error_title), - body = context.getString(R.string.wallet__boost_error_msg), + titleRes = R.string.wallet__boost_error_title, + bodyRes = R.string.wallet__boost_error_msg, testTag = "BoostFailureToast" ) detailViewModel.onDismissBoostSheet() @@ -271,15 +270,15 @@ fun ActivityDetailScreen( onMaxFee = { app.toast( type = ToastType.ERROR, - title = context.getString(R.string.wallet__send_fee_error), - body = context.getString(R.string.wallet__send_fee_error_max) + titleRes = R.string.wallet__send_fee_error, + bodyRes = R.string.wallet__send_fee_error_max, ) }, onMinFee = { app.toast( type = ToastType.ERROR, - title = context.getString(R.string.wallet__send_fee_error), - body = context.getString(R.string.wallet__send_fee_error_min) + titleRes = R.string.wallet__send_fee_error, + bodyRes = R.string.wallet__send_fee_error_min, ) } ) diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index e57e4e180..ac1c19c2d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource @@ -54,7 +53,6 @@ fun SettingsScreen( val isDevModeEnabled by settings.isDevModeEnabled.collectAsStateWithLifecycle() var enableDevModeTapCount by remember { mutableIntStateOf(0) } val haptic = LocalHapticFeedback.current - val context = LocalContext.current SettingsScreenContent( isDevModeEnabled = isDevModeEnabled, @@ -75,22 +73,20 @@ fun SettingsScreen( settings.setIsDevModeEnabled(newValue) haptic.performHapticFeedback(HapticFeedbackType.LongPress) + val titleRes = if (newValue) { + R.string.settings__dev_enabled_title + } else { + R.string.settings__dev_disabled_title + } + val bodyRes = if (newValue) { + R.string.settings__dev_enabled_message + } else { + R.string.settings__dev_disabled_message + } app.toast( type = ToastType.SUCCESS, - title = context.getString( - if (newValue) { - R.string.settings__dev_enabled_title - } else { - R.string.settings__dev_disabled_title - } - ), - body = context.getString( - if (newValue) { - R.string.settings__dev_enabled_message - } else { - R.string.settings__dev_disabled_message - } - ), + titleRes = titleRes, + bodyRes = bodyRes, ) enableDevModeTapCount = 0 } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index 6754fea54..58ff99f8c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -30,6 +30,7 @@ import androidx.navigation.NavController import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.models.AddressModel +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay import to.bitkit.ui.appViewModel @@ -79,8 +80,8 @@ fun AddressViewerScreen( context.setClipboardText(text) app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.common__copied), - body = text, + title = ToastText.Resource(R.string.common__copied), + body = ToastText.Literal(text), ) } ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 1a06e5556..57b675d30 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServerPeer +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM @@ -76,17 +77,19 @@ fun ElectrumConfigScreen( if (result.isSuccess) { app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.settings__es__server_updated_title), - body = context.getString(R.string.settings__es__server_updated_message) - .replace("{host}", uiState.host) - .replace("{port}", uiState.port), + title = ToastText.Resource(R.string.settings__es__server_updated_title), + body = ToastText.Literal( + context.getString(R.string.settings__es__server_updated_message) + .replace("{host}", uiState.host) + .replace("{port}", uiState.port) + ), testTag = "ElectrumUpdatedToast", ) } else { app.toast( type = ToastType.WARNING, - title = context.getString(R.string.settings__es__server_error), - body = context.getString(R.string.settings__es__server_error_description), + titleRes = R.string.settings__es__server_error, + bodyRes = R.string.settings__es__server_error_description, testTag = "ElectrumErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 5072b5f15..27962dde2 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -26,6 +25,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM @@ -51,7 +51,6 @@ fun RgsServerScreen( ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val app = appViewModel ?: return - val context = LocalContext.current // Handle result from Scanner LaunchedEffect(savedStateHandle) { @@ -69,15 +68,15 @@ fun RgsServerScreen( if (result.isSuccess) { app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.settings__rgs__update_success_title), - body = context.getString(R.string.settings__rgs__update_success_description), + titleRes = R.string.settings__rgs__update_success_title, + bodyRes = R.string.settings__rgs__update_success_description, testTag = "RgsUpdatedToast", ) } else { app.toast( type = ToastType.ERROR, - title = context.getString(R.string.wallet__ldk_start_error_title), - body = result.exceptionOrNull()?.message ?: "Unknown error", + title = ToastText.Resource(R.string.wallet__ldk_start_error_title), + body = ToastText.Literal(result.exceptionOrNull()?.message ?: "Unknown error"), testTag = "RgsErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 2cd13a0fb..ca53e3928 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -55,6 +55,7 @@ import to.bitkit.ext.DatePattern import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.setClipboardText +import to.bitkit.models.ToastText import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.appViewModel @@ -130,8 +131,8 @@ fun ChannelDetailScreen( context.setClipboardText(text) app.toast( type = ToastType.SUCCESS, - title = context.getString(R.string.common__copied), - body = text, + title = ToastText.Resource(R.string.common__copied), + body = ToastText.Literal(text), ) }, onOpenUrl = { txId -> From a9b764968172a2d629e847303a5f978101d4cd9f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 23:14:04 +0100 Subject: [PATCH 14/35] refactor: use @StringRes in ViewModel toasts Co-Authored-By: Claude Opus 4.5 --- .../recovery/RecoveryMnemonicViewModel.kt | 2 +- .../ui/screens/recovery/RecoveryViewModel.kt | 12 +++---- .../external/ExternalNodeViewModel.kt | 27 +++++++++------- .../external/LnurlChannelViewModel.kt | 9 +++--- .../screens/wallets/send/SendFeeViewModel.kt | 8 ++--- .../advanced/ElectrumConfigViewModel.kt | 9 +++--- .../backups/BackupNavSheetViewModel.kt | 8 ++--- .../LightningConnectionsViewModel.kt | 12 +++---- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 32 +++++++++++++++++++ .../to/bitkit/viewmodels/TransferViewModel.kt | 17 +++++----- .../to/bitkit/viewmodels/WalletViewModel.kt | 13 ++++---- 11 files changed, 95 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt index 48fcd3816..ea11d4f30 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt @@ -42,7 +42,7 @@ class RecoveryMnemonicViewModel @Inject constructor( isLoading = false, ) } - toaster.error(context.getString(R.string.security__mnemonic_load_error)) + toaster.error(titleRes = R.string.security__mnemonic_load_error) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index d229d64d3..0612d4dd6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -74,8 +74,8 @@ class RecoveryViewModel @Inject constructor( ) } toaster.error( - context.getString(R.string.common__error), - context.getString(R.string.other__logs_export_error), + titleRes = R.string.common__error, + bodyRes = R.string.other__logs_export_error, ) } ) @@ -98,8 +98,8 @@ class RecoveryViewModel @Inject constructor( Logger.error("Failed to open support links", fallbackError, context = TAG) viewModelScope.launch { toaster.error( - context.getString(R.string.common__error), - context.getString(R.string.settings__support__link_error), + titleRes = R.string.common__error, + bodyRes = R.string.settings__support__link_error, ) } } @@ -120,8 +120,8 @@ class RecoveryViewModel @Inject constructor( toaster.error(error) }.onSuccess { toaster.success( - context.getString(R.string.security__wiped_title), - context.getString(R.string.security__wiped_message), + titleRes = R.string.security__wiped_title, + bodyRes = R.string.security__wiped_message, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index ed1fcea4e..329be3aa7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -20,6 +20,7 @@ import to.bitkit.data.SettingsStore import to.bitkit.ext.WatchResult import to.bitkit.ext.of import to.bitkit.ext.watchUntil +import to.bitkit.models.ToastText import to.bitkit.models.TransactionSpeed import to.bitkit.models.TransferType import to.bitkit.models.formatToModernDisplay @@ -74,8 +75,8 @@ class ExternalNodeViewModel @Inject constructor( setEffect(SideEffect.ConnectionSuccess) } else { toaster.error( - context.getString(R.string.lightning__error_add_title), - context.getString(R.string.lightning__error_add), + titleRes = R.string.lightning__error_add_title, + bodyRes = R.string.lightning__error_add, ) } } @@ -88,7 +89,7 @@ class ExternalNodeViewModel @Inject constructor( if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } } else { - toaster.error(context.getString(R.string.lightning__error_add_uri)) + toaster.error(titleRes = R.string.lightning__error_add_uri) } } } @@ -99,9 +100,11 @@ class ExternalNodeViewModel @Inject constructor( if (sats > maxAmount) { viewModelScope.launch { toaster.error( - title = context.getString(R.string.lightning__spending_amount__error_max__title), - body = context.getString(R.string.lightning__spending_amount__error_max__description) - .replace("{amount}", maxAmount.formatToModernDisplay()), + title = ToastText.Resource(R.string.lightning__spending_amount__error_max__title), + body = ToastText.Literal( + context.getString(R.string.lightning__spending_amount__error_max__description) + .replace("{amount}", maxAmount.formatToModernDisplay()) + ), ) } return @@ -133,8 +136,8 @@ class ExternalNodeViewModel @Inject constructor( val feeRate = _uiState.value.customFeeRate ?: 0u if (feeRate == 0u) { toaster.info( - context.getString(R.string.wallet__min_possible_fee_rate), - context.getString(R.string.wallet__min_possible_fee_rate_msg), + titleRes = R.string.wallet__min_possible_fee_rate, + bodyRes = R.string.wallet__min_possible_fee_rate_msg, ) return false } @@ -188,9 +191,11 @@ class ExternalNodeViewModel @Inject constructor( val error = e.message.orEmpty() Logger.warn("Error opening channel with peer: '${_uiState.value.peer}': '$error'") toaster.error( - title = context.getString(R.string.lightning__error_channel_purchase), - body = context.getString(R.string.lightning__error_channel_setup_msg) - .replace("{raw}", error), + title = ToastText.Resource(R.string.lightning__error_channel_purchase), + body = ToastText.Literal( + context.getString(R.string.lightning__error_channel_setup_msg) + .replace("{raw}", error) + ), ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index f60d580fb..40ac52756 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R import to.bitkit.ext.of +import to.bitkit.models.ToastText import to.bitkit.repositories.LightningRepo import to.bitkit.ui.Routes import to.bitkit.ui.shared.toast.Toaster @@ -69,8 +70,8 @@ class LnurlChannelViewModel @Inject constructor( lightningRepo.requestLnurlChannel(callback = params.callback, k1 = params.k1, nodeId = nodeId) .onSuccess { toaster.success( - context.getString(R.string.other__lnurl_channel_success_title), - context.getString(R.string.other__lnurl_channel_success_msg_no_peer), + titleRes = R.string.other__lnurl_channel_success_title, + bodyRes = R.string.other__lnurl_channel_success_msg_no_peer, ) _uiState.update { it.copy(isConnected = true) } }.onFailure { error -> @@ -83,8 +84,8 @@ class LnurlChannelViewModel @Inject constructor( suspend fun errorToast(error: Throwable) { toaster.error( - title = context.getString(R.string.other__lnurl_channel_error), - body = error.message ?: "Unknown error", + title = ToastText.Resource(R.string.other__lnurl_channel_error), + body = ToastText.Literal(error.message ?: context.getString(R.string.common__error_body)), ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index 417a8eef9..3d44046e8 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -106,16 +106,16 @@ class SendFeeViewModel @Inject constructor( val minSatsPerVByte = sendUiState.feeRates?.slow ?: 1u if (satsPerVByte < minSatsPerVByte) { toaster.info( - context.getString(R.string.wallet__min_possible_fee_rate), - context.getString(R.string.wallet__min_possible_fee_rate_msg), + titleRes = R.string.wallet__min_possible_fee_rate, + bodyRes = R.string.wallet__min_possible_fee_rate_msg, ) return false } if (satsPerVByte > maxSatsPerVByte) { toaster.info( - context.getString(R.string.wallet__max_possible_fee_rate), - context.getString(R.string.wallet__max_possible_fee_rate_msg), + titleRes = R.string.wallet__max_possible_fee_rate, + bodyRes = R.string.wallet__max_possible_fee_rate_msg, ) return false } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt index 70b456f10..bb189f8b5 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt @@ -23,6 +23,7 @@ import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServer import to.bitkit.models.ElectrumServerPeer import to.bitkit.models.MAX_VALID_PORT +import to.bitkit.models.ToastText import to.bitkit.models.getDefaultPort import to.bitkit.repositories.LightningRepo import to.bitkit.ui.shared.toast.Toaster @@ -247,8 +248,8 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput() if (validationError != null) { toaster.warning( - title = context.getString(R.string.settings__es__error_peer), - body = validationError, + title = ToastText.Resource(R.string.settings__es__error_peer), + body = ToastText.Literal(validationError), ) } else { connectToServer() @@ -268,8 +269,8 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput(host, port) if (validationError != null) { toaster.warning( - title = context.getString(R.string.settings__es__error_peer), - body = validationError, + title = ToastText.Resource(R.string.settings__es__error_peer), + body = ToastText.Literal(validationError), ) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index 8bb0947f3..e50f624f3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -87,8 +87,8 @@ class BackupNavSheetViewModel @Inject constructor( }.onFailure { Logger.error("Error loading mnemonic", it, context = TAG) toaster.warning( - context.getString(R.string.security__mnemonic_error), - context.getString(R.string.security__mnemonic_error_description), + titleRes = R.string.security__mnemonic_error, + bodyRes = R.string.security__mnemonic_error_description, ) } } @@ -157,8 +157,8 @@ class BackupNavSheetViewModel @Inject constructor( fun onMnemonicCopied() { viewModelScope.launch { toaster.success( - context.getString(R.string.common__copied), - context.getString(R.string.security__mnemonic_copied), + titleRes = R.string.common__copied, + bodyRes = R.string.security__mnemonic_copied, ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index 0126fbf6b..52f728917 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -340,8 +340,8 @@ class LightningConnectionsViewModel @Inject constructor( .onSuccess { uri -> onReady(uri) } .onFailure { toaster.warning( - context.getString(R.string.lightning__error_logs), - context.getString(R.string.lightning__error_logs_description), + titleRes = R.string.lightning__error_logs, + bodyRes = R.string.lightning__error_logs_description, ) } } @@ -454,8 +454,8 @@ class LightningConnectionsViewModel @Inject constructor( walletRepo.syncNodeAndWallet() toaster.success( - context.getString(R.string.lightning__close_success_title), - context.getString(R.string.lightning__close_success_msg), + titleRes = R.string.lightning__close_success_title, + bodyRes = R.string.lightning__close_success_msg, ) _closeConnectionUiState.update { @@ -469,8 +469,8 @@ class LightningConnectionsViewModel @Inject constructor( Logger.error("Failed to close channel", e = error, context = TAG) toaster.warning( - context.getString(R.string.lightning__close_error), - context.getString(R.string.lightning__close_error_msg), + titleRes = R.string.lightning__close_error, + bodyRes = R.string.lightning__close_error_msg, ) _closeConnectionUiState.update { it.copy(isLoading = false) } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index cae6b7b37..59d103fc3 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -95,6 +95,38 @@ class Toaster @Inject constructor() { ) // endregion + // region ToastText overloads + suspend fun success( + title: ToastText, + body: ToastText? = null, + testTag: String? = null, + ) = emit(ToastType.SUCCESS, title, body, testTag = testTag) + + suspend fun info( + title: ToastText, + body: ToastText? = null, + testTag: String? = null, + ) = emit(ToastType.INFO, title, body, testTag = testTag) + + suspend fun lightning( + title: ToastText, + body: ToastText? = null, + testTag: String? = null, + ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) + + suspend fun warning( + title: ToastText, + body: ToastText? = null, + testTag: String? = null, + ) = emit(ToastType.WARNING, title, body, testTag = testTag) + + suspend fun error( + title: ToastText, + body: ToastText? = null, + testTag: String? = null, + ) = emit(ToastType.ERROR, title, body, testTag = testTag) + // endregion + // region String literal overloads suspend fun success( title: String, diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 02af58076..705089029 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -29,6 +29,7 @@ import to.bitkit.R import to.bitkit.data.CacheStore import to.bitkit.data.SettingsStore import to.bitkit.ext.amountOnClose +import to.bitkit.models.ToastText import to.bitkit.models.TransactionSpeed import to.bitkit.models.TransferType import to.bitkit.models.safe @@ -459,8 +460,8 @@ class TransferViewModel @Inject constructor( channelsToClose = emptyList() Logger.error("Cannot force close channels with trusted peer", context = TAG) toaster.error( - context.getString(R.string.lightning__force_failed_title), - context.getString(R.string.lightning__force_failed_msg), + titleRes = R.string.lightning__force_failed_title, + bodyRes = R.string.lightning__force_failed_msg, ) return@runCatching } @@ -483,21 +484,21 @@ class TransferViewModel @Inject constructor( val skippedMsg = context.getString(R.string.lightning__force_channels_skipped) val bodyText = if (trustedChannels.isNotEmpty()) "$initMsg $skippedMsg" else initMsg toaster.lightning( - title = context.getString(R.string.lightning__force_init_title), - body = bodyText, + title = ToastText.Resource(R.string.lightning__force_init_title), + body = ToastText.Literal(bodyText), ) } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) toaster.error( - context.getString(R.string.lightning__force_failed_title), - context.getString(R.string.lightning__force_failed_msg), + titleRes = R.string.lightning__force_failed_title, + bodyRes = R.string.lightning__force_failed_msg, ) } }.onFailure { Logger.error("Force close failed", e = it, context = TAG) toaster.error( - context.getString(R.string.lightning__force_failed_title), - context.getString(R.string.lightning__force_failed_msg), + titleRes = R.string.lightning__force_failed_title, + bodyRes = R.string.lightning__force_failed_msg, ) } _isForceTransferLoading.value = false diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 9fc2a6d38..22192fa7c 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -26,6 +26,7 @@ import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.di.BgDispatcher +import to.bitkit.models.ToastText import to.bitkit.repositories.BackupRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo @@ -301,14 +302,14 @@ class WalletViewModel @Inject constructor( lightningRepo.disconnectPeer(peer) .onSuccess { toaster.info( - context.getString(R.string.common__success), - context.getString(R.string.wallet__peer_disconnected), + titleRes = R.string.common__success, + bodyRes = R.string.wallet__peer_disconnected, ) } .onFailure { toaster.error( - title = context.getString(R.string.common__error), - body = it.message ?: context.getString(R.string.common__error_body) + title = ToastText.Resource(R.string.common__error), + body = ToastText.Literal(it.message ?: context.getString(R.string.common__error_body)), ) } } @@ -317,8 +318,8 @@ class WalletViewModel @Inject constructor( fun updateBip21Invoice(amountSats: ULong? = walletState.value.bip21AmountSats) = viewModelScope.launch { walletRepo.updateBip21Invoice(amountSats).onFailure { error -> toaster.error( - title = context.getString(R.string.wallet__error_invoice_update), - body = error.message ?: context.getString(R.string.common__error_body) + title = ToastText.Resource(R.string.wallet__error_invoice_update), + body = ToastText.Literal(error.message ?: context.getString(R.string.common__error_body)), ) } } From 84c5eee51b49791e109200b6dd68c4b4450e89fa Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Fri, 16 Jan 2026 23:18:25 +0100 Subject: [PATCH 15/35] fix: localize hardcoded toast strings Co-Authored-By: Claude Opus 4.5 --- .../screens/wallets/send/SendCoinSelectionViewModel.kt | 9 ++++++++- app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt | 6 ++++-- .../main/java/to/bitkit/viewmodels/WalletViewModel.kt | 4 ++-- app/src/main/res/values/strings.xml | 3 +++ 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt index b3cfcb9b6..3f5c1bb07 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt @@ -1,16 +1,19 @@ package to.bitkit.ui.screens.wallets.send +import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.synonym.bitkitcore.Activity import com.synonym.bitkitcore.Activity.Onchain import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.SpendableUtxo +import to.bitkit.R import to.bitkit.di.BgDispatcher import to.bitkit.env.Defaults import to.bitkit.ext.rawId @@ -22,6 +25,7 @@ import javax.inject.Inject @HiltViewModel class SendCoinSelectionViewModel @Inject constructor( + @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val lightningRepo: LightningRepo, private val activityRepo: ActivityRepo, @@ -68,7 +72,10 @@ class SendCoinSelectionViewModel @Inject constructor( } }.onFailure { Logger.error("Failed to load UTXOs for coin selection", it, context = TAG) - toaster.error("Failed to load UTXOs: ${it.message}") + toaster.error( + context.getString(R.string.wallet__error_utxo_load) + .replace("{raw}", it.message.orEmpty()) + ) } } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 59d103fc3..23370f216 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -4,6 +4,7 @@ import androidx.annotation.StringRes import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow +import to.bitkit.R import to.bitkit.models.Toast import to.bitkit.models.ToastText import to.bitkit.models.ToastType @@ -185,8 +186,9 @@ class Toaster @Inject constructor() { suspend fun error(throwable: Throwable) = emit( type = ToastType.ERROR, - title = ToastText.Literal("Error"), - body = ToastText.Literal(throwable.message ?: "An unknown error occurred"), + title = ToastText.Resource(R.string.common__error), + body = throwable.message?.let { ToastText.Literal(it) } + ?: ToastText.Resource(R.string.common__error_body), ) // endregion } diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 22192fa7c..56eaca2a2 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -128,8 +128,8 @@ class WalletViewModel @Inject constructor( migrationService.markMigrationChecked() migrationService.setShowingMigrationLoading(false) toaster.error( - title = "Migration Failed", - body = "Please restore your wallet manually using your recovery phrase" + titleRes = R.string.wallet__migration_error_title, + bodyRes = R.string.wallet__migration_error_body, ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54f5d0b3c..415efb3db 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1094,6 +1094,7 @@ Please check your transaction info and try again. No transaction is available to broadcast. Error Sending + Failed to load UTXOs: {raw} Apply Clear Select Range @@ -1112,6 +1113,8 @@ Withdraw Bitcoin Fee Exceeds Maximum Limit Lower the custom fee and try again. + Please restore your wallet manually using your recovery phrase + Migration Failed Fee Below Minimum Limit Increase the custom fee and try again. MINIMUM From f50889f04ed997147a8883cf607b8a90d547ced3 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 03:14:57 +0100 Subject: [PATCH 16/35] refactor: prettify Toast and Toaster APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add ToastText() factory constructors (invoke operators) - Remove redundant @Stable from ToastType enum and value classes - Rename titleRes/bodyRes → title/body in @StringRes overloads - Rename warning() → warn() across all overloads - Update all call sites for the API changes Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/Toast.kt | 1 - .../main/java/to/bitkit/models/ToastText.kt | 7 ++- .../bitkit/ui/components/IsOnlineTracker.kt | 10 ++-- .../recovery/RecoveryMnemonicViewModel.kt | 2 +- .../ui/screens/recovery/RecoveryViewModel.kt | 12 ++--- .../ui/screens/scanner/QrScanningScreen.kt | 8 ++-- .../screens/transfer/SavingsProgressScreen.kt | 8 ++-- .../external/ExternalNodeViewModel.kt | 10 ++-- .../external/LnurlChannelViewModel.kt | 4 +- .../wallets/activity/ActivityDetailScreen.kt | 16 +++---- .../screens/wallets/send/SendFeeViewModel.kt | 8 ++-- .../to/bitkit/ui/settings/SettingsScreen.kt | 4 +- .../settings/advanced/ElectrumConfigScreen.kt | 4 +- .../advanced/ElectrumConfigViewModel.kt | 4 +- .../ui/settings/advanced/RgsServerScreen.kt | 4 +- .../backups/BackupNavSheetViewModel.kt | 10 ++-- .../LightningConnectionsViewModel.kt | 16 +++---- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 46 +++++++++---------- .../java/to/bitkit/viewmodels/AppViewModel.kt | 8 ++-- .../bitkit/viewmodels/DevSettingsViewModel.kt | 6 +-- .../to/bitkit/viewmodels/LdkDebugViewModel.kt | 8 ++-- .../to/bitkit/viewmodels/TransferViewModel.kt | 12 ++--- .../to/bitkit/viewmodels/WalletViewModel.kt | 8 ++-- 23 files changed, 109 insertions(+), 107 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index 8dbc764ab..debb58267 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -18,5 +18,4 @@ data class Toast( } } -@Stable enum class ToastType { SUCCESS, INFO, LIGHTNING, WARNING, ERROR } diff --git a/app/src/main/java/to/bitkit/models/ToastText.kt b/app/src/main/java/to/bitkit/models/ToastText.kt index bb9335387..fb36e8a26 100644 --- a/app/src/main/java/to/bitkit/models/ToastText.kt +++ b/app/src/main/java/to/bitkit/models/ToastText.kt @@ -9,12 +9,15 @@ import androidx.compose.ui.res.stringResource @Stable sealed interface ToastText { @JvmInline - @Stable value class Resource(@StringRes val resId: Int) : ToastText @JvmInline - @Stable 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) + } } @Composable diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index 2b0bcd07c..37a65fcb8 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -30,15 +30,15 @@ fun IsOnlineTracker( when (connectivityState) { ConnectivityState.CONNECTED -> { toaster.success( - titleRes = R.string.other__connection_back_title, - bodyRes = R.string.other__connection_back_msg, + title = R.string.other__connection_back_title, + body = R.string.other__connection_back_msg, ) } ConnectivityState.DISCONNECTED -> { - toaster.warning( - titleRes = R.string.other__connection_issue, - bodyRes = R.string.other__connection_issue_explain, + toaster.warn( + title = R.string.other__connection_issue, + body = R.string.other__connection_issue_explain, ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt index ea11d4f30..537ddff7b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt @@ -42,7 +42,7 @@ class RecoveryMnemonicViewModel @Inject constructor( isLoading = false, ) } - toaster.error(titleRes = R.string.security__mnemonic_load_error) + toaster.error(title = R.string.security__mnemonic_load_error) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index 0612d4dd6..df55fd3fc 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -74,8 +74,8 @@ class RecoveryViewModel @Inject constructor( ) } toaster.error( - titleRes = R.string.common__error, - bodyRes = R.string.other__logs_export_error, + title = R.string.common__error, + body = R.string.other__logs_export_error, ) } ) @@ -98,8 +98,8 @@ class RecoveryViewModel @Inject constructor( Logger.error("Failed to open support links", fallbackError, context = TAG) viewModelScope.launch { toaster.error( - titleRes = R.string.common__error, - bodyRes = R.string.settings__support__link_error, + title = R.string.common__error, + body = R.string.settings__support__link_error, ) } } @@ -120,8 +120,8 @@ class RecoveryViewModel @Inject constructor( toaster.error(error) }.onSuccess { toaster.success( - titleRes = R.string.security__wiped_title, - bodyRes = R.string.security__wiped_message, + title = R.string.security__wiped_title, + body = R.string.security__wiped_message, ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 0490be66d..860d19bea 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -151,8 +151,8 @@ fun QrScanningScreen( Logger.error("Failed to scan QR code", error) app.toast( type = ToastType.ERROR, - titleRes = R.string.other__qr_error_header, - bodyRes = R.string.other__qr_error_text, + title = R.string.other__qr_error_header, + body = R.string.other__qr_error_text, ) } } @@ -257,8 +257,8 @@ private fun handlePaste( if (clipboard.isNullOrBlank()) { app.toast( type = ToastType.WARNING, - titleRes = R.string.wallet__send_clipboard_empty_title, - bodyRes = R.string.wallet__send_clipboard_empty_text, + title = R.string.wallet__send_clipboard_empty_title, + body = R.string.wallet__send_clipboard_empty_text, ) } setScanResult(clipboard) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 089241e90..0114cb4cf 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -70,8 +70,8 @@ fun SavingsProgressScreen( // All channels are trusted peers - show error and navigate back immediately app.toast( type = ToastType.ERROR, - titleRes = R.string.lightning__close_error, - bodyRes = R.string.lightning__close_error_msg, + title = R.string.lightning__close_error, + body = R.string.lightning__close_error_msg, ) onTransferUnavailable() } else { @@ -81,8 +81,8 @@ fun SavingsProgressScreen( onTransferUnavailable = { app.toast( type = ToastType.ERROR, - titleRes = R.string.lightning__close_error, - bodyRes = R.string.lightning__close_error_msg, + title = R.string.lightning__close_error, + body = R.string.lightning__close_error_msg, ) onTransferUnavailable() }, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index 329be3aa7..cc3aca148 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -75,8 +75,8 @@ class ExternalNodeViewModel @Inject constructor( setEffect(SideEffect.ConnectionSuccess) } else { toaster.error( - titleRes = R.string.lightning__error_add_title, - bodyRes = R.string.lightning__error_add, + title = R.string.lightning__error_add_title, + body = R.string.lightning__error_add, ) } } @@ -89,7 +89,7 @@ class ExternalNodeViewModel @Inject constructor( if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } } else { - toaster.error(titleRes = R.string.lightning__error_add_uri) + toaster.error(title = R.string.lightning__error_add_uri) } } } @@ -136,8 +136,8 @@ class ExternalNodeViewModel @Inject constructor( val feeRate = _uiState.value.customFeeRate ?: 0u if (feeRate == 0u) { toaster.info( - titleRes = R.string.wallet__min_possible_fee_rate, - bodyRes = R.string.wallet__min_possible_fee_rate_msg, + title = R.string.wallet__min_possible_fee_rate, + body = R.string.wallet__min_possible_fee_rate_msg, ) return false } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index 40ac52756..ce08fb56d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -70,8 +70,8 @@ class LnurlChannelViewModel @Inject constructor( lightningRepo.requestLnurlChannel(callback = params.callback, k1 = params.k1, nodeId = nodeId) .onSuccess { toaster.success( - titleRes = R.string.other__lnurl_channel_success_title, - bodyRes = R.string.other__lnurl_channel_success_msg_no_peer, + title = R.string.other__lnurl_channel_success_title, + body = R.string.other__lnurl_channel_success_msg_no_peer, ) _uiState.update { it.copy(isConnected = true) } }.onFailure { error -> diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index e0e4abd82..e50ef2593 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -251,8 +251,8 @@ fun ActivityDetailScreen( onSuccess = { app.toast( type = ToastType.SUCCESS, - titleRes = R.string.wallet__boost_success_title, - bodyRes = R.string.wallet__boost_success_msg, + title = R.string.wallet__boost_success_title, + body = R.string.wallet__boost_success_msg, testTag = "BoostSuccessToast" ) listViewModel.resync() @@ -261,8 +261,8 @@ fun ActivityDetailScreen( onFailure = { app.toast( type = ToastType.ERROR, - titleRes = R.string.wallet__boost_error_title, - bodyRes = R.string.wallet__boost_error_msg, + title = R.string.wallet__boost_error_title, + body = R.string.wallet__boost_error_msg, testTag = "BoostFailureToast" ) detailViewModel.onDismissBoostSheet() @@ -270,15 +270,15 @@ fun ActivityDetailScreen( onMaxFee = { app.toast( type = ToastType.ERROR, - titleRes = R.string.wallet__send_fee_error, - bodyRes = R.string.wallet__send_fee_error_max, + title = R.string.wallet__send_fee_error, + body = R.string.wallet__send_fee_error_max, ) }, onMinFee = { app.toast( type = ToastType.ERROR, - titleRes = R.string.wallet__send_fee_error, - bodyRes = R.string.wallet__send_fee_error_min, + title = R.string.wallet__send_fee_error, + body = R.string.wallet__send_fee_error_min, ) } ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index 3d44046e8..105bafe93 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -106,16 +106,16 @@ class SendFeeViewModel @Inject constructor( val minSatsPerVByte = sendUiState.feeRates?.slow ?: 1u if (satsPerVByte < minSatsPerVByte) { toaster.info( - titleRes = R.string.wallet__min_possible_fee_rate, - bodyRes = R.string.wallet__min_possible_fee_rate_msg, + title = R.string.wallet__min_possible_fee_rate, + body = R.string.wallet__min_possible_fee_rate_msg, ) return false } if (satsPerVByte > maxSatsPerVByte) { toaster.info( - titleRes = R.string.wallet__max_possible_fee_rate, - bodyRes = R.string.wallet__max_possible_fee_rate_msg, + title = R.string.wallet__max_possible_fee_rate, + body = R.string.wallet__max_possible_fee_rate_msg, ) return false } diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index ac1c19c2d..c864549e0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -85,8 +85,8 @@ fun SettingsScreen( } app.toast( type = ToastType.SUCCESS, - titleRes = titleRes, - bodyRes = bodyRes, + title = titleRes, + body = bodyRes, ) enableDevModeTapCount = 0 } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 57b675d30..ae98fc792 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -88,8 +88,8 @@ fun ElectrumConfigScreen( } else { app.toast( type = ToastType.WARNING, - titleRes = R.string.settings__es__server_error, - bodyRes = R.string.settings__es__server_error_description, + title = R.string.settings__es__server_error, + body = R.string.settings__es__server_error_description, testTag = "ElectrumErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt index bb189f8b5..a43d52f99 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt @@ -247,7 +247,7 @@ class ElectrumConfigViewModel @Inject constructor( viewModelScope.launch { val validationError = validateInput() if (validationError != null) { - toaster.warning( + toaster.warn( title = ToastText.Resource(R.string.settings__es__error_peer), body = ToastText.Literal(validationError), ) @@ -268,7 +268,7 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput(host, port) if (validationError != null) { - toaster.warning( + toaster.warn( title = ToastText.Resource(R.string.settings__es__error_peer), body = ToastText.Literal(validationError), ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 27962dde2..1c1c11f8c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -68,8 +68,8 @@ fun RgsServerScreen( if (result.isSuccess) { app.toast( type = ToastType.SUCCESS, - titleRes = R.string.settings__rgs__update_success_title, - bodyRes = R.string.settings__rgs__update_success_description, + title = R.string.settings__rgs__update_success_title, + body = R.string.settings__rgs__update_success_description, testTag = "RgsUpdatedToast", ) } else { diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index e50f624f3..a5e6b80ff 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -86,9 +86,9 @@ class BackupNavSheetViewModel @Inject constructor( } }.onFailure { Logger.error("Error loading mnemonic", it, context = TAG) - toaster.warning( - titleRes = R.string.security__mnemonic_error, - bodyRes = R.string.security__mnemonic_error_description, + toaster.warn( + title = R.string.security__mnemonic_error, + body = R.string.security__mnemonic_error_description, ) } } @@ -157,8 +157,8 @@ class BackupNavSheetViewModel @Inject constructor( fun onMnemonicCopied() { viewModelScope.launch { toaster.success( - titleRes = R.string.common__copied, - bodyRes = R.string.security__mnemonic_copied, + title = R.string.common__copied, + body = R.string.security__mnemonic_copied, ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index 52f728917..dd5e4c670 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -339,9 +339,9 @@ class LightningConnectionsViewModel @Inject constructor( logsRepo.zipLogsForSharing() .onSuccess { uri -> onReady(uri) } .onFailure { - toaster.warning( - titleRes = R.string.lightning__error_logs, - bodyRes = R.string.lightning__error_logs_description, + toaster.warn( + title = R.string.lightning__error_logs, + body = R.string.lightning__error_logs_description, ) } } @@ -454,8 +454,8 @@ class LightningConnectionsViewModel @Inject constructor( walletRepo.syncNodeAndWallet() toaster.success( - titleRes = R.string.lightning__close_success_title, - bodyRes = R.string.lightning__close_success_msg, + title = R.string.lightning__close_success_title, + body = R.string.lightning__close_success_msg, ) _closeConnectionUiState.update { @@ -468,9 +468,9 @@ class LightningConnectionsViewModel @Inject constructor( onFailure = { error -> Logger.error("Failed to close channel", e = error, context = TAG) - toaster.warning( - titleRes = R.string.lightning__close_error, - bodyRes = R.string.lightning__close_error_msg, + toaster.warn( + title = R.string.lightning__close_error, + body = R.string.lightning__close_error_msg, ) _closeConnectionUiState.update { it.copy(isLoading = false) } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 23370f216..39b40bffa 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -41,57 +41,57 @@ class Toaster @Inject constructor() { // region @StringRes overloads suspend fun success( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + @StringRes title: Int, + @StringRes body: Int? = null, testTag: String? = null, ) = emit( ToastType.SUCCESS, - ToastText.Resource(titleRes), - bodyRes?.let { ToastText.Resource(it) }, + ToastText.Resource(title), + body?.let { ToastText.Resource(it) }, testTag = testTag ) suspend fun info( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + @StringRes title: Int, + @StringRes body: Int? = null, testTag: String? = null, ) = emit( ToastType.INFO, - ToastText.Resource(titleRes), - bodyRes?.let { ToastText.Resource(it) }, + ToastText.Resource(title), + body?.let { ToastText.Resource(it) }, testTag = testTag, ) suspend fun lightning( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + @StringRes title: Int, + @StringRes body: Int? = null, testTag: String? = null, ) = emit( ToastType.LIGHTNING, - ToastText.Resource(titleRes), - bodyRes?.let { ToastText.Resource(it) }, + ToastText.Resource(title), + body?.let { ToastText.Resource(it) }, testTag = testTag ) - suspend fun warning( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + suspend fun warn( + @StringRes title: Int, + @StringRes body: Int? = null, testTag: String? = null, ) = emit( ToastType.WARNING, - ToastText.Resource(titleRes), - bodyRes?.let { ToastText.Resource(it) }, + ToastText.Resource(title), + body?.let { ToastText.Resource(it) }, testTag = testTag ) suspend fun error( - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + @StringRes title: Int, + @StringRes body: Int? = null, testTag: String? = null, ) = emit( ToastType.ERROR, - ToastText.Resource(titleRes), - bodyRes?.let { ToastText.Resource(it) }, + ToastText.Resource(title), + body?.let { ToastText.Resource(it) }, testTag = testTag, ) // endregion @@ -115,7 +115,7 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) - suspend fun warning( + suspend fun warn( title: ToastText, body: ToastText? = null, testTag: String? = null, @@ -162,7 +162,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun warning( + suspend fun warn( title: String, body: String? = null, testTag: String? = null, diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 3e3c8d9e4..c31098478 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1817,15 +1817,15 @@ class AppViewModel @Inject constructor( fun toast( type: ToastType, - @StringRes titleRes: Int, - @StringRes bodyRes: Int? = null, + @StringRes title: Int, + @StringRes body: Int? = null, autoHide: Boolean = true, duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, ) = toast( type = type, - title = ToastText.Resource(titleRes), - body = bodyRes?.let { ToastText.Resource(it) }, + title = ToastText.Resource(title), + body = body?.let { ToastText.Resource(it) }, autoHide = autoHide, duration = duration, testTag = testTag, diff --git a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt index c5e0a4ad2..3e8733923 100644 --- a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt @@ -47,7 +47,7 @@ class DevSettingsViewModel @Inject constructor( val peer = lightningRepo.getPeers()?.firstOrNull() if (peer == null) { - toaster.warning("No peer connected") + toaster.warn("No peer connected") return@launch } @@ -72,7 +72,7 @@ class DevSettingsViewModel @Inject constructor( ) toaster.info("LSP notification sent to this device") }.onFailure { - toaster.warning("Error testing LSP notification") + toaster.warn("Error testing LSP notification") } } @@ -99,7 +99,7 @@ class DevSettingsViewModel @Inject constructor( logsRepo.zipLogsForSharing() .onSuccess { uri -> onReady(uri) } .onFailure { - toaster.warning( + toaster.warn( context.getString(R.string.lightning__error_logs), context.getString(R.string.lightning__error_logs_description), ) diff --git a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt index 1ade94f06..3121e8577 100644 --- a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt @@ -43,7 +43,7 @@ class LdkDebugViewModel @Inject constructor( fun addPeer() { val uri = _uiState.value.nodeUri.trim() if (uri.isEmpty()) { - viewModelScope.launch { toaster.warning("Please enter a node URI") } + viewModelScope.launch { toaster.warn("Please enter a node URI") } return } connectPeer(uri) @@ -55,7 +55,7 @@ class LdkDebugViewModel @Inject constructor( val pastedUri = clipData?.getItemAt(0)?.text?.toString()?.trim() if (pastedUri.isNullOrEmpty()) { - viewModelScope.launch { toaster.warning("Clipboard is empty") } + viewModelScope.launch { toaster.warn("Clipboard is empty") } return } @@ -99,7 +99,7 @@ class LdkDebugViewModel @Inject constructor( _uiState.update { it.copy(networkGraphInfo = info) } toaster.info("Network graph info logged") } else { - toaster.warning("Failed to get network graph info") + toaster.warn("Failed to get network graph info") } } } @@ -162,7 +162,7 @@ class LdkDebugViewModel @Inject constructor( } toaster.info("Deleted key: $key") } else { - toaster.warning("Key not found: $key") + toaster.warn("Key not found: $key") } } .onFailure { e -> diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 705089029..4670bdcd5 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -460,8 +460,8 @@ class TransferViewModel @Inject constructor( channelsToClose = emptyList() Logger.error("Cannot force close channels with trusted peer", context = TAG) toaster.error( - titleRes = R.string.lightning__force_failed_title, - bodyRes = R.string.lightning__force_failed_msg, + title = R.string.lightning__force_failed_title, + body = R.string.lightning__force_failed_msg, ) return@runCatching } @@ -490,15 +490,15 @@ class TransferViewModel @Inject constructor( } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) toaster.error( - titleRes = R.string.lightning__force_failed_title, - bodyRes = R.string.lightning__force_failed_msg, + title = R.string.lightning__force_failed_title, + body = R.string.lightning__force_failed_msg, ) } }.onFailure { Logger.error("Force close failed", e = it, context = TAG) toaster.error( - titleRes = R.string.lightning__force_failed_title, - bodyRes = R.string.lightning__force_failed_msg, + title = R.string.lightning__force_failed_title, + body = R.string.lightning__force_failed_msg, ) } _isForceTransferLoading.value = false diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 56eaca2a2..78e7b1921 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -128,8 +128,8 @@ class WalletViewModel @Inject constructor( migrationService.markMigrationChecked() migrationService.setShowingMigrationLoading(false) toaster.error( - titleRes = R.string.wallet__migration_error_title, - bodyRes = R.string.wallet__migration_error_body, + title = R.string.wallet__migration_error_title, + body = R.string.wallet__migration_error_body, ) } } @@ -302,8 +302,8 @@ class WalletViewModel @Inject constructor( lightningRepo.disconnectPeer(peer) .onSuccess { toaster.info( - titleRes = R.string.common__success, - bodyRes = R.string.wallet__peer_disconnected, + title = R.string.common__success, + body = R.string.wallet__peer_disconnected, ) } .onFailure { From bb124a6d874207798cafa0e6867cf69e9d4f8e54 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 03:17:20 +0100 Subject: [PATCH 17/35] refactor: use ToastText() factory constructor Replace ToastText.Literal() and ToastText.Resource() calls with the cleaner ToastText() factory constructor. Co-Authored-By: Claude Opus 4.5 --- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 4 +- .../java/to/bitkit/ui/components/ToastView.kt | 20 ++++---- .../external/ExternalNodeViewModel.kt | 8 ++-- .../external/LnurlChannelViewModel.kt | 4 +- .../settings/advanced/AddressViewerScreen.kt | 4 +- .../settings/advanced/ElectrumConfigScreen.kt | 4 +- .../advanced/ElectrumConfigViewModel.kt | 8 ++-- .../ui/settings/advanced/RgsServerScreen.kt | 4 +- .../settings/lightning/ChannelDetailScreen.kt | 4 +- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 46 +++++++++---------- .../java/to/bitkit/viewmodels/AppViewModel.kt | 14 +++--- .../to/bitkit/viewmodels/TransferViewModel.kt | 4 +- .../to/bitkit/viewmodels/WalletViewModel.kt | 8 ++-- 13 files changed, 66 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 2317fe37a..719bf5130 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -93,8 +93,8 @@ fun NodeInfoScreen( onCopy = { text -> app.toast( type = ToastType.SUCCESS, - title = ToastText.Resource(R.string.common__copied), - body = ToastText.Literal(text), + title = ToastText(R.string.common__copied), + body = ToastText(text), ) }, ) diff --git a/app/src/main/java/to/bitkit/ui/components/ToastView.kt b/app/src/main/java/to/bitkit/ui/components/ToastView.kt index 98f89fc0d..f4389c030 100644 --- a/app/src/main/java/to/bitkit/ui/components/ToastView.kt +++ b/app/src/main/java/to/bitkit/ui/components/ToastView.kt @@ -309,8 +309,8 @@ private fun Preview() { ToastContent( toast = Toast( type = ToastType.WARNING, - title = ToastText.Literal("You're still offline"), - body = ToastText.Literal("Check your connection to keep using Bitkit."), + title = ToastText("You're still offline"), + body = ToastText("Check your connection to keep using Bitkit."), autoHide = true, ), onDismiss = {}, @@ -318,8 +318,8 @@ private fun Preview() { ToastContent( toast = Toast( type = ToastType.LIGHTNING, - title = ToastText.Literal("Instant Payments Ready"), - body = ToastText.Literal("You can now pay anyone, anywhere, instantly."), + title = ToastText("Instant Payments Ready"), + body = ToastText("You can now pay anyone, anywhere, instantly."), autoHide = true, ), onDismiss = {}, @@ -327,8 +327,8 @@ private fun Preview() { ToastContent( toast = Toast( type = ToastType.SUCCESS, - title = ToastText.Literal("You're Back Online!"), - body = ToastText.Literal("Successfully reconnected to the Internet."), + title = ToastText("You're Back Online!"), + body = ToastText("Successfully reconnected to the Internet."), autoHide = true, ), onDismiss = {}, @@ -336,8 +336,8 @@ private fun Preview() { ToastContent( toast = Toast( type = ToastType.INFO, - title = ToastText.Literal("General Message"), - body = ToastText.Literal("Used for neutral content to inform the user."), + title = ToastText("General Message"), + body = ToastText("Used for neutral content to inform the user."), autoHide = false, ), onDismiss = {}, @@ -345,8 +345,8 @@ private fun Preview() { ToastContent( toast = Toast( type = ToastType.ERROR, - title = ToastText.Literal("Error Toast"), - body = ToastText.Literal("This is a toast message."), + title = ToastText("Error Toast"), + body = ToastText("This is a toast message."), autoHide = true, ), onDismiss = {}, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index cc3aca148..1df49579a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -100,8 +100,8 @@ class ExternalNodeViewModel @Inject constructor( if (sats > maxAmount) { viewModelScope.launch { toaster.error( - title = ToastText.Resource(R.string.lightning__spending_amount__error_max__title), - body = ToastText.Literal( + title = ToastText(R.string.lightning__spending_amount__error_max__title), + body = ToastText( context.getString(R.string.lightning__spending_amount__error_max__description) .replace("{amount}", maxAmount.formatToModernDisplay()) ), @@ -191,8 +191,8 @@ class ExternalNodeViewModel @Inject constructor( val error = e.message.orEmpty() Logger.warn("Error opening channel with peer: '${_uiState.value.peer}': '$error'") toaster.error( - title = ToastText.Resource(R.string.lightning__error_channel_purchase), - body = ToastText.Literal( + title = ToastText(R.string.lightning__error_channel_purchase), + body = ToastText( context.getString(R.string.lightning__error_channel_setup_msg) .replace("{raw}", error) ), diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index ce08fb56d..ccb7ef4ad 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -84,8 +84,8 @@ class LnurlChannelViewModel @Inject constructor( suspend fun errorToast(error: Throwable) { toaster.error( - title = ToastText.Resource(R.string.other__lnurl_channel_error), - body = ToastText.Literal(error.message ?: context.getString(R.string.common__error_body)), + title = ToastText(R.string.other__lnurl_channel_error), + body = ToastText(error.message ?: context.getString(R.string.common__error_body)), ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index 58ff99f8c..55d8719a7 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -80,8 +80,8 @@ fun AddressViewerScreen( context.setClipboardText(text) app.toast( type = ToastType.SUCCESS, - title = ToastText.Resource(R.string.common__copied), - body = ToastText.Literal(text), + title = ToastText(R.string.common__copied), + body = ToastText(text), ) } ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index ae98fc792..e188903ba 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -77,8 +77,8 @@ fun ElectrumConfigScreen( if (result.isSuccess) { app.toast( type = ToastType.SUCCESS, - title = ToastText.Resource(R.string.settings__es__server_updated_title), - body = ToastText.Literal( + title = ToastText(R.string.settings__es__server_updated_title), + body = ToastText( context.getString(R.string.settings__es__server_updated_message) .replace("{host}", uiState.host) .replace("{port}", uiState.port) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt index a43d52f99..c3b53c322 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigViewModel.kt @@ -248,8 +248,8 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput() if (validationError != null) { toaster.warn( - title = ToastText.Resource(R.string.settings__es__error_peer), - body = ToastText.Literal(validationError), + title = ToastText(R.string.settings__es__error_peer), + body = ToastText(validationError), ) } else { connectToServer() @@ -269,8 +269,8 @@ class ElectrumConfigViewModel @Inject constructor( val validationError = validateInput(host, port) if (validationError != null) { toaster.warn( - title = ToastText.Resource(R.string.settings__es__error_peer), - body = ToastText.Literal(validationError), + title = ToastText(R.string.settings__es__error_peer), + body = ToastText(validationError), ) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 1c1c11f8c..b5eeccd4d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -75,8 +75,8 @@ fun RgsServerScreen( } else { app.toast( type = ToastType.ERROR, - title = ToastText.Resource(R.string.wallet__ldk_start_error_title), - body = ToastText.Literal(result.exceptionOrNull()?.message ?: "Unknown error"), + title = ToastText(R.string.wallet__ldk_start_error_title), + body = ToastText(result.exceptionOrNull()?.message ?: "Unknown error"), testTag = "RgsErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index ca53e3928..afd3bc17d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -131,8 +131,8 @@ fun ChannelDetailScreen( context.setClipboardText(text) app.toast( type = ToastType.SUCCESS, - title = ToastText.Resource(R.string.common__copied), - body = ToastText.Literal(text), + title = ToastText(R.string.common__copied), + body = ToastText(text), ) }, onOpenUrl = { txId -> diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 39b40bffa..8260ee841 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -46,8 +46,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.SUCCESS, - ToastText.Resource(title), - body?.let { ToastText.Resource(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag ) @@ -57,8 +57,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.INFO, - ToastText.Resource(title), - body?.let { ToastText.Resource(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) @@ -68,8 +68,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.LIGHTNING, - ToastText.Resource(title), - body?.let { ToastText.Resource(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag ) @@ -79,8 +79,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.WARNING, - ToastText.Resource(title), - body?.let { ToastText.Resource(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag ) @@ -90,8 +90,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.ERROR, - ToastText.Resource(title), - body?.let { ToastText.Resource(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) // endregion @@ -135,8 +135,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.SUCCESS, - ToastText.Literal(title), - body?.let { ToastText.Literal(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) @@ -146,8 +146,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.INFO, - ToastText.Literal(title), - body?.let { ToastText.Literal(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) @@ -157,8 +157,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.LIGHTNING, - ToastText.Literal(title), - body?.let { ToastText.Literal(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) @@ -168,8 +168,8 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.WARNING, - ToastText.Literal(title), - body?.let { ToastText.Literal(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) @@ -179,16 +179,16 @@ class Toaster @Inject constructor() { testTag: String? = null, ) = emit( ToastType.ERROR, - ToastText.Literal(title), - body?.let { ToastText.Literal(it) }, + ToastText(title), + body?.let { ToastText(it) }, testTag = testTag, ) suspend fun error(throwable: Throwable) = emit( type = ToastType.ERROR, - title = ToastText.Resource(R.string.common__error), - body = throwable.message?.let { ToastText.Literal(it) } - ?: ToastText.Resource(R.string.common__error_body), + title = ToastText(R.string.common__error), + body = throwable.message?.let { ToastText(it) } + ?: ToastText(R.string.common__error_body), ) // endregion } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index c31098478..109635996 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -1824,8 +1824,8 @@ class AppViewModel @Inject constructor( testTag: String? = null, ) = toast( type = type, - title = ToastText.Resource(title), - body = body?.let { ToastText.Resource(it) }, + title = ToastText(title), + body = body?.let { ToastText(it) }, autoHide = autoHide, duration = duration, testTag = testTag, @@ -1840,8 +1840,8 @@ class AppViewModel @Inject constructor( testTag: String? = null, ) = toast( type = type, - title = ToastText.Literal(title), - body = body?.let { ToastText.Literal(it) }, + title = ToastText(title), + body = body?.let { ToastText(it) }, autoHide = autoHide, duration = duration, testTag = testTag, @@ -1850,9 +1850,9 @@ class AppViewModel @Inject constructor( fun toast(error: Throwable) { toast( type = ToastType.ERROR, - title = ToastText.Resource(R.string.common__error), - body = error.message?.let { ToastText.Literal(it) } - ?: ToastText.Resource(R.string.common__error_body) + title = ToastText(R.string.common__error), + body = error.message?.let { ToastText(it) } + ?: ToastText(R.string.common__error_body) ) } diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 4670bdcd5..996260567 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -484,8 +484,8 @@ class TransferViewModel @Inject constructor( val skippedMsg = context.getString(R.string.lightning__force_channels_skipped) val bodyText = if (trustedChannels.isNotEmpty()) "$initMsg $skippedMsg" else initMsg toaster.lightning( - title = ToastText.Resource(R.string.lightning__force_init_title), - body = ToastText.Literal(bodyText), + title = ToastText(R.string.lightning__force_init_title), + body = ToastText(bodyText), ) } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 78e7b1921..76dbcb3cf 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -308,8 +308,8 @@ class WalletViewModel @Inject constructor( } .onFailure { toaster.error( - title = ToastText.Resource(R.string.common__error), - body = ToastText.Literal(it.message ?: context.getString(R.string.common__error_body)), + title = ToastText(R.string.common__error), + body = ToastText(it.message ?: context.getString(R.string.common__error_body)), ) } } @@ -318,8 +318,8 @@ class WalletViewModel @Inject constructor( fun updateBip21Invoice(amountSats: ULong? = walletState.value.bip21AmountSats) = viewModelScope.launch { walletRepo.updateBip21Invoice(amountSats).onFailure { error -> toaster.error( - title = ToastText.Resource(R.string.wallet__error_invoice_update), - body = ToastText.Literal(error.message ?: context.getString(R.string.common__error_body)), + title = ToastText(R.string.wallet__error_invoice_update), + body = ToastText(error.message ?: context.getString(R.string.common__error_body)), ) } } From fbe9826762f207c662752f3416ced5b48d9115ce Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 03:31:19 +0100 Subject: [PATCH 18/35] refactor: make Toaster methods non-suspend - Change emit() to use tryEmit() on SharedFlow - Remove suspend modifier from all toast methods - Make LocalToaster non-nullable with error default Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/ui/Locals.kt | 4 +-- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 36 +++++++++---------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/Locals.kt b/app/src/main/java/to/bitkit/ui/Locals.kt index 89688e9eb..e381b78b1 100644 --- a/app/src/main/java/to/bitkit/ui/Locals.kt +++ b/app/src/main/java/to/bitkit/ui/Locals.kt @@ -30,7 +30,7 @@ val LocalActivityListViewModel = staticCompositionLocalOf { null } val LocalSettingsViewModel = staticCompositionLocalOf { null } val LocalBackupsViewModel = staticCompositionLocalOf { null } -val LocalToaster = staticCompositionLocalOf { null } +val LocalToaster = staticCompositionLocalOf { error("Toaster not provided") } val appViewModel: AppViewModel? @Composable get() = LocalAppViewModel.current @@ -59,5 +59,5 @@ val backupsViewModel: BackupsViewModel? val drawerState: DrawerState? @Composable get() = LocalDrawerState.current -val toaster: Toaster? +val toaster: Toaster @Composable get() = LocalToaster.current diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index 8260ee841..f41feccb6 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -19,7 +19,7 @@ class Toaster @Inject constructor() { val events: SharedFlow = _events.asSharedFlow() @Suppress("LongParameterList") - private suspend fun emit( + private fun emit( type: ToastType, title: ToastText, body: ToastText? = null, @@ -27,7 +27,7 @@ class Toaster @Inject constructor() { duration: Duration = Toast.DURATION_DEFAULT, testTag: String? = null, ) { - _events.emit( + _events.tryEmit( Toast( type = type, title = title, @@ -40,7 +40,7 @@ class Toaster @Inject constructor() { } // region @StringRes overloads - suspend fun success( + fun success( @StringRes title: Int, @StringRes body: Int? = null, testTag: String? = null, @@ -51,7 +51,7 @@ class Toaster @Inject constructor() { testTag = testTag ) - suspend fun info( + fun info( @StringRes title: Int, @StringRes body: Int? = null, testTag: String? = null, @@ -62,7 +62,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun lightning( + fun lightning( @StringRes title: Int, @StringRes body: Int? = null, testTag: String? = null, @@ -73,7 +73,7 @@ class Toaster @Inject constructor() { testTag = testTag ) - suspend fun warn( + fun warn( @StringRes title: Int, @StringRes body: Int? = null, testTag: String? = null, @@ -84,7 +84,7 @@ class Toaster @Inject constructor() { testTag = testTag ) - suspend fun error( + fun error( @StringRes title: Int, @StringRes body: Int? = null, testTag: String? = null, @@ -97,31 +97,31 @@ class Toaster @Inject constructor() { // endregion // region ToastText overloads - suspend fun success( + fun success( title: ToastText, body: ToastText? = null, testTag: String? = null, ) = emit(ToastType.SUCCESS, title, body, testTag = testTag) - suspend fun info( + fun info( title: ToastText, body: ToastText? = null, testTag: String? = null, ) = emit(ToastType.INFO, title, body, testTag = testTag) - suspend fun lightning( + fun lightning( title: ToastText, body: ToastText? = null, testTag: String? = null, ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) - suspend fun warn( + fun warn( title: ToastText, body: ToastText? = null, testTag: String? = null, ) = emit(ToastType.WARNING, title, body, testTag = testTag) - suspend fun error( + fun error( title: ToastText, body: ToastText? = null, testTag: String? = null, @@ -129,7 +129,7 @@ class Toaster @Inject constructor() { // endregion // region String literal overloads - suspend fun success( + fun success( title: String, body: String? = null, testTag: String? = null, @@ -140,7 +140,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun info( + fun info( title: String, body: String? = null, testTag: String? = null, @@ -151,7 +151,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun lightning( + fun lightning( title: String, body: String? = null, testTag: String? = null, @@ -162,7 +162,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun warn( + fun warn( title: String, body: String? = null, testTag: String? = null, @@ -173,7 +173,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun error( + fun error( title: String, body: String? = null, testTag: String? = null, @@ -184,7 +184,7 @@ class Toaster @Inject constructor() { testTag = testTag, ) - suspend fun error(throwable: Throwable) = emit( + fun error(throwable: Throwable) = emit( type = ToastType.ERROR, title = ToastText(R.string.common__error), body = throwable.message?.let { ToastText(it) } From 22a08391aff1ce62175868152013d90bdd5f840b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 03:40:32 +0100 Subject: [PATCH 19/35] refactor: migrate external app.toast calls to toaster Replace all app.toast() and appViewModel.toast() calls in Screen composables with direct toaster method calls. Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/ui/ContentView.kt | 6 ++-- .../main/java/to/bitkit/ui/MainActivity.kt | 6 ++-- .../main/java/to/bitkit/ui/NodeInfoScreen.kt | 6 ++-- .../ui/screens/scanner/QrScanningScreen.kt | 25 ++++++++--------- .../ui/screens/settings/DevSettingsScreen.kt | 27 +++++++++--------- .../screens/transfer/SavingsProgressScreen.kt | 9 +++--- .../transfer/SpendingAdvancedScreen.kt | 10 +++---- .../wallets/activity/ActivityDetailScreen.kt | 20 +++++-------- .../wallets/activity/ActivityExploreScreen.kt | 8 ++---- .../wallets/receive/ReceiveAmountScreen.kt | 6 ++-- .../ui/settings/BlocktankRegtestScreen.kt | 28 +++++++------------ .../to/bitkit/ui/settings/SettingsScreen.kt | 8 ++---- .../settings/advanced/AddressViewerScreen.kt | 8 ++---- .../settings/advanced/ElectrumConfigScreen.kt | 11 +++----- .../ui/settings/advanced/RgsServerScreen.kt | 11 +++----- .../settings/lightning/ChannelDetailScreen.kt | 8 ++---- 16 files changed, 79 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 9d9b77290..43566bd51 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -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.ToastType import to.bitkit.models.WidgetType import to.bitkit.ui.Routes.ExternalConnection import to.bitkit.ui.components.AuthCheckScreen @@ -626,10 +625,9 @@ private fun RootNavHost( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, - toastException = { appViewModel.toast(it) }, + toastException = { appViewModel.toaster.error(it) }, toast = { title, body -> - appViewModel.toast( - type = ToastType.ERROR, + appViewModel.toaster.error( title = title, body = body ) diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 52bd312a1..9fc9b9030 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -265,7 +265,7 @@ private fun OnboardingNav( walletViewModel.setInitNodeLifecycleState() walletViewModel.createWallet(bip39Passphrase = null) }.onFailure { - appViewModel.toast(it) + appViewModel.toaster.error(it) } } }, @@ -295,7 +295,7 @@ private fun OnboardingNav( appViewModel.resetIsAuthenticatedState() walletViewModel.restoreWallet(mnemonic, passphrase) }.onFailure { - appViewModel.toast(it) + appViewModel.toaster.error(it) } } } @@ -310,7 +310,7 @@ private fun OnboardingNav( appViewModel.resetIsAuthenticatedState() walletViewModel.createWallet(bip39Passphrase = passphrase) }.onFailure { - appViewModel.toast(it) + appViewModel.toaster.error(it) } } }, diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 719bf5130..67eafc6c4 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -47,7 +47,6 @@ import to.bitkit.ext.formatToString import to.bitkit.ext.uri import to.bitkit.models.NodeLifecycleState import to.bitkit.models.ToastText -import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay import to.bitkit.repositories.LightningState import to.bitkit.ui.components.BodyM @@ -76,7 +75,7 @@ fun NodeInfoScreen( navController: NavController, ) { val wallet = walletViewModel ?: return - val app = appViewModel ?: return + val toaster = toaster val settings = settingsViewModel ?: return val isRefreshing by wallet.isRefreshing.collectAsStateWithLifecycle() @@ -91,8 +90,7 @@ fun NodeInfoScreen( onRefresh = { wallet.onPullToRefresh() }, onDisconnectPeer = { wallet.disconnectPeer(it) }, onCopy = { text -> - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = ToastText(R.string.common__copied), body = ToastText(text), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 860d19bea..4cda47e6d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -65,8 +65,6 @@ import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ext.getClipboardText import to.bitkit.ext.startActivityAppSettings -import to.bitkit.models.ToastType -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TextInput @@ -74,10 +72,11 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppAlertDialog import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.SheetTopBar +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.utils.Logger -import to.bitkit.viewmodels.AppViewModel import java.util.concurrent.Executors const val SCAN_REQUEST_KEY = "SCAN_REQUEST" @@ -93,7 +92,7 @@ fun QrScanningScreen( onBack: () -> Unit = { navController.popBackStack() }, onScanSuccess: (String) -> Unit, ) { - val app = appViewModel ?: return + val toaster = toaster val (scanResult, setScanResult) = remember { mutableStateOf(null) } @@ -140,7 +139,7 @@ fun QrScanningScreen( val context = LocalContext.current val previewView = remember { PreviewView(context) } val preview = remember { Preview.Builder().build() } - val analyzer = remember { + val analyzer = remember(toaster) { QrCodeAnalyzer { result -> if (result.isSuccess) { val qrCode = result.getOrThrow() @@ -149,8 +148,7 @@ fun QrScanningScreen( } else { val error = requireNotNull(result.exceptionOrNull()) Logger.error("Failed to scan QR code", error) - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.other__qr_error_header, body = R.string.other__qr_error_text, ) @@ -166,12 +164,12 @@ fun QrScanningScreen( val galleryLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent(), onResult = { uri -> - uri?.let { processImageFromGallery(context, it, setScanResult, onError = { e -> app.toast(e) }) } + uri?.let { processImageFromGallery(context, it, setScanResult, onError = { e -> toaster.error(e) }) } } ) val pickMedia = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { uri -> - uri?.let { processImageFromGallery(context, it, setScanResult, onError = { e -> app.toast(e) }) } + uri?.let { processImageFromGallery(context, it, setScanResult, onError = { e -> toaster.error(e) }) } } LaunchedEffect(lensFacing) { @@ -210,7 +208,7 @@ fun QrScanningScreen( context.startActivityAppSettings() }, onClickRetry = cameraPermissionState::launchPermissionRequest, - onClickPaste = handlePaste(context, app, setScanResult), + onClickPaste = handlePaste(context, toaster, setScanResult), onBack = onBack, ) }, @@ -239,7 +237,7 @@ fun QrScanningScreen( galleryLauncher.launch("image/*") } }, - onPasteFromClipboard = handlePaste(context, app, setScanResult), + onPasteFromClipboard = handlePaste(context, toaster, setScanResult), onSubmitDebug = setScanResult, ) } @@ -250,13 +248,12 @@ fun QrScanningScreen( @Composable private fun handlePaste( context: Context, - app: AppViewModel, + toaster: Toaster, setScanResult: (String?) -> Unit, ): () -> Unit = { val clipboard = context.getClipboardText()?.trim() if (clipboard.isNullOrBlank()) { - app.toast( - type = ToastType.WARNING, + toaster.warn( title = R.string.wallet__send_clipboard_empty_title, body = R.string.wallet__send_clipboard_empty_text, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt index c93bc2338..eb600dc5b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt @@ -14,10 +14,8 @@ import androidx.navigation.NavController import org.lightningdevkit.ldknode.Network import to.bitkit.R import to.bitkit.env.Env -import to.bitkit.models.ToastType import to.bitkit.ui.Routes import to.bitkit.ui.activityListViewModel -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.settings.SectionHeader import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.components.settings.SettingsTextButtonRow @@ -26,6 +24,7 @@ import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.util.shareZipFile +import to.bitkit.ui.toaster import to.bitkit.viewmodels.DevSettingsViewModel @Composable @@ -33,7 +32,7 @@ fun DevSettingsScreen( navController: NavController, viewModel: DevSettingsViewModel = hiltViewModel(), ) { - val app = appViewModel ?: return + val toaster = toaster val activity = activityListViewModel ?: return val settings = settingsViewModel ?: return val context = LocalContext.current @@ -78,63 +77,63 @@ fun DevSettingsScreen( title = "Reset Settings State", onClick = { settings.reset() - app.toast(type = ToastType.SUCCESS, title = "Settings state reset") + toaster.success(title = "Settings state reset") } ) SettingsTextButtonRow( title = "Reset All Activities", onClick = { activity.removeAllActivities() - app.toast(type = ToastType.SUCCESS, title = "Activities removed") + toaster.success(title = "Activities removed") } ) SettingsTextButtonRow( title = "Reset Backup State", onClick = { viewModel.resetBackupState() - app.toast(type = ToastType.SUCCESS, title = "Backup state reset") + toaster.success(title = "Backup state reset") } ) SettingsTextButtonRow( title = "Reset Widgets State", onClick = { viewModel.resetWidgetsState() - app.toast(type = ToastType.SUCCESS, title = "Widgets state reset") + toaster.success(title = "Widgets state reset") } ) SettingsTextButtonRow( title = "Refresh Currency Rates", onClick = { viewModel.refreshCurrencyRates() - app.toast(type = ToastType.SUCCESS, title = "Currency rates refreshed") + toaster.success(title = "Currency rates refreshed") } ) SettingsTextButtonRow( title = "Reset App Database", onClick = { viewModel.resetDatabase() - app.toast(type = ToastType.SUCCESS, title = "Database state reset") + toaster.success(title = "Database state reset") } ) SettingsTextButtonRow( title = "Reset Blocktank State", onClick = { viewModel.resetBlocktankState() - app.toast(type = ToastType.SUCCESS, title = "Blocktank state reset") + toaster.success(title = "Blocktank state reset") } ) SettingsTextButtonRow( title = "Reset Cache Store", onClick = { viewModel.resetCacheStore() - app.toast(type = ToastType.SUCCESS, title = "Cache store reset") + toaster.success(title = "Cache store reset") } ) SettingsTextButtonRow( title = "Wipe App", onClick = { viewModel.wipeWallet() - app.toast(type = ToastType.SUCCESS, title = "Wallet wiped") + toaster.success(title = "Wallet wiped") } ) @@ -145,14 +144,14 @@ fun DevSettingsScreen( onClick = { val count = 100 activity.generateRandomTestData(count) - app.toast(type = ToastType.SUCCESS, title = "Generated $count test activities") + toaster.success(title = "Generated $count test activities") } ) SettingsTextButtonRow( "Fake New BG Receive", onClick = { viewModel.fakeBgReceive() - app.toast(type = ToastType.INFO, title = "Restart app to see the payment received sheet") + toaster.info(title = "Restart app to see the payment received sheet") } ) SettingsTextButtonRow( diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 0114cb4cf..c8946335f 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import to.bitkit.R -import to.bitkit.models.ToastType import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton @@ -36,6 +35,7 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.transfer.components.TransferAnimationView import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.removeAccentTags import to.bitkit.ui.utils.withAccent import to.bitkit.ui.utils.withAccentBoldBright @@ -51,6 +51,7 @@ fun SavingsProgressScreen( onContinueClick: () -> Unit = {}, onTransferUnavailable: () -> Unit = {}, ) { + val toaster = toaster var progressState by remember { mutableStateOf(SavingsProgressState.PROGRESS) } // Effect to close channels & update UI @@ -68,8 +69,7 @@ fun SavingsProgressScreen( if (nonTrustedChannels.isEmpty()) { // All channels are trusted peers - show error and navigate back immediately - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.lightning__close_error, body = R.string.lightning__close_error_msg, ) @@ -79,8 +79,7 @@ fun SavingsProgressScreen( channels = nonTrustedChannels, onGiveUp = { app.showSheet(Sheet.ForceTransfer) }, onTransferUnavailable = { - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.lightning__close_error, body = R.string.lightning__close_error_msg, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt index 5d76b4d1f..bd9da7188 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt @@ -26,10 +26,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ext.mockOrder -import to.bitkit.models.ToastType import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight @@ -45,6 +43,7 @@ import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.TransferEffect @@ -63,7 +62,7 @@ fun SpendingAdvancedScreen( amountInputViewModel: AmountInputViewModel = hiltViewModel(), ) { val currentOnOrderCreated by rememberUpdatedState(onOrderCreated) - val app = appViewModel ?: return + val toaster = toaster val state by viewModel.spendingUiState.collectAsStateWithLifecycle() val order = state.order ?: return val amountUiState by amountInputViewModel.uiState.collectAsStateWithLifecycle() @@ -85,13 +84,12 @@ fun SpendingAdvancedScreen( TransferEffect.OnOrderCreated -> currentOnOrderCreated() is TransferEffect.ToastException -> { isLoading = false - app.toast(effect.e) + toaster.error(effect.e) } is TransferEffect.ToastError -> { isLoading = false - app.toast( - type = ToastType.ERROR, + toaster.error( title = effect.title, body = effect.body, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index e50ef2593..ba0ce7d48 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -58,9 +58,7 @@ import to.bitkit.ext.toActivityItemDate import to.bitkit.ext.toActivityItemTime import to.bitkit.ext.totalValue import to.bitkit.models.FeeRate.Companion.getFeeShortDescription -import to.bitkit.models.ToastType import to.bitkit.ui.Routes -import to.bitkit.ui.appViewModel import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.BodySSB @@ -81,6 +79,7 @@ import to.bitkit.ui.sheets.BoostTransactionSheet import to.bitkit.ui.sheets.ComingSoonSheet import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.copyToClipboard import to.bitkit.ui.utils.getScreenTitleRes import to.bitkit.viewmodels.ActivityDetailViewModel @@ -163,7 +162,7 @@ fun ActivityDetailScreen( is ActivityDetailViewModel.ActivityLoadState.Success -> { val item = loadState.activity - val app = appViewModel ?: return@Box + val toaster = toaster val copyToastTitle = stringResource(R.string.common__copied) val tags by detailViewModel.tags.collectAsStateWithLifecycle() @@ -226,8 +225,7 @@ fun ActivityDetailScreen( isCpfpChild = isCpfpChild, boostTxDoesExist = boostTxDoesExist, onCopy = { text -> - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = copyToastTitle, body = text.ellipsisMiddle(40) ) @@ -249,8 +247,7 @@ fun ActivityDetailScreen( onDismiss = detailViewModel::onDismissBoostSheet, item = it, onSuccess = { - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = R.string.wallet__boost_success_title, body = R.string.wallet__boost_success_msg, testTag = "BoostSuccessToast" @@ -259,8 +256,7 @@ fun ActivityDetailScreen( onCloseClick() }, onFailure = { - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.wallet__boost_error_title, body = R.string.wallet__boost_error_msg, testTag = "BoostFailureToast" @@ -268,15 +264,13 @@ fun ActivityDetailScreen( detailViewModel.onDismissBoostSheet() }, onMaxFee = { - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.wallet__send_fee_error, body = R.string.wallet__send_fee_error_max, ) }, onMinFee = { - app.toast( - type = ToastType.ERROR, + toaster.error( title = R.string.wallet__send_fee_error, body = R.string.wallet__send_fee_error_min, ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index 7de2fbf5a..362166753 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -45,9 +45,7 @@ import to.bitkit.ext.create import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.isSent import to.bitkit.ext.totalValue -import to.bitkit.models.ToastType import to.bitkit.ui.Routes -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.BodySSB import to.bitkit.ui.components.Caption13Up @@ -59,6 +57,7 @@ import to.bitkit.ui.screens.wallets.activity.components.ActivityIcon import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.copyToClipboard import to.bitkit.ui.utils.getBlockExplorerUrl import to.bitkit.ui.utils.getScreenTitleRes @@ -131,7 +130,7 @@ fun ActivityExploreScreen( is ActivityDetailViewModel.ActivityLoadState.Success -> { val item = loadState.activity - val app = appViewModel ?: return@ScreenColumn + val toaster = toaster val context = LocalContext.current val txDetails by detailViewModel.txDetails.collectAsStateWithLifecycle() @@ -166,8 +165,7 @@ fun ActivityExploreScreen( txDetails = txDetails, boostTxDoesExist = boostTxDoesExist, onCopy = { text -> - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = toastMessage, body = text.ellipsisMiddle(40), ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt index 15ecf20d8..d9ff266d7 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt @@ -31,7 +31,6 @@ import to.bitkit.R import to.bitkit.models.NodeLifecycleState import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies -import to.bitkit.ui.appViewModel import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.Caption13Up @@ -49,6 +48,7 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.walletViewModel import to.bitkit.utils.Logger import to.bitkit.viewmodels.AmountInputViewModel @@ -62,7 +62,7 @@ fun ReceiveAmountScreen( currencies: CurrencyState = LocalCurrencies.current, amountInputViewModel: AmountInputViewModel = hiltViewModel(), ) { - val app = appViewModel ?: return + val toaster = toaster val wallet = walletViewModel ?: return val blocktank = blocktankViewModel ?: return val lightningState by wallet.lightningState.collectAsStateWithLifecycle() @@ -106,7 +106,7 @@ fun ReceiveAmountScreen( ) ) }.onFailure { e -> - app.toast(e) + toaster.error(e) Logger.error("Failed to create CJIT", e) } isCreatingInvoice = false diff --git a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt index b0d3406d4..6f9ced42a 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt @@ -28,8 +28,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import kotlinx.coroutines.launch import to.bitkit.R -import to.bitkit.models.ToastType -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption import to.bitkit.ui.components.Caption13Up @@ -39,6 +37,7 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.walletViewModel import to.bitkit.utils.Logger @@ -50,7 +49,7 @@ fun BlocktankRegtestScreen( ) { val coroutineScope = rememberCoroutineScope() val wallet = walletViewModel ?: return - val app = appViewModel ?: return + val toaster = toaster val walletState by wallet.walletState.collectAsStateWithLifecycle() ScreenColumn { @@ -112,15 +111,13 @@ fun BlocktankRegtestScreen( val sats = depositAmount.toULongOrNull() ?: error("Invalid deposit amount: $depositAmount") val txId = viewModel.regtestDeposit(depositAddress, sats) Logger.debug("Deposit successful with txId: $txId") - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = "Success", body = "Deposit successful. TxID: $txId", ) }.onFailure { Logger.error("Deposit failed", it) - app.toast( - type = ToastType.ERROR, + toaster.error( title = "Failed to deposit", body = it.message.orEmpty(), ) @@ -158,15 +155,13 @@ fun BlocktankRegtestScreen( mineBlockCount.toUIntOrNull() ?: error("Invalid block count: $mineBlockCount") viewModel.regtestMine(count) Logger.debug("Successfully mined $count blocks") - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = "Success", body = "Successfully mined $count blocks", ) }.onFailure { Logger.error("Mining failed", it) - app.toast( - type = ToastType.ERROR, + toaster.error( title = "Failed to mine", body = it.message.orEmpty(), ) @@ -210,15 +205,13 @@ fun BlocktankRegtestScreen( val amount = if (paymentAmount.isEmpty()) null else paymentAmount.toULongOrNull() val paymentId = viewModel.regtestPay(paymentInvoice, amount) Logger.debug("Payment successful with ID: $paymentId") - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = "Success", body = "Payment successful. ID: $paymentId", ) }.onFailure { Logger.error("Payment failed", it) - app.toast( - type = ToastType.ERROR, + toaster.error( title = "Failed to pay invoice from LND", body = it.message.orEmpty(), ) @@ -277,14 +270,13 @@ fun BlocktankRegtestScreen( forceCloseAfterS = closeAfter, ) Logger.debug("Channel closed successfully with txId: $closingTxId") - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = "Success", body = "Channel closed. Closing TxID: $closingTxId" ) }.onFailure { Logger.error("Channel close failed", it) - app.toast(it) + toaster.error(it) } } }, diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index c864549e0..c18e72c28 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -25,9 +25,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R -import to.bitkit.models.ToastType import to.bitkit.ui.Routes -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.navigateToAboutSettings import to.bitkit.ui.navigateToAdvancedSettings @@ -41,6 +39,7 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface +import to.bitkit.ui.toaster private const val DEV_MODE_TAP_THRESHOLD = 5 @@ -48,7 +47,7 @@ private const val DEV_MODE_TAP_THRESHOLD = 5 fun SettingsScreen( navController: NavController, ) { - val app = appViewModel ?: return + val toaster = toaster val settings = settingsViewModel ?: return val isDevModeEnabled by settings.isDevModeEnabled.collectAsStateWithLifecycle() var enableDevModeTapCount by remember { mutableIntStateOf(0) } @@ -83,8 +82,7 @@ fun SettingsScreen( } else { R.string.settings__dev_disabled_message } - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = titleRes, body = bodyRes, ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index 55d8719a7..7cdb1a559 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -31,9 +31,7 @@ import to.bitkit.R import to.bitkit.ext.setClipboardText import to.bitkit.models.AddressModel import to.bitkit.models.ToastText -import to.bitkit.models.ToastType import to.bitkit.models.formatToModernDisplay -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption @@ -50,6 +48,7 @@ import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.BlockExplorerType import to.bitkit.ui.utils.getBlockExplorerUrl @@ -58,8 +57,8 @@ fun AddressViewerScreen( navController: NavController, viewModel: AddressViewerViewModel = hiltViewModel(), ) { - val app = appViewModel ?: return val context = LocalContext.current + val toaster = toaster val uiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -78,8 +77,7 @@ fun AddressViewerScreen( onGenerateMoreAddresses = viewModel::loadMoreAddresses, onCopy = { text -> context.setClipboardText(text) - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = ToastText(R.string.common__copied), body = ToastText(text), ) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index e188903ba..22fbf3422 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -32,8 +32,6 @@ import to.bitkit.R import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServerPeer import to.bitkit.models.ToastText -import to.bitkit.models.ToastType -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.FillHeight @@ -50,6 +48,7 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster @Composable fun ElectrumConfigScreen( @@ -58,8 +57,8 @@ fun ElectrumConfigScreen( viewModel: ElectrumConfigViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val app = appViewModel ?: return val context = LocalContext.current + val toaster = toaster // Handle result from Scanner LaunchedEffect(savedStateHandle) { @@ -75,8 +74,7 @@ fun ElectrumConfigScreen( LaunchedEffect(uiState.connectionResult) { uiState.connectionResult?.let { result -> if (result.isSuccess) { - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = ToastText(R.string.settings__es__server_updated_title), body = ToastText( context.getString(R.string.settings__es__server_updated_message) @@ -86,8 +84,7 @@ fun ElectrumConfigScreen( testTag = "ElectrumUpdatedToast", ) } else { - app.toast( - type = ToastType.WARNING, + toaster.warn( title = R.string.settings__es__server_error, body = R.string.settings__es__server_error_description, testTag = "ElectrumErrorToast", diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index b5eeccd4d..276f42340 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -26,8 +26,6 @@ import androidx.navigation.NavController import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.models.ToastText -import to.bitkit.models.ToastType -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.FillHeight @@ -42,6 +40,7 @@ import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster @Composable fun RgsServerScreen( @@ -50,7 +49,7 @@ fun RgsServerScreen( viewModel: RgsServerViewModel = hiltViewModel(), ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val app = appViewModel ?: return + val toaster = toaster // Handle result from Scanner LaunchedEffect(savedStateHandle) { @@ -66,15 +65,13 @@ fun RgsServerScreen( LaunchedEffect(uiState.connectionResult) { uiState.connectionResult?.let { result -> if (result.isSuccess) { - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = R.string.settings__rgs__update_success_title, body = R.string.settings__rgs__update_success_description, testTag = "RgsUpdatedToast", ) } else { - app.toast( - type = ToastType.ERROR, + toaster.error( title = ToastText(R.string.wallet__ldk_start_error_title), body = ToastText(result.exceptionOrNull()?.message ?: "Unknown error"), testTag = "RgsErrorToast", diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index afd3bc17d..75c3e540c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -56,9 +56,7 @@ import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.setClipboardText import to.bitkit.models.ToastText -import to.bitkit.models.ToastType import to.bitkit.ui.Routes -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB import to.bitkit.ui.components.ChannelStatusUi @@ -75,6 +73,7 @@ import to.bitkit.ui.settings.lightning.components.ChannelStatusView import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.ui.utils.getBlockExplorerUrl import to.bitkit.ui.walletViewModel import java.time.Instant @@ -88,7 +87,7 @@ fun ChannelDetailScreen( viewModel: LightningConnectionsViewModel, ) { val context = LocalContext.current - val app = appViewModel ?: return + val toaster = toaster val wallet = walletViewModel ?: return val selectedChannel by viewModel.selectedChannel.collectAsStateWithLifecycle() @@ -129,8 +128,7 @@ fun ChannelDetailScreen( }, onCopyText = { text -> context.setClipboardText(text) - app.toast( - type = ToastType.SUCCESS, + toaster.success( title = ToastText(R.string.common__copied), body = ToastText(text), ) From 9db7c89c6f0c3d77c91ea04e0ec28482edbc9ede Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 03:45:34 +0100 Subject: [PATCH 20/35] refactor: migrate internal AppViewModel toast calls Co-Authored-By: Claude Opus 4.5 --- .../java/to/bitkit/viewmodels/AppViewModel.kt | 141 ++++++++---------- 1 file changed, 63 insertions(+), 78 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 109635996..74236cc69 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -451,8 +451,7 @@ class AppViewModel @Inject constructor( migrationService.setShowingMigrationLoading(false) delay(MIGRATION_AUTH_RESET_DELAY_MS) resetIsAuthenticatedStateInternal() - toast( - type = ToastType.ERROR, + toaster.error( title = "Migration Warning", body = "Migration completed but node restart failed. Please restart the app." ) @@ -535,20 +534,18 @@ class AppViewModel @Inject constructor( activityRepo.insertActivityFromCjit(cjitEntry = cjitEntry, channel = channel) return } - toast( - type = ToastType.LIGHTNING, - title = context.getString(R.string.lightning__channel_opened_title), - body = context.getString(R.string.lightning__channel_opened_msg), + toaster.lightning( + title = R.string.lightning__channel_opened_title, + body = R.string.lightning__channel_opened_msg, testTag = "SpendingBalanceReadyToast", ) } private suspend fun notifyTransactionRemoved(event: Event.OnchainTransactionEvicted) { if (activityRepo.wasTransactionReplaced(event.txid)) return - toast( - type = ToastType.WARNING, - title = context.getString(R.string.wallet__toast_transaction_removed_title), - body = context.getString(R.string.wallet__toast_transaction_removed_description), + toaster.warn( + title = R.string.wallet__toast_transaction_removed_title, + body = R.string.wallet__toast_transaction_removed_description, testTag = "TransactionRemovedToast", ) } @@ -560,25 +557,27 @@ class AppViewModel @Inject constructor( showTransactionSheet(result.sheet) } - private fun notifyTransactionUnconfirmed() = toast( - type = ToastType.WARNING, - title = context.getString(R.string.wallet__toast_transaction_unconfirmed_title), - body = context.getString(R.string.wallet__toast_transaction_unconfirmed_description), + private fun notifyTransactionUnconfirmed() = toaster.warn( + title = R.string.wallet__toast_transaction_unconfirmed_title, + body = R.string.wallet__toast_transaction_unconfirmed_description, testTag = "TransactionUnconfirmedToast", ) private suspend fun notifyTransactionReplaced(event: Event.OnchainTransactionReplaced) { val isReceive = activityRepo.isReceivedTransaction(event.txid) - toast( - type = ToastType.INFO, - title = when (isReceive) { - true -> R.string.wallet__toast_received_transaction_replaced_title - else -> R.string.wallet__toast_transaction_replaced_title - }.let { context.getString(it) }, - body = when (isReceive) { - true -> R.string.wallet__toast_received_transaction_replaced_description - else -> R.string.wallet__toast_transaction_replaced_description - }.let { context.getString(it) }, + toaster.info( + title = context.getString( + when (isReceive) { + true -> R.string.wallet__toast_received_transaction_replaced_title + else -> R.string.wallet__toast_transaction_replaced_title + } + ), + body = context.getString( + when (isReceive) { + true -> R.string.wallet__toast_received_transaction_replaced_description + else -> R.string.wallet__toast_transaction_replaced_description + } + ), testTag = when (isReceive) { true -> "ReceivedTransactionReplacedToast" else -> "TransactionReplacedToast" @@ -586,10 +585,9 @@ class AppViewModel @Inject constructor( ) } - private fun notifyPaymentFailed() = toast( - type = ToastType.ERROR, - title = context.getString(R.string.wallet__toast_payment_failed_title), - body = context.getString(R.string.wallet__toast_payment_failed_description), + private fun notifyPaymentFailed() = toaster.error( + title = R.string.wallet__toast_payment_failed_title, + body = R.string.wallet__toast_payment_failed_description, testTag = "PaymentFailedToast", ) @@ -777,8 +775,7 @@ class AppViewModel @Inject constructor( if (lnurl is LnurlParams.LnurlPay) { val minSendable = lnurl.data.minSendableSat() if (_sendUiState.value.amount < minSendable) { - toast( - type = ToastType.ERROR, + toaster.error( title = context.getString(R.string.wallet__lnurl_pay__error_min__title), body = context.getString(R.string.wallet__lnurl_pay__error_min__description) .replace("{amount}", minSendable.toString()), @@ -832,10 +829,9 @@ class AppViewModel @Inject constructor( private fun onPasteClick() { val data = context.getClipboardText()?.trim() if (data.isNullOrBlank()) { - toast( - type = ToastType.WARNING, - title = context.getString(R.string.wallet__send_clipboard_empty_title), - body = context.getString(R.string.wallet__send_clipboard_empty_text), + toaster.warn( + title = R.string.wallet__send_clipboard_empty_title, + body = R.string.wallet__send_clipboard_empty_text, ) return } @@ -877,10 +873,9 @@ class AppViewModel @Inject constructor( is Scanner.Gift -> onScanGift(scan.code, scan.amount) else -> { Logger.warn("Unhandled scan data: $scan", context = TAG) - toast( - type = ToastType.WARNING, - title = context.getString(R.string.other__scan_err_decoding), - body = context.getString(R.string.other__scan_err_interpret_title), + toaster.warn( + title = R.string.other__scan_err_decoding, + body = R.string.other__scan_err_interpret_title, ) } } @@ -894,10 +889,9 @@ class AppViewModel @Inject constructor( ?.invoice ?.takeIf { invoice -> if (invoice.isExpired) { - toast( - type = ToastType.ERROR, - title = context.getString(R.string.other__scan_err_decoding), - body = context.getString(R.string.other__scan__error__expired), + toaster.error( + title = R.string.other__scan_err_decoding, + body = R.string.other__scan__error__expired, ) Logger.debug( @@ -963,10 +957,9 @@ class AppViewModel @Inject constructor( private suspend fun onScanLightning(invoice: LightningInvoice, scanResult: String) { if (invoice.isExpired) { - toast( - type = ToastType.ERROR, - title = context.getString(R.string.other__scan_err_decoding), - body = context.getString(R.string.other__scan__error__expired), + toaster.error( + title = R.string.other__scan_err_decoding, + body = R.string.other__scan__error__expired, ) return } @@ -975,10 +968,9 @@ class AppViewModel @Inject constructor( if (quickPayHandled) return if (!lightningRepo.canSend(invoice.amountSatoshis)) { - toast( - type = ToastType.ERROR, - title = context.getString(R.string.wallet__error_insufficient_funds_title), - body = context.getString(R.string.wallet__error_insufficient_funds_msg) + toaster.error( + title = R.string.wallet__error_insufficient_funds_title, + body = R.string.wallet__error_insufficient_funds_msg, ) return } @@ -1019,10 +1011,9 @@ class AppViewModel @Inject constructor( val maxSendable = data.maxSendableSat() if (!lightningRepo.canSend(minSendable)) { - toast( - type = ToastType.WARNING, - title = context.getString(R.string.other__lnurl_pay_error), - body = context.getString(R.string.other__lnurl_pay_error_no_capacity), + toaster.warn( + title = R.string.other__lnurl_pay_error, + body = R.string.other__lnurl_pay_error_no_capacity, ) return } @@ -1067,10 +1058,9 @@ class AppViewModel @Inject constructor( val maxWithdrawable = data.maxWithdrawableSat() if (minWithdrawable > maxWithdrawable) { - toast( - type = ToastType.WARNING, - title = context.getString(R.string.other__lnurl_withdr_error), - body = context.getString(R.string.other__lnurl_withdr_error_minmax) + toaster.warn( + title = R.string.other__lnurl_withdr_error, + body = R.string.other__lnurl_withdr_error_minmax, ) return } @@ -1116,15 +1106,13 @@ class AppViewModel @Inject constructor( k1 = k1, domain = domain, ).onFailure { - toast( - type = ToastType.WARNING, + toaster.warn( title = context.getString(R.string.other__lnurl_auth_error), body = context.getString(R.string.other__lnurl_auth_error_msg) .replace("{raw}", it.message?.takeIf { m -> m.isNotBlank() } ?: it.javaClass.simpleName), ) }.onSuccess { - toast( - type = ToastType.SUCCESS, + toaster.success( title = context.getString(R.string.other__lnurl_auth_success_title), body = when (domain.isNotBlank()) { true -> context.getString(R.string.other__lnurl_auth_success_msg_domain) @@ -1325,7 +1313,7 @@ class AppViewModel @Inject constructor( it.copy(decodedInvoice = invoice) } }.onFailure { - toast(Exception(context.getString(R.string.wallet__error_lnurl_invoice_fetch))) + toaster.error(Exception(context.getString(R.string.wallet__error_lnurl_invoice_fetch))) hideSheet() return } @@ -1338,7 +1326,7 @@ class AppViewModel @Inject constructor( val validatedAddress = runCatching { validateBitcoinAddress(address) } .getOrElse { e -> Logger.error("Invalid bitcoin send address: '$address'", e, context = TAG) - toast(Exception(context.getString(R.string.wallet__error_invalid_bitcoin_address))) + toaster.error(Exception(context.getString(R.string.wallet__error_invalid_bitcoin_address))) hideSheet() return } @@ -1360,10 +1348,9 @@ class AppViewModel @Inject constructor( activityRepo.syncActivities() }.onFailure { e -> Logger.error(msg = "Error sending onchain payment", e = e, context = TAG) - toast( - type = ToastType.ERROR, + toaster.error( title = context.getString(R.string.wallet__error_sending_title), - body = e.message ?: context.getString(R.string.common__error_body) + body = e.message ?: context.getString(R.string.common__error_body), ) hideSheet() } @@ -1411,7 +1398,7 @@ class AppViewModel @Inject constructor( preActivityMetadataRepo.deletePreActivityMetadata(createdMetadataPaymentId) } Logger.error("Error sending lightning payment", e, context = TAG) - toast(e) + toaster.error(e) hideSheet() } } @@ -1452,10 +1439,9 @@ class AppViewModel @Inject constructor( callback = lnurl.data.callback, paymentRequest = invoice ).onSuccess { - toast( - type = ToastType.SUCCESS, - title = context.getString(R.string.other__lnurl_withdr_success_title), - body = context.getString(R.string.other__lnurl_withdr_success_msg), + toaster.success( + title = R.string.other__lnurl_withdr_success_title, + body = R.string.other__lnurl_withdr_success_msg, ) hideSheet() _sendUiState.update { it.copy(isLoading = false) } @@ -1485,7 +1471,7 @@ class AppViewModel @Inject constructor( mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) }.onFailure { e -> Logger.error(msg = "Activity not found", context = TAG) - toast(e) + toaster.error(e) _transactionSheet.update { it.copy(isLoadingDetails = false) } } } @@ -1509,7 +1495,7 @@ class AppViewModel @Inject constructor( mainScreenEffect(MainScreenEffect.Navigate(nextRoute)) }.onFailure { e -> Logger.error(msg = "Activity not found", context = TAG) - toast(e) + toaster.error(e) _successSendUiState.update { it.copy(isLoadingDetails = false) } } } @@ -1894,10 +1880,9 @@ class AppViewModel @Inject constructor( keychain.upsertString(Keychain.Key.PIN_ATTEMPTS_REMAINING.name, newAttempts.toString()) if (newAttempts <= 0) { - toast( - type = ToastType.SUCCESS, - title = context.getString(R.string.security__wiped_title), - body = context.getString(R.string.security__wiped_message), + toaster.success( + title = R.string.security__wiped_title, + body = R.string.security__wiped_message, ) delay(250) // small delay for UI feedback mainScreenEffect(MainScreenEffect.WipeWallet) From 72513b95debcfa797f3f577a8faf89467ddb87ae Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 08:40:12 +0100 Subject: [PATCH 21/35] refactor: remove toast() methods from AppViewModel Co-Authored-By: Claude Opus 4.5 --- .../screens/wallets/send/SendAmountScreen.kt | 14 ++-- .../wallets/send/SendRecipientScreen.kt | 19 +++--- .../java/to/bitkit/viewmodels/AppViewModel.kt | 66 ------------------- 3 files changed, 13 insertions(+), 86 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index a857fac93..4c7d2de4b 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 @@ -32,12 +31,10 @@ import to.bitkit.ext.maxWithdrawableSat import to.bitkit.models.BalanceState import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.NodeLifecycleState -import to.bitkit.models.ToastType import to.bitkit.models.safe import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalBalances import to.bitkit.ui.LocalCurrencies -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.FillWidth @@ -57,6 +54,7 @@ import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.toaster import to.bitkit.viewmodels.AmountInputUiState import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.LnurlParams @@ -76,8 +74,7 @@ fun SendAmountScreen( currencies: CurrencyState = LocalCurrencies.current, amountInputViewModel: AmountInputViewModel = hiltViewModel(), ) { - val app = appViewModel - val context = LocalContext.current + val toaster = toaster val amountInputUiState: AmountInputUiState by amountInputViewModel.uiState.collectAsStateWithLifecycle() val currentOnEvent by rememberUpdatedState(onEvent) @@ -108,10 +105,9 @@ fun SendAmountScreen( }.takeIf { canGoBack }, onClickMax = { maxSats -> if (uiState.lnurl == null) { - app?.toast( - type = ToastType.INFO, - title = context.getString(R.string.wallet__send_max_spending__title), - body = context.getString(R.string.wallet__send_max_spending__description) + toaster.info( + title = R.string.wallet__send_max_spending__title, + body = R.string.wallet__send_max_spending__description, ) } amountInputViewModel.setSats(maxSats, currencies) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index dcec61ea4..eb33429c9 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -59,8 +59,6 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import to.bitkit.R import to.bitkit.ext.startActivityAppSettings -import to.bitkit.models.ToastType -import to.bitkit.ui.appViewModel import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BottomSheetPreview @@ -76,6 +74,7 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.Shapes import to.bitkit.ui.theme.TRANSITION_SCREEN_MS +import to.bitkit.ui.toaster import to.bitkit.ui.utils.withAccent import to.bitkit.utils.AppError import to.bitkit.utils.Logger @@ -91,7 +90,7 @@ fun SendRecipientScreen( onEvent: (SendEvent) -> Unit, modifier: Modifier = Modifier, ) { - val app = appViewModel + val toaster = toaster // Context & lifecycle val context = LocalContext.current @@ -139,10 +138,9 @@ fun SendRecipientScreen( } else { val error = requireNotNull(result.exceptionOrNull()) Logger.error("Scan failed", error, context = TAG) - app?.toast( - type = ToastType.ERROR, - title = context.getString(R.string.other__qr_error_header), - body = context.getString(R.string.other__qr_error_text), + toaster.error( + title = R.string.other__qr_error_header, + body = R.string.other__qr_error_text, ) } } @@ -173,11 +171,10 @@ fun SendRecipientScreen( isCameraInitialized = true }.onFailure { Logger.error("Camera initialization failed", it, context = TAG) - app?.toast( - type = ToastType.ERROR, + toaster.error( title = context.getString(R.string.other__qr_error_header), body = context.getString(R.string.other__camera_init_error) - .replace("{message}", it.message.orEmpty()) + .replace("{message}", it.message.orEmpty()), ) isCameraInitialized = false } @@ -206,7 +203,7 @@ fun SendRecipientScreen( onEvent(SendEvent.AddressContinue(qrCode)) } - val handleGalleryError: (Throwable) -> Unit = { app?.toast(it) } + val handleGalleryError: (Throwable) -> Unit = { toaster.error(it) } val galleryLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent(), diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 74236cc69..072927b8e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -83,8 +83,6 @@ import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.Suggestion import to.bitkit.models.Toast -import to.bitkit.models.ToastText -import to.bitkit.models.ToastType import to.bitkit.models.TransactionSpeed import to.bitkit.models.safe import to.bitkit.models.toActivityFilter @@ -120,7 +118,6 @@ import to.bitkit.utils.timedsheets.sheets.QuickPayTimedSheet import java.math.BigDecimal import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException -import kotlin.time.Duration import kotlin.time.ExperimentalTime @OptIn(ExperimentalTime::class) @@ -1781,69 +1778,6 @@ class AppViewModel @Inject constructor( private val toastQueue = toastQueueProvider(Dispatchers.Main.immediate) val currentToast: StateFlow = toastQueue.currentToast - fun toast( - type: ToastType, - title: ToastText, - body: ToastText? = null, - autoHide: Boolean = true, - duration: Duration = Toast.DURATION_DEFAULT, - testTag: String? = null, - ) { - toastQueue.enqueue( - Toast( - type = type, - title = title, - body = body, - autoHide = autoHide, - duration = duration, - testTag = testTag, - ) - ) - } - - fun toast( - type: ToastType, - @StringRes title: Int, - @StringRes body: Int? = null, - autoHide: Boolean = true, - duration: Duration = Toast.DURATION_DEFAULT, - testTag: String? = null, - ) = toast( - type = type, - title = ToastText(title), - body = body?.let { ToastText(it) }, - autoHide = autoHide, - duration = duration, - testTag = testTag, - ) - - fun toast( - type: ToastType, - title: String, - body: String? = null, - autoHide: Boolean = true, - duration: Duration = Toast.DURATION_DEFAULT, - testTag: String? = null, - ) = toast( - type = type, - title = ToastText(title), - body = body?.let { ToastText(it) }, - autoHide = autoHide, - duration = duration, - testTag = testTag, - ) - - fun toast(error: Throwable) { - toast( - type = ToastType.ERROR, - title = ToastText(R.string.common__error), - body = error.message?.let { ToastText(it) } - ?: ToastText(R.string.common__error_body) - ) - } - - fun toast(toast: Toast) = toastQueue.enqueue(toast) - fun hideToast() = toastQueue.dismissCurrentToast() fun pauseToast() = toastQueue.pauseCurrentToast() From f1797479b99200c41886ad3f3ee1a17ee2667528 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 13:43:44 +0100 Subject: [PATCH 22/35] refactor: add ToastText.Parameterized Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/ToastText.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/app/src/main/java/to/bitkit/models/ToastText.kt b/app/src/main/java/to/bitkit/models/ToastText.kt index fb36e8a26..9d15c5e1c 100644 --- a/app/src/main/java/to/bitkit/models/ToastText.kt +++ b/app/src/main/java/to/bitkit/models/ToastText.kt @@ -11,22 +11,34 @@ sealed interface ToastText { @JvmInline value class Resource(@StringRes val resId: Int) : ToastText + data class Parameterized( + @StringRes val resId: Int, + val params: Map, + ) : 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): 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 } From ac36cd31de87702cbadfa23e14fbe4e4d1cfb588 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 13:57:47 +0100 Subject: [PATCH 23/35] refactor: consolidate Toaster to single ToastText API Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/Toast.kt | 2 +- .../java/to/bitkit/repositories/BackupRepo.kt | 8 +- .../to/bitkit/repositories/CurrencyRepo.kt | 5 +- app/src/main/java/to/bitkit/ui/ContentView.kt | 5 +- .../bitkit/ui/components/IsOnlineTracker.kt | 9 +- .../recovery/RecoveryMnemonicViewModel.kt | 3 +- .../ui/screens/recovery/RecoveryViewModel.kt | 13 +- .../ui/screens/scanner/QrScanningScreen.kt | 9 +- .../ui/screens/settings/DevSettingsScreen.kt | 23 +-- .../screens/transfer/SavingsProgressScreen.kt | 9 +- .../transfer/SpendingAdvancedScreen.kt | 5 +- .../external/ExternalNodeViewModel.kt | 10 +- .../external/LnurlChannelViewModel.kt | 4 +- .../wallets/activity/ActivityDetailScreen.kt | 21 +- .../wallets/activity/ActivityExploreScreen.kt | 5 +- .../screens/wallets/send/SendAmountScreen.kt | 5 +- .../send/SendCoinSelectionViewModel.kt | 7 +- .../screens/wallets/send/SendFeeViewModel.kt | 9 +- .../wallets/send/SendRecipientScreen.kt | 13 +- .../ui/settings/BlocktankRegtestScreen.kt | 29 +-- .../to/bitkit/ui/settings/SettingsScreen.kt | 5 +- .../settings/advanced/ElectrumConfigScreen.kt | 4 +- .../ui/settings/advanced/RgsServerScreen.kt | 4 +- .../backups/BackupNavSheetViewModel.kt | 9 +- .../LightningConnectionsViewModel.kt | 13 +- .../java/to/bitkit/ui/shared/toast/Toaster.kt | 184 ++---------------- .../java/to/bitkit/viewmodels/AppViewModel.kt | 91 +++++---- .../bitkit/viewmodels/DevSettingsViewModel.kt | 15 +- .../to/bitkit/viewmodels/LdkDebugViewModel.kt | 37 ++-- .../to/bitkit/viewmodels/TransferViewModel.kt | 12 +- .../to/bitkit/viewmodels/WalletViewModel.kt | 8 +- 31 files changed, 229 insertions(+), 347 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/Toast.kt b/app/src/main/java/to/bitkit/models/Toast.kt index debb58267..3d7ee25c1 100644 --- a/app/src/main/java/to/bitkit/models/Toast.kt +++ b/app/src/main/java/to/bitkit/models/Toast.kt @@ -9,7 +9,7 @@ data class Toast( val type: ToastType, val title: ToastText, val body: ToastText? = null, - val autoHide: Boolean, + val autoHide: Boolean = true, val duration: Duration = DURATION_DEFAULT, val testTag: String? = null, ) { diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index cb1476ecf..40f158354 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -42,6 +42,7 @@ import to.bitkit.models.BackupItemStatus import to.bitkit.models.BlocktankBackupV1 import to.bitkit.models.MetadataBackupV1 import to.bitkit.models.SettingsBackupV1 +import to.bitkit.models.ToastText import to.bitkit.models.WalletBackupV1 import to.bitkit.models.WidgetsBackupV1 import to.bitkit.services.LightningService @@ -374,9 +375,10 @@ class BackupRepo @Inject constructor( scope.launch { toaster.error( - title = context.getString(R.string.settings__backup__failed_title), - body = context.getString(R.string.settings__backup__failed_message).formatPlural( - mapOf("interval" to (BACKUP_CHECK_INTERVAL / MINUTE_IN_MS)) + 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()) ), ) } diff --git a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt index 68db7f2c2..0db66abdb 100644 --- a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt @@ -29,6 +29,7 @@ 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.ToastText import to.bitkit.models.asBtc import to.bitkit.models.formatCurrency import to.bitkit.services.CurrencyService @@ -93,8 +94,8 @@ class CurrencyRepo @Inject constructor( .collect { isStale -> if (isStale) { toaster.error( - title = "Rates currently unavailable", - body = "An error has occurred. Please try again later." + title = ToastText("Rates currently unavailable"), + body = ToastText("An error has occurred. Please try again later.") ) } } diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index 43566bd51..fd364b18e 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -46,6 +46,7 @@ import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import to.bitkit.env.Env import to.bitkit.models.NodeLifecycleState +import to.bitkit.models.ToastText import to.bitkit.models.WidgetType import to.bitkit.ui.Routes.ExternalConnection import to.bitkit.ui.components.AuthCheckScreen @@ -628,8 +629,8 @@ private fun RootNavHost( toastException = { appViewModel.toaster.error(it) }, toast = { title, body -> appViewModel.toaster.error( - title = title, - body = body + title = ToastText(title), + body = ToastText(body) ) }, ) diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index 37a65fcb8..aa167fa70 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R +import to.bitkit.models.ToastText import to.bitkit.repositories.ConnectivityState import to.bitkit.ui.toaster import to.bitkit.viewmodels.AppViewModel @@ -30,15 +31,15 @@ fun IsOnlineTracker( when (connectivityState) { ConnectivityState.CONNECTED -> { toaster.success( - title = R.string.other__connection_back_title, - body = R.string.other__connection_back_msg, + title = ToastText(R.string.other__connection_back_title), + body = ToastText(R.string.other__connection_back_msg), ) } ConnectivityState.DISCONNECTED -> { toaster.warn( - title = R.string.other__connection_issue, - body = R.string.other__connection_issue_explain, + title = ToastText(R.string.other__connection_issue), + body = ToastText(R.string.other__connection_issue_explain), ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt index 537ddff7b..d8f9a2ccb 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryMnemonicViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.data.keychain.Keychain +import to.bitkit.models.ToastText import to.bitkit.ui.shared.toast.Toaster import to.bitkit.utils.Logger import javax.inject.Inject @@ -42,7 +43,7 @@ class RecoveryMnemonicViewModel @Inject constructor( isLoading = false, ) } - toaster.error(title = R.string.security__mnemonic_load_error) + toaster.error(title = ToastText(R.string.security__mnemonic_load_error)) return@launch } diff --git a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt index df55fd3fc..57d03994d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/recovery/RecoveryViewModel.kt @@ -17,6 +17,7 @@ import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.data.SettingsStore import to.bitkit.env.Env +import to.bitkit.models.ToastText import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.LogsRepo import to.bitkit.repositories.WalletRepo @@ -74,8 +75,8 @@ class RecoveryViewModel @Inject constructor( ) } toaster.error( - title = R.string.common__error, - body = R.string.other__logs_export_error, + title = ToastText(R.string.common__error), + body = ToastText(R.string.other__logs_export_error), ) } ) @@ -98,8 +99,8 @@ class RecoveryViewModel @Inject constructor( Logger.error("Failed to open support links", fallbackError, context = TAG) viewModelScope.launch { toaster.error( - title = R.string.common__error, - body = R.string.settings__support__link_error, + title = ToastText(R.string.common__error), + body = ToastText(R.string.settings__support__link_error), ) } } @@ -120,8 +121,8 @@ class RecoveryViewModel @Inject constructor( toaster.error(error) }.onSuccess { toaster.success( - title = R.string.security__wiped_title, - body = R.string.security__wiped_message, + title = ToastText(R.string.security__wiped_title), + body = ToastText(R.string.security__wiped_message), ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index 4cda47e6d..a06972b4c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -65,6 +65,7 @@ import to.bitkit.R import to.bitkit.env.Env import to.bitkit.ext.getClipboardText import to.bitkit.ext.startActivityAppSettings +import to.bitkit.models.ToastText import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TextInput @@ -149,8 +150,8 @@ fun QrScanningScreen( val error = requireNotNull(result.exceptionOrNull()) Logger.error("Failed to scan QR code", error) toaster.error( - title = R.string.other__qr_error_header, - body = R.string.other__qr_error_text, + title = ToastText(R.string.other__qr_error_header), + body = ToastText(R.string.other__qr_error_text), ) } } @@ -254,8 +255,8 @@ private fun handlePaste( val clipboard = context.getClipboardText()?.trim() if (clipboard.isNullOrBlank()) { toaster.warn( - title = R.string.wallet__send_clipboard_empty_title, - body = R.string.wallet__send_clipboard_empty_text, + title = ToastText(R.string.wallet__send_clipboard_empty_title), + body = ToastText(R.string.wallet__send_clipboard_empty_text), ) } setScanResult(clipboard) diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt index eb600dc5b..523e99b1c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt @@ -14,6 +14,7 @@ import androidx.navigation.NavController import org.lightningdevkit.ldknode.Network import to.bitkit.R import to.bitkit.env.Env +import to.bitkit.models.ToastText import to.bitkit.ui.Routes import to.bitkit.ui.activityListViewModel import to.bitkit.ui.components.settings.SectionHeader @@ -77,63 +78,63 @@ fun DevSettingsScreen( title = "Reset Settings State", onClick = { settings.reset() - toaster.success(title = "Settings state reset") + toaster.success(title = ToastText("Settings state reset")) } ) SettingsTextButtonRow( title = "Reset All Activities", onClick = { activity.removeAllActivities() - toaster.success(title = "Activities removed") + toaster.success(title = ToastText("Activities removed")) } ) SettingsTextButtonRow( title = "Reset Backup State", onClick = { viewModel.resetBackupState() - toaster.success(title = "Backup state reset") + toaster.success(title = ToastText("Backup state reset")) } ) SettingsTextButtonRow( title = "Reset Widgets State", onClick = { viewModel.resetWidgetsState() - toaster.success(title = "Widgets state reset") + toaster.success(title = ToastText("Widgets state reset")) } ) SettingsTextButtonRow( title = "Refresh Currency Rates", onClick = { viewModel.refreshCurrencyRates() - toaster.success(title = "Currency rates refreshed") + toaster.success(title = ToastText("Currency rates refreshed")) } ) SettingsTextButtonRow( title = "Reset App Database", onClick = { viewModel.resetDatabase() - toaster.success(title = "Database state reset") + toaster.success(title = ToastText("Database state reset")) } ) SettingsTextButtonRow( title = "Reset Blocktank State", onClick = { viewModel.resetBlocktankState() - toaster.success(title = "Blocktank state reset") + toaster.success(title = ToastText("Blocktank state reset")) } ) SettingsTextButtonRow( title = "Reset Cache Store", onClick = { viewModel.resetCacheStore() - toaster.success(title = "Cache store reset") + toaster.success(title = ToastText("Cache store reset")) } ) SettingsTextButtonRow( title = "Wipe App", onClick = { viewModel.wipeWallet() - toaster.success(title = "Wallet wiped") + toaster.success(title = ToastText("Wallet wiped")) } ) @@ -144,14 +145,14 @@ fun DevSettingsScreen( onClick = { val count = 100 activity.generateRandomTestData(count) - toaster.success(title = "Generated $count test activities") + toaster.success(title = ToastText("Generated $count test activities")) } ) SettingsTextButtonRow( "Fake New BG Receive", onClick = { viewModel.fakeBgReceive() - toaster.info(title = "Restart app to see the payment received sheet") + toaster.info(title = ToastText("Restart app to see the payment received sheet")) } ) SettingsTextButtonRow( diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index c8946335f..959a43791 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import to.bitkit.R +import to.bitkit.models.ToastText import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton @@ -70,8 +71,8 @@ fun SavingsProgressScreen( if (nonTrustedChannels.isEmpty()) { // All channels are trusted peers - show error and navigate back immediately toaster.error( - title = R.string.lightning__close_error, - body = R.string.lightning__close_error_msg, + title = ToastText(R.string.lightning__close_error), + body = ToastText(R.string.lightning__close_error_msg), ) onTransferUnavailable() } else { @@ -80,8 +81,8 @@ fun SavingsProgressScreen( onGiveUp = { app.showSheet(Sheet.ForceTransfer) }, onTransferUnavailable = { toaster.error( - title = R.string.lightning__close_error, - body = R.string.lightning__close_error_msg, + title = ToastText(R.string.lightning__close_error), + body = ToastText(R.string.lightning__close_error_msg), ) onTransferUnavailable() }, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt index bd9da7188..be7fd0d94 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt @@ -26,6 +26,7 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ext.mockOrder +import to.bitkit.models.ToastText import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.components.Caption13Up @@ -90,8 +91,8 @@ fun SpendingAdvancedScreen( is TransferEffect.ToastError -> { isLoading = false toaster.error( - title = effect.title, - body = effect.body, + title = ToastText(effect.title), + body = ToastText(effect.body), ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index 1df49579a..7ffb28f3d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -75,8 +75,8 @@ class ExternalNodeViewModel @Inject constructor( setEffect(SideEffect.ConnectionSuccess) } else { toaster.error( - title = R.string.lightning__error_add_title, - body = R.string.lightning__error_add, + title = ToastText(R.string.lightning__error_add_title), + body = ToastText(R.string.lightning__error_add), ) } } @@ -89,7 +89,7 @@ class ExternalNodeViewModel @Inject constructor( if (result.isSuccess) { _uiState.update { it.copy(peer = result.getOrNull()) } } else { - toaster.error(title = R.string.lightning__error_add_uri) + toaster.error(title = ToastText(R.string.lightning__error_add_uri)) } } } @@ -136,8 +136,8 @@ class ExternalNodeViewModel @Inject constructor( val feeRate = _uiState.value.customFeeRate ?: 0u if (feeRate == 0u) { toaster.info( - title = R.string.wallet__min_possible_fee_rate, - body = R.string.wallet__min_possible_fee_rate_msg, + title = ToastText(R.string.wallet__min_possible_fee_rate), + body = ToastText(R.string.wallet__min_possible_fee_rate_msg), ) return false } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt index ccb7ef4ad..16f543eb3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/LnurlChannelViewModel.kt @@ -70,8 +70,8 @@ class LnurlChannelViewModel @Inject constructor( lightningRepo.requestLnurlChannel(callback = params.callback, k1 = params.k1, nodeId = nodeId) .onSuccess { toaster.success( - title = R.string.other__lnurl_channel_success_title, - body = R.string.other__lnurl_channel_success_msg_no_peer, + title = ToastText(R.string.other__lnurl_channel_success_title), + body = ToastText(R.string.other__lnurl_channel_success_msg_no_peer), ) _uiState.update { it.copy(isConnected = true) } }.onFailure { error -> diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index ba0ce7d48..44178e916 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -50,6 +50,7 @@ import com.synonym.bitkitcore.PaymentType import to.bitkit.R import to.bitkit.ext.create import to.bitkit.ext.ellipsisMiddle +import to.bitkit.models.ToastText import to.bitkit.ext.isSent import to.bitkit.ext.isTransfer import to.bitkit.ext.rawId @@ -226,8 +227,8 @@ fun ActivityDetailScreen( boostTxDoesExist = boostTxDoesExist, onCopy = { text -> toaster.success( - title = copyToastTitle, - body = text.ellipsisMiddle(40) + title = ToastText(copyToastTitle), + body = ToastText(text.ellipsisMiddle(40)) ) }, feeRates = feeRates, @@ -248,8 +249,8 @@ fun ActivityDetailScreen( item = it, onSuccess = { toaster.success( - title = R.string.wallet__boost_success_title, - body = R.string.wallet__boost_success_msg, + title = ToastText(R.string.wallet__boost_success_title), + body = ToastText(R.string.wallet__boost_success_msg), testTag = "BoostSuccessToast" ) listViewModel.resync() @@ -257,22 +258,22 @@ fun ActivityDetailScreen( }, onFailure = { toaster.error( - title = R.string.wallet__boost_error_title, - body = R.string.wallet__boost_error_msg, + title = ToastText(R.string.wallet__boost_error_title), + body = ToastText(R.string.wallet__boost_error_msg), testTag = "BoostFailureToast" ) detailViewModel.onDismissBoostSheet() }, onMaxFee = { toaster.error( - title = R.string.wallet__send_fee_error, - body = R.string.wallet__send_fee_error_max, + title = ToastText(R.string.wallet__send_fee_error), + body = ToastText(R.string.wallet__send_fee_error_max), ) }, onMinFee = { toaster.error( - title = R.string.wallet__send_fee_error, - body = R.string.wallet__send_fee_error_min, + title = ToastText(R.string.wallet__send_fee_error), + body = ToastText(R.string.wallet__send_fee_error_min), ) } ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index 362166753..b3a4c3418 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -42,6 +42,7 @@ import com.synonym.bitkitcore.PaymentType import com.synonym.bitkitcore.TransactionDetails import to.bitkit.R import to.bitkit.ext.create +import to.bitkit.models.ToastText import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.isSent import to.bitkit.ext.totalValue @@ -166,8 +167,8 @@ fun ActivityExploreScreen( boostTxDoesExist = boostTxDoesExist, onCopy = { text -> toaster.success( - title = toastMessage, - body = text.ellipsisMiddle(40), + title = ToastText(toastMessage), + body = ToastText(text.ellipsisMiddle(40)), ) }, onClickExplore = { txid -> diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index 4c7d2de4b..c18a26847 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -31,6 +31,7 @@ import to.bitkit.ext.maxWithdrawableSat import to.bitkit.models.BalanceState import to.bitkit.models.BitcoinDisplayUnit import to.bitkit.models.NodeLifecycleState +import to.bitkit.models.ToastText import to.bitkit.models.safe import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalBalances @@ -106,8 +107,8 @@ fun SendAmountScreen( onClickMax = { maxSats -> if (uiState.lnurl == null) { toaster.info( - title = R.string.wallet__send_max_spending__title, - body = R.string.wallet__send_max_spending__description, + title = ToastText(R.string.wallet__send_max_spending__title), + body = ToastText(R.string.wallet__send_max_spending__description), ) } amountInputViewModel.setSats(maxSats, currencies) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt index 3f5c1bb07..40c9bb7c5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt @@ -16,6 +16,7 @@ import org.lightningdevkit.ldknode.SpendableUtxo import to.bitkit.R import to.bitkit.di.BgDispatcher import to.bitkit.env.Defaults +import to.bitkit.models.ToastText import to.bitkit.ext.rawId import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.LightningRepo @@ -73,8 +74,10 @@ class SendCoinSelectionViewModel @Inject constructor( }.onFailure { Logger.error("Failed to load UTXOs for coin selection", it, context = TAG) toaster.error( - context.getString(R.string.wallet__error_utxo_load) - .replace("{raw}", it.message.orEmpty()) + title = ToastText( + R.string.wallet__error_utxo_load, + mapOf("raw" to it.message.orEmpty()) + ) ) } } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt index 105bafe93..193b3202c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendFeeViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.ext.getSatsPerVByteFor import to.bitkit.models.FeeRate +import to.bitkit.models.ToastText import to.bitkit.models.TransactionSpeed import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.LightningRepo @@ -106,16 +107,16 @@ class SendFeeViewModel @Inject constructor( val minSatsPerVByte = sendUiState.feeRates?.slow ?: 1u if (satsPerVByte < minSatsPerVByte) { toaster.info( - title = R.string.wallet__min_possible_fee_rate, - body = R.string.wallet__min_possible_fee_rate_msg, + title = ToastText(R.string.wallet__min_possible_fee_rate), + body = ToastText(R.string.wallet__min_possible_fee_rate_msg), ) return false } if (satsPerVByte > maxSatsPerVByte) { toaster.info( - title = R.string.wallet__max_possible_fee_rate, - body = R.string.wallet__max_possible_fee_rate_msg, + title = ToastText(R.string.wallet__max_possible_fee_rate), + body = ToastText(R.string.wallet__max_possible_fee_rate_msg), ) return false } diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index eb33429c9..b443f1d82 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -58,6 +58,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import to.bitkit.R +import to.bitkit.models.ToastText import to.bitkit.ext.startActivityAppSettings import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB @@ -139,8 +140,8 @@ fun SendRecipientScreen( val error = requireNotNull(result.exceptionOrNull()) Logger.error("Scan failed", error, context = TAG) toaster.error( - title = R.string.other__qr_error_header, - body = R.string.other__qr_error_text, + title = ToastText(R.string.other__qr_error_header), + body = ToastText(R.string.other__qr_error_text), ) } } @@ -172,9 +173,11 @@ fun SendRecipientScreen( }.onFailure { Logger.error("Camera initialization failed", it, context = TAG) toaster.error( - title = context.getString(R.string.other__qr_error_header), - body = context.getString(R.string.other__camera_init_error) - .replace("{message}", it.message.orEmpty()), + title = ToastText(R.string.other__qr_error_header), + body = ToastText( + R.string.other__camera_init_error, + mapOf("message" to it.message.orEmpty()) + ), ) isCameraInitialized = false } diff --git a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt index 6f9ced42a..721f0f325 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt @@ -28,6 +28,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import kotlinx.coroutines.launch import to.bitkit.R +import to.bitkit.models.ToastText import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption import to.bitkit.ui.components.Caption13Up @@ -112,14 +113,14 @@ fun BlocktankRegtestScreen( val txId = viewModel.regtestDeposit(depositAddress, sats) Logger.debug("Deposit successful with txId: $txId") toaster.success( - title = "Success", - body = "Deposit successful. TxID: $txId", + title = ToastText("Success"), + body = ToastText("Deposit successful. TxID: $txId"), ) }.onFailure { Logger.error("Deposit failed", it) toaster.error( - title = "Failed to deposit", - body = it.message.orEmpty(), + title = ToastText("Failed to deposit"), + body = ToastText(it.message.orEmpty()), ) } @@ -156,14 +157,14 @@ fun BlocktankRegtestScreen( viewModel.regtestMine(count) Logger.debug("Successfully mined $count blocks") toaster.success( - title = "Success", - body = "Successfully mined $count blocks", + title = ToastText("Success"), + body = ToastText("Successfully mined $count blocks"), ) }.onFailure { Logger.error("Mining failed", it) toaster.error( - title = "Failed to mine", - body = it.message.orEmpty(), + title = ToastText("Failed to mine"), + body = ToastText(it.message.orEmpty()), ) } isMining = false @@ -206,14 +207,14 @@ fun BlocktankRegtestScreen( val paymentId = viewModel.regtestPay(paymentInvoice, amount) Logger.debug("Payment successful with ID: $paymentId") toaster.success( - title = "Success", - body = "Payment successful. ID: $paymentId", + title = ToastText("Success"), + body = ToastText("Payment successful. ID: $paymentId"), ) }.onFailure { Logger.error("Payment failed", it) toaster.error( - title = "Failed to pay invoice from LND", - body = it.message.orEmpty(), + title = ToastText("Failed to pay invoice from LND"), + body = ToastText(it.message.orEmpty()), ) } } @@ -271,8 +272,8 @@ fun BlocktankRegtestScreen( ) Logger.debug("Channel closed successfully with txId: $closingTxId") toaster.success( - title = "Success", - body = "Channel closed. Closing TxID: $closingTxId" + title = ToastText("Success"), + body = ToastText("Channel closed. Closing TxID: $closingTxId") ) }.onFailure { Logger.error("Channel close failed", it) diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index c18e72c28..e68f4b7cc 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R +import to.bitkit.models.ToastText import to.bitkit.ui.Routes import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.navigateToAboutSettings @@ -83,8 +84,8 @@ fun SettingsScreen( R.string.settings__dev_disabled_message } toaster.success( - title = titleRes, - body = bodyRes, + title = ToastText(titleRes), + body = ToastText(bodyRes), ) enableDevModeTapCount = 0 } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 22fbf3422..2d8a6de80 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -85,8 +85,8 @@ fun ElectrumConfigScreen( ) } else { toaster.warn( - title = R.string.settings__es__server_error, - body = R.string.settings__es__server_error_description, + title = ToastText(R.string.settings__es__server_error), + body = ToastText(R.string.settings__es__server_error_description), testTag = "ElectrumErrorToast", ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 276f42340..27511b6f9 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -66,8 +66,8 @@ fun RgsServerScreen( uiState.connectionResult?.let { result -> if (result.isSuccess) { toaster.success( - title = R.string.settings__rgs__update_success_title, - body = R.string.settings__rgs__update_success_description, + title = ToastText(R.string.settings__rgs__update_success_title), + body = ToastText(R.string.settings__rgs__update_success_description), testTag = "RgsUpdatedToast", ) } else { diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index a5e6b80ff..090adc56b 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.data.CacheStore +import to.bitkit.models.ToastText import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain import to.bitkit.models.BackupCategory @@ -87,8 +88,8 @@ class BackupNavSheetViewModel @Inject constructor( }.onFailure { Logger.error("Error loading mnemonic", it, context = TAG) toaster.warn( - title = R.string.security__mnemonic_error, - body = R.string.security__mnemonic_error_description, + title = ToastText(R.string.security__mnemonic_error), + body = ToastText(R.string.security__mnemonic_error_description), ) } } @@ -157,8 +158,8 @@ class BackupNavSheetViewModel @Inject constructor( fun onMnemonicCopied() { viewModelScope.launch { toaster.success( - title = R.string.common__copied, - body = R.string.security__mnemonic_copied, + title = ToastText(R.string.common__copied), + body = ToastText(R.string.security__mnemonic_copied), ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index dd5e4c670..7814e2eb1 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -26,6 +26,7 @@ import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.OutPoint import to.bitkit.R import to.bitkit.di.BgDispatcher +import to.bitkit.models.ToastText import to.bitkit.ext.amountOnClose import to.bitkit.ext.calculateRemoteBalance import to.bitkit.ext.createChannelDetails @@ -340,8 +341,8 @@ class LightningConnectionsViewModel @Inject constructor( .onSuccess { uri -> onReady(uri) } .onFailure { toaster.warn( - title = R.string.lightning__error_logs, - body = R.string.lightning__error_logs_description, + title = ToastText(R.string.lightning__error_logs), + body = ToastText(R.string.lightning__error_logs_description), ) } } @@ -454,8 +455,8 @@ class LightningConnectionsViewModel @Inject constructor( walletRepo.syncNodeAndWallet() toaster.success( - title = R.string.lightning__close_success_title, - body = R.string.lightning__close_success_msg, + title = ToastText(R.string.lightning__close_success_title), + body = ToastText(R.string.lightning__close_success_msg), ) _closeConnectionUiState.update { @@ -469,8 +470,8 @@ class LightningConnectionsViewModel @Inject constructor( Logger.error("Failed to close channel", e = error, context = TAG) toaster.warn( - title = R.string.lightning__close_error, - body = R.string.lightning__close_error_msg, + title = ToastText(R.string.lightning__close_error), + body = ToastText(R.string.lightning__close_error_msg), ) _closeConnectionUiState.update { it.copy(isLoading = false) } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt index f41feccb6..1790cb98f 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/Toaster.kt @@ -1,6 +1,5 @@ package to.bitkit.ui.shared.toast -import androidx.annotation.StringRes import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow @@ -10,185 +9,34 @@ import to.bitkit.models.ToastText import to.bitkit.models.ToastType import javax.inject.Inject import javax.inject.Singleton -import kotlin.time.Duration -@Suppress("TooManyFunctions") @Singleton class Toaster @Inject constructor() { private val _events = MutableSharedFlow(extraBufferCapacity = 1) val events: SharedFlow = _events.asSharedFlow() - @Suppress("LongParameterList") - private fun emit( - type: ToastType, - title: ToastText, - body: ToastText? = null, - autoHide: Boolean = true, - duration: Duration = Toast.DURATION_DEFAULT, - testTag: String? = null, - ) { - _events.tryEmit( - Toast( - type = type, - title = title, - body = body, - autoHide = autoHide, - duration = duration, - testTag = testTag, - ) - ) - } - - // region @StringRes overloads - fun success( - @StringRes title: Int, - @StringRes body: Int? = null, - testTag: String? = null, - ) = emit( - ToastType.SUCCESS, - ToastText(title), - body?.let { ToastText(it) }, - testTag = testTag - ) - - fun info( - @StringRes title: Int, - @StringRes body: Int? = null, - testTag: String? = null, - ) = emit( - ToastType.INFO, - ToastText(title), - body?.let { ToastText(it) }, - testTag = testTag, - ) - - fun lightning( - @StringRes title: Int, - @StringRes body: Int? = null, - testTag: String? = null, - ) = emit( - ToastType.LIGHTNING, - ToastText(title), - body?.let { ToastText(it) }, - testTag = testTag - ) - - fun warn( - @StringRes title: Int, - @StringRes body: Int? = null, - testTag: String? = null, - ) = emit( - ToastType.WARNING, - ToastText(title), - body?.let { ToastText(it) }, - testTag = testTag - ) - - fun error( - @StringRes title: Int, - @StringRes body: Int? = null, - testTag: String? = null, - ) = emit( - ToastType.ERROR, - ToastText(title), - body?.let { ToastText(it) }, - testTag = testTag, - ) - // endregion + private fun emit(toast: Toast) = _events.tryEmit(toast) - // region ToastText overloads - fun success( - title: ToastText, - body: ToastText? = null, - testTag: String? = null, - ) = emit(ToastType.SUCCESS, title, body, testTag = testTag) + fun success(title: ToastText, body: ToastText? = null, testTag: String? = null) = + emit(Toast(ToastType.SUCCESS, title, body, testTag = testTag)) - fun info( - title: ToastText, - body: ToastText? = null, - testTag: String? = null, - ) = emit(ToastType.INFO, title, body, testTag = testTag) + fun info(title: ToastText, body: ToastText? = null, testTag: String? = null) = + emit(Toast(ToastType.INFO, title, body, testTag = testTag)) - fun lightning( - title: ToastText, - body: ToastText? = null, - testTag: String? = null, - ) = emit(ToastType.LIGHTNING, title, body, testTag = testTag) + fun lightning(title: ToastText, body: ToastText? = null, testTag: String? = null) = + emit(Toast(ToastType.LIGHTNING, title, body, testTag = testTag)) - fun warn( - title: ToastText, - body: ToastText? = null, - testTag: String? = null, - ) = emit(ToastType.WARNING, title, body, testTag = testTag) + fun warn(title: ToastText, body: ToastText? = null, testTag: String? = null) = + emit(Toast(ToastType.WARNING, title, body, testTag = testTag)) - fun error( - title: ToastText, - body: ToastText? = null, - testTag: String? = null, - ) = emit(ToastType.ERROR, title, body, testTag = testTag) - // endregion - - // region String literal overloads - fun success( - title: String, - body: String? = null, - testTag: String? = null, - ) = emit( - ToastType.SUCCESS, - ToastText(title), - body?.let { ToastText(it) }, - testTag = testTag, - ) - - fun info( - title: String, - body: String? = null, - testTag: String? = null, - ) = emit( - ToastType.INFO, - ToastText(title), - body?.let { ToastText(it) }, - testTag = testTag, - ) - - fun lightning( - title: String, - body: String? = null, - testTag: String? = null, - ) = emit( - ToastType.LIGHTNING, - ToastText(title), - body?.let { ToastText(it) }, - testTag = testTag, - ) - - fun warn( - title: String, - body: String? = null, - testTag: String? = null, - ) = emit( - ToastType.WARNING, - ToastText(title), - body?.let { ToastText(it) }, - testTag = testTag, - ) - - fun error( - title: String, - body: String? = null, - testTag: String? = null, - ) = emit( - ToastType.ERROR, - ToastText(title), - body?.let { ToastText(it) }, - testTag = testTag, - ) + fun error(title: ToastText, body: ToastText? = null, testTag: String? = null) = + emit(Toast(ToastType.ERROR, title, body, testTag = testTag)) fun error(throwable: Throwable) = emit( - type = ToastType.ERROR, - title = ToastText(R.string.common__error), - body = throwable.message?.let { ToastText(it) } - ?: ToastText(R.string.common__error_body), + Toast( + type = ToastType.ERROR, + title = ToastText(R.string.common__error), + body = throwable.message?.let { ToastText(it) } ?: ToastText(R.string.common__error_body), + ) ) - // endregion } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 072927b8e..7eb0cf7b8 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -83,6 +83,7 @@ import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType import to.bitkit.models.Suggestion import to.bitkit.models.Toast +import to.bitkit.models.ToastText import to.bitkit.models.TransactionSpeed import to.bitkit.models.safe import to.bitkit.models.toActivityFilter @@ -449,8 +450,8 @@ class AppViewModel @Inject constructor( delay(MIGRATION_AUTH_RESET_DELAY_MS) resetIsAuthenticatedStateInternal() toaster.error( - title = "Migration Warning", - body = "Migration completed but node restart failed. Please restart the app." + title = ToastText("Migration Warning"), + body = ToastText("Migration completed but node restart failed. Please restart the app.") ) } @@ -532,8 +533,8 @@ class AppViewModel @Inject constructor( return } toaster.lightning( - title = R.string.lightning__channel_opened_title, - body = R.string.lightning__channel_opened_msg, + title = ToastText(R.string.lightning__channel_opened_title), + body = ToastText(R.string.lightning__channel_opened_msg), testTag = "SpendingBalanceReadyToast", ) } @@ -541,8 +542,8 @@ class AppViewModel @Inject constructor( private suspend fun notifyTransactionRemoved(event: Event.OnchainTransactionEvicted) { if (activityRepo.wasTransactionReplaced(event.txid)) return toaster.warn( - title = R.string.wallet__toast_transaction_removed_title, - body = R.string.wallet__toast_transaction_removed_description, + title = ToastText(R.string.wallet__toast_transaction_removed_title), + body = ToastText(R.string.wallet__toast_transaction_removed_description), testTag = "TransactionRemovedToast", ) } @@ -555,21 +556,21 @@ class AppViewModel @Inject constructor( } private fun notifyTransactionUnconfirmed() = toaster.warn( - title = R.string.wallet__toast_transaction_unconfirmed_title, - body = R.string.wallet__toast_transaction_unconfirmed_description, + title = ToastText(R.string.wallet__toast_transaction_unconfirmed_title), + body = ToastText(R.string.wallet__toast_transaction_unconfirmed_description), testTag = "TransactionUnconfirmedToast", ) private suspend fun notifyTransactionReplaced(event: Event.OnchainTransactionReplaced) { val isReceive = activityRepo.isReceivedTransaction(event.txid) toaster.info( - title = context.getString( + title = ToastText( when (isReceive) { true -> R.string.wallet__toast_received_transaction_replaced_title else -> R.string.wallet__toast_transaction_replaced_title } ), - body = context.getString( + body = ToastText( when (isReceive) { true -> R.string.wallet__toast_received_transaction_replaced_description else -> R.string.wallet__toast_transaction_replaced_description @@ -583,8 +584,8 @@ class AppViewModel @Inject constructor( } private fun notifyPaymentFailed() = toaster.error( - title = R.string.wallet__toast_payment_failed_title, - body = R.string.wallet__toast_payment_failed_description, + title = ToastText(R.string.wallet__toast_payment_failed_title), + body = ToastText(R.string.wallet__toast_payment_failed_description), testTag = "PaymentFailedToast", ) @@ -773,9 +774,11 @@ class AppViewModel @Inject constructor( val minSendable = lnurl.data.minSendableSat() if (_sendUiState.value.amount < minSendable) { toaster.error( - title = context.getString(R.string.wallet__lnurl_pay__error_min__title), - body = context.getString(R.string.wallet__lnurl_pay__error_min__description) - .replace("{amount}", minSendable.toString()), + title = ToastText(R.string.wallet__lnurl_pay__error_min__title), + body = ToastText( + R.string.wallet__lnurl_pay__error_min__description, + mapOf("amount" to minSendable.toString()) + ), testTag = "LnurlPayAmountTooLowToast", ) return @@ -827,8 +830,8 @@ class AppViewModel @Inject constructor( val data = context.getClipboardText()?.trim() if (data.isNullOrBlank()) { toaster.warn( - title = R.string.wallet__send_clipboard_empty_title, - body = R.string.wallet__send_clipboard_empty_text, + title = ToastText(R.string.wallet__send_clipboard_empty_title), + body = ToastText(R.string.wallet__send_clipboard_empty_text), ) return } @@ -871,8 +874,8 @@ class AppViewModel @Inject constructor( else -> { Logger.warn("Unhandled scan data: $scan", context = TAG) toaster.warn( - title = R.string.other__scan_err_decoding, - body = R.string.other__scan_err_interpret_title, + title = ToastText(R.string.other__scan_err_decoding), + body = ToastText(R.string.other__scan_err_interpret_title), ) } } @@ -887,8 +890,8 @@ class AppViewModel @Inject constructor( ?.takeIf { invoice -> if (invoice.isExpired) { toaster.error( - title = R.string.other__scan_err_decoding, - body = R.string.other__scan__error__expired, + title = ToastText(R.string.other__scan_err_decoding), + body = ToastText(R.string.other__scan__error__expired), ) Logger.debug( @@ -955,8 +958,8 @@ class AppViewModel @Inject constructor( private suspend fun onScanLightning(invoice: LightningInvoice, scanResult: String) { if (invoice.isExpired) { toaster.error( - title = R.string.other__scan_err_decoding, - body = R.string.other__scan__error__expired, + title = ToastText(R.string.other__scan_err_decoding), + body = ToastText(R.string.other__scan__error__expired), ) return } @@ -966,8 +969,8 @@ class AppViewModel @Inject constructor( if (!lightningRepo.canSend(invoice.amountSatoshis)) { toaster.error( - title = R.string.wallet__error_insufficient_funds_title, - body = R.string.wallet__error_insufficient_funds_msg, + title = ToastText(R.string.wallet__error_insufficient_funds_title), + body = ToastText(R.string.wallet__error_insufficient_funds_msg), ) return } @@ -1009,8 +1012,8 @@ class AppViewModel @Inject constructor( if (!lightningRepo.canSend(minSendable)) { toaster.warn( - title = R.string.other__lnurl_pay_error, - body = R.string.other__lnurl_pay_error_no_capacity, + title = ToastText(R.string.other__lnurl_pay_error), + body = ToastText(R.string.other__lnurl_pay_error_no_capacity), ) return } @@ -1056,8 +1059,8 @@ class AppViewModel @Inject constructor( if (minWithdrawable > maxWithdrawable) { toaster.warn( - title = R.string.other__lnurl_withdr_error, - body = R.string.other__lnurl_withdr_error_minmax, + title = ToastText(R.string.other__lnurl_withdr_error), + body = ToastText(R.string.other__lnurl_withdr_error_minmax), ) return } @@ -1104,18 +1107,22 @@ class AppViewModel @Inject constructor( domain = domain, ).onFailure { toaster.warn( - title = context.getString(R.string.other__lnurl_auth_error), - body = context.getString(R.string.other__lnurl_auth_error_msg) - .replace("{raw}", it.message?.takeIf { m -> m.isNotBlank() } ?: it.javaClass.simpleName), + title = ToastText(R.string.other__lnurl_auth_error), + body = ToastText( + R.string.other__lnurl_auth_error_msg, + mapOf("raw" to (it.message?.takeIf { m -> m.isNotBlank() } ?: it.javaClass.simpleName)) + ), ) }.onSuccess { toaster.success( - title = context.getString(R.string.other__lnurl_auth_success_title), + title = ToastText(R.string.other__lnurl_auth_success_title), body = when (domain.isNotBlank()) { - true -> context.getString(R.string.other__lnurl_auth_success_msg_domain) - .replace("{domain}", domain) + true -> ToastText( + R.string.other__lnurl_auth_success_msg_domain, + mapOf("domain" to domain) + ) - else -> context.getString(R.string.other__lnurl_auth_success_msg_no_domain) + else -> ToastText(R.string.other__lnurl_auth_success_msg_no_domain) }, ) } @@ -1346,8 +1353,8 @@ class AppViewModel @Inject constructor( }.onFailure { e -> Logger.error(msg = "Error sending onchain payment", e = e, context = TAG) toaster.error( - title = context.getString(R.string.wallet__error_sending_title), - body = e.message ?: context.getString(R.string.common__error_body), + title = ToastText(R.string.wallet__error_sending_title), + body = ToastText(e.message ?: context.getString(R.string.common__error_body)), ) hideSheet() } @@ -1437,8 +1444,8 @@ class AppViewModel @Inject constructor( paymentRequest = invoice ).onSuccess { toaster.success( - title = R.string.other__lnurl_withdr_success_title, - body = R.string.other__lnurl_withdr_success_msg, + title = ToastText(R.string.other__lnurl_withdr_success_title), + body = ToastText(R.string.other__lnurl_withdr_success_msg), ) hideSheet() _sendUiState.update { it.copy(isLoading = false) } @@ -1815,8 +1822,8 @@ class AppViewModel @Inject constructor( if (newAttempts <= 0) { toaster.success( - title = R.string.security__wiped_title, - body = R.string.security__wiped_message, + title = ToastText(R.string.security__wiped_title), + body = ToastText(R.string.security__wiped_message), ) delay(250) // small delay for UI feedback mainScreenEffect(MainScreenEffect.WipeWallet) diff --git a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt index 3e8733923..5c5fa87a2 100644 --- a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await import to.bitkit.R import to.bitkit.data.AppDb +import to.bitkit.models.ToastText import to.bitkit.data.CacheStore import to.bitkit.data.WidgetsStore import to.bitkit.env.Env @@ -47,18 +48,18 @@ class DevSettingsViewModel @Inject constructor( val peer = lightningRepo.getPeers()?.firstOrNull() if (peer == null) { - toaster.warn("No peer connected") + toaster.warn(title = ToastText("No peer connected")) return@launch } lightningRepo.openChannel(peer, 50_000u, 25_000u) - .onSuccess { toaster.info("Channel pending") } + .onSuccess { toaster.info(title = ToastText("Channel pending")) } .onFailure { toaster.error(it) } } fun registerForNotifications() = viewModelScope.launch { lightningRepo.registerForNotifications() - .onSuccess { toaster.info("Registered for notifications") } + .onSuccess { toaster.info(title = ToastText("Registered for notifications")) } .onFailure { toaster.error(it) } } @@ -70,9 +71,9 @@ class DevSettingsViewModel @Inject constructor( notificationType = "incomingHtlc", customUrl = Env.blocktankNotificationApiUrl, ) - toaster.info("LSP notification sent to this device") + toaster.info(title = ToastText("LSP notification sent to this device")) }.onFailure { - toaster.warn("Error testing LSP notification") + toaster.warn(title = ToastText("Error testing LSP notification")) } } @@ -100,8 +101,8 @@ class DevSettingsViewModel @Inject constructor( .onSuccess { uri -> onReady(uri) } .onFailure { toaster.warn( - context.getString(R.string.lightning__error_logs), - context.getString(R.string.lightning__error_logs_description), + title = ToastText(R.string.lightning__error_logs), + body = ToastText(R.string.lightning__error_logs_description), ) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt index 3121e8577..e9ccb3967 100644 --- a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.data.backup.VssBackupClient import to.bitkit.di.BgDispatcher +import to.bitkit.models.ToastText import to.bitkit.ext.of import to.bitkit.repositories.LightningRepo import to.bitkit.services.NetworkGraphInfo @@ -43,7 +44,7 @@ class LdkDebugViewModel @Inject constructor( fun addPeer() { val uri = _uiState.value.nodeUri.trim() if (uri.isEmpty()) { - viewModelScope.launch { toaster.warn("Please enter a node URI") } + viewModelScope.launch { toaster.warn(title = ToastText("Please enter a node URI")) } return } connectPeer(uri) @@ -55,7 +56,7 @@ class LdkDebugViewModel @Inject constructor( val pastedUri = clipData?.getItemAt(0)?.text?.toString()?.trim() if (pastedUri.isNullOrEmpty()) { - viewModelScope.launch { toaster.warn("Clipboard is empty") } + viewModelScope.launch { toaster.warn(title = ToastText("Clipboard is empty")) } return } @@ -71,15 +72,15 @@ class LdkDebugViewModel @Inject constructor( lightningRepo.connectPeer(peer) }.onSuccess { result -> result.onSuccess { - toaster.info("Peer connected") + toaster.info(title = ToastText("Peer connected")) _uiState.update { it.copy(nodeUri = "") } }.onFailure { e -> Logger.error("Failed to connect peer", e, context = TAG) - toaster.error("Failed to connect peer", e.message) + toaster.error(title = ToastText("Failed to connect peer"), body = e.message?.let { ToastText(it) }) } }.onFailure { e -> Logger.error("Failed to parse peer URI", e, context = TAG) - toaster.error("Invalid node URI format", e.message) + toaster.error(title = ToastText("Invalid node URI format"), body = e.message?.let { ToastText(it) }) } _uiState.update { it.copy(isLoading = false) } } @@ -97,9 +98,9 @@ class LdkDebugViewModel @Inject constructor( context = TAG ) _uiState.update { it.copy(networkGraphInfo = info) } - toaster.info("Network graph info logged") + toaster.info(title = ToastText("Network graph info logged")) } else { - toaster.warn("Failed to get network graph info") + toaster.warn(title = ToastText("Failed to get network graph info")) } } } @@ -110,11 +111,11 @@ class LdkDebugViewModel @Inject constructor( val outputDir = context.cacheDir.absolutePath lightningRepo.exportNetworkGraphToFile(outputDir).onSuccess { file -> Logger.info("Network graph exported to: ${file.absolutePath}", context = TAG) - toaster.info("Network graph exported") + toaster.info(title = ToastText("Network graph exported")) onFileReady(file) }.onFailure { e -> Logger.error("Failed to export network graph", e, context = TAG) - toaster.error("Failed to export network graph", e.message) + toaster.error(title = ToastText("Failed to export network graph"), body = e.message?.let { ToastText(it) }) } _uiState.update { it.copy(isLoading = false) } } @@ -126,10 +127,10 @@ class LdkDebugViewModel @Inject constructor( vssBackupClient.listKeys().onSuccess { keys -> Logger.info("VSS keys: ${keys.size}", context = TAG) _uiState.update { it.copy(vssKeys = keys) } - toaster.info("Found ${keys.size} VSS key(s)") + toaster.info(title = ToastText("Found ${keys.size} VSS key(s)")) }.onFailure { e -> Logger.error("Failed to list VSS keys", e, context = TAG) - toaster.error("Failed to list VSS keys", e.message) + toaster.error(title = ToastText("Failed to list VSS keys"), body = e.message?.let { ToastText(it) }) } _uiState.update { it.copy(isLoading = false) } } @@ -141,10 +142,10 @@ class LdkDebugViewModel @Inject constructor( vssBackupClient.deleteAllKeys().onSuccess { deletedCount -> Logger.info("Deleted $deletedCount VSS keys", context = TAG) _uiState.update { it.copy(vssKeys = emptyList()) } - toaster.info("Deleted $deletedCount VSS key(s)") + toaster.info(title = ToastText("Deleted $deletedCount VSS key(s)")) }.onFailure { e -> Logger.error("Failed to delete VSS keys", e, context = TAG) - toaster.error("Failed to delete VSS keys", e.message) + toaster.error(title = ToastText("Failed to delete VSS keys"), body = e.message?.let { ToastText(it) }) } _uiState.update { it.copy(isLoading = false) } } @@ -160,14 +161,14 @@ class LdkDebugViewModel @Inject constructor( _uiState.update { state -> state.copy(vssKeys = state.vssKeys.filter { it.key != key }) } - toaster.info("Deleted key: $key") + toaster.info(title = ToastText("Deleted key: $key")) } else { - toaster.warn("Key not found: $key") + toaster.warn(title = ToastText("Key not found: $key")) } } .onFailure { e -> Logger.error("Failed to delete VSS key: $key", e, context = TAG) - toaster.error("Failed to delete key", e.message) + toaster.error(title = ToastText("Failed to delete key"), body = e.message?.let { ToastText(it) }) } _uiState.update { it.copy(isLoading = false) } } @@ -179,11 +180,11 @@ class LdkDebugViewModel @Inject constructor( lightningRepo.restartNode() .onSuccess { Logger.info("Node restarted successfully", context = TAG) - toaster.info("Node restarted") + toaster.info(title = ToastText("Node restarted")) } .onFailure { e -> Logger.error("Failed to restart node", e, context = TAG) - toaster.error("Failed to restart node", e.message) + toaster.error(title = ToastText("Failed to restart node"), body = e.message?.let { ToastText(it) }) } _uiState.update { it.copy(isLoading = false) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index 996260567..b96d6d372 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -460,8 +460,8 @@ class TransferViewModel @Inject constructor( channelsToClose = emptyList() Logger.error("Cannot force close channels with trusted peer", context = TAG) toaster.error( - title = R.string.lightning__force_failed_title, - body = R.string.lightning__force_failed_msg, + title = ToastText(R.string.lightning__force_failed_title), + body = ToastText(R.string.lightning__force_failed_msg), ) return@runCatching } @@ -490,15 +490,15 @@ class TransferViewModel @Inject constructor( } else { Logger.error("Force close failed for ${failedChannels.size} channels", context = TAG) toaster.error( - title = R.string.lightning__force_failed_title, - body = R.string.lightning__force_failed_msg, + title = ToastText(R.string.lightning__force_failed_title), + body = ToastText(R.string.lightning__force_failed_msg), ) } }.onFailure { Logger.error("Force close failed", e = it, context = TAG) toaster.error( - title = R.string.lightning__force_failed_title, - body = R.string.lightning__force_failed_msg, + title = ToastText(R.string.lightning__force_failed_title), + body = ToastText(R.string.lightning__force_failed_msg), ) } _isForceTransferLoading.value = false diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index 76dbcb3cf..b854d5ff0 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -128,8 +128,8 @@ class WalletViewModel @Inject constructor( migrationService.markMigrationChecked() migrationService.setShowingMigrationLoading(false) toaster.error( - title = R.string.wallet__migration_error_title, - body = R.string.wallet__migration_error_body, + title = ToastText(R.string.wallet__migration_error_title), + body = ToastText(R.string.wallet__migration_error_body), ) } } @@ -302,8 +302,8 @@ class WalletViewModel @Inject constructor( lightningRepo.disconnectPeer(peer) .onSuccess { toaster.info( - title = R.string.common__success, - body = R.string.wallet__peer_disconnected, + title = ToastText(R.string.common__success), + body = ToastText(R.string.wallet__peer_disconnected), ) } .onFailure { From b6a4ad27e6c0225877355411d02b7c3b657725b0 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 14:01:21 +0100 Subject: [PATCH 24/35] refactor: restore ToastQueue to use CoroutineScope Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/di/ViewModelModule.kt | 7 ------- app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt | 7 +++---- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 4 +--- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt index a7d6da319..0e7e8481d 100644 --- a/app/src/main/java/to/bitkit/di/ViewModelModule.kt +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -5,8 +5,6 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import kotlinx.coroutines.CoroutineDispatcher -import to.bitkit.ui.shared.toast.ToastQueue import javax.inject.Singleton @Module @@ -17,9 +15,4 @@ object ViewModelModule { fun provideFirebaseMessaging(): FirebaseMessaging { return FirebaseMessaging.getInstance() } - - @Provides - fun provideToastQueueProvider(): (CoroutineDispatcher) -> ToastQueue { - return ::ToastQueue - } } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt index 499721a52..673a413ed 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt @@ -1,6 +1,6 @@ package to.bitkit.ui.shared.toast -import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow @@ -8,7 +8,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import to.bitkit.async.BaseCoroutineScope import to.bitkit.models.Toast import kotlin.time.Duration @@ -28,7 +27,7 @@ private const val MAX_QUEUE_SIZE = 5 * - Auto-advance to next toast on completion * - Max queue size with FIFO overflow handling */ -class ToastQueue(dispatcher: CoroutineDispatcher) : BaseCoroutineScope(dispatcher) { +class ToastQueue(private val scope: CoroutineScope) { // Public state exposed to UI private val _currentToast = MutableStateFlow(null) val currentToast: StateFlow = _currentToast.asStateFlow() @@ -116,7 +115,7 @@ class ToastQueue(dispatcher: CoroutineDispatcher) : BaseCoroutineScope(dispatche private fun startTimer(duration: Duration) { cancelTimer() - timerJob = launch { + timerJob = scope.launch { delay(duration) if (!isPaused) { _currentToast.value = null diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 7eb0cf7b8..60b46405b 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -28,7 +28,6 @@ import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -127,7 +126,6 @@ import kotlin.time.ExperimentalTime class AppViewModel @Inject constructor( connectivityRepo: ConnectivityRepo, healthRepo: HealthRepo, - toastQueueProvider: @JvmSuppressWildcards (CoroutineDispatcher) -> ToastQueue, timedSheetManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> TimedSheetManager, val toaster: Toaster, @ApplicationContext private val context: Context, @@ -1782,7 +1780,7 @@ class AppViewModel @Inject constructor( // endregion // region Toasts - private val toastQueue = toastQueueProvider(Dispatchers.Main.immediate) + private val toastQueue = ToastQueue(viewModelScope) val currentToast: StateFlow = toastQueue.currentToast fun hideToast() = toastQueue.dismissCurrentToast() From 029e401dd816ecb3490ad79d11330584c887a116 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 14:11:13 +0100 Subject: [PATCH 25/35] refactor: use composable parameter pattern for toaster Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/models/ToastText.kt | 5 ++++- app/src/main/java/to/bitkit/ui/Locals.kt | 3 --- app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt | 3 ++- .../main/java/to/bitkit/ui/components/IsOnlineTracker.kt | 5 +++-- .../java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt | 5 ++--- .../to/bitkit/ui/screens/settings/DevSettingsScreen.kt | 5 +++-- .../bitkit/ui/screens/transfer/SavingsProgressScreen.kt | 5 +++-- .../bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt | 5 +++-- .../ui/screens/wallets/activity/ActivityDetailScreen.kt | 6 +++--- .../ui/screens/wallets/activity/ActivityExploreScreen.kt | 6 +++--- .../ui/screens/wallets/receive/ReceiveAmountScreen.kt | 5 +++-- .../to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt | 5 +++-- .../bitkit/ui/screens/wallets/send/SendRecipientScreen.kt | 8 ++++---- .../java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt | 5 +++-- app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt | 5 +++-- .../to/bitkit/ui/settings/advanced/AddressViewerScreen.kt | 5 +++-- .../bitkit/ui/settings/advanced/ElectrumConfigScreen.kt | 5 +++-- .../to/bitkit/ui/settings/advanced/RgsServerScreen.kt | 5 +++-- .../bitkit/ui/settings/lightning/ChannelDetailScreen.kt | 5 +++-- 19 files changed, 54 insertions(+), 42 deletions(-) diff --git a/app/src/main/java/to/bitkit/models/ToastText.kt b/app/src/main/java/to/bitkit/models/ToastText.kt index 9d15c5e1c..74abb429b 100644 --- a/app/src/main/java/to/bitkit/models/ToastText.kt +++ b/app/src/main/java/to/bitkit/models/ToastText.kt @@ -22,7 +22,10 @@ sealed interface 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): ToastText = Parameterized(resId, params) + operator fun invoke( + @StringRes resId: Int, + params: Map, + ): ToastText = Parameterized(resId, params) } } diff --git a/app/src/main/java/to/bitkit/ui/Locals.kt b/app/src/main/java/to/bitkit/ui/Locals.kt index e381b78b1..b8a2575e8 100644 --- a/app/src/main/java/to/bitkit/ui/Locals.kt +++ b/app/src/main/java/to/bitkit/ui/Locals.kt @@ -58,6 +58,3 @@ val backupsViewModel: BackupsViewModel? val drawerState: DrawerState? @Composable get() = LocalDrawerState.current - -val toaster: Toaster - @Composable get() = LocalToaster.current diff --git a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt index 67eafc6c4..046eaf14a 100644 --- a/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt +++ b/app/src/main/java/to/bitkit/ui/NodeInfoScreen.kt @@ -63,6 +63,7 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.shared.modifiers.clickableAlpha +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.copyToClipboard @@ -73,9 +74,9 @@ import kotlin.time.ExperimentalTime @Composable fun NodeInfoScreen( navController: NavController, + toaster: Toaster = LocalToaster.current, ) { val wallet = walletViewModel ?: return - val toaster = toaster val settings = settingsViewModel ?: return val isRefreshing by wallet.isRefreshing.collectAsStateWithLifecycle() diff --git a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt index aa167fa70..c84420288 100644 --- a/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt +++ b/app/src/main/java/to/bitkit/ui/components/IsOnlineTracker.kt @@ -9,14 +9,15 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.models.ToastText import to.bitkit.repositories.ConnectivityState -import to.bitkit.ui.toaster +import to.bitkit.ui.LocalToaster +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.viewmodels.AppViewModel @Composable fun IsOnlineTracker( app: AppViewModel, + toaster: Toaster = LocalToaster.current, ) { - val toaster = toaster ?: return val connectivityState by app.isOnline.collectAsStateWithLifecycle(initialValue = ConnectivityState.CONNECTED) val (isFirstEmission, setIsFirstEmission) = remember { mutableStateOf(true) } diff --git a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt index a06972b4c..bb729822a 100644 --- a/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/scanner/QrScanningScreen.kt @@ -66,6 +66,7 @@ import to.bitkit.env.Env import to.bitkit.ext.getClipboardText import to.bitkit.ext.startActivityAppSettings import to.bitkit.models.ToastText +import to.bitkit.ui.LocalToaster import to.bitkit.ui.components.PrimaryButton import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.TextInput @@ -76,7 +77,6 @@ import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster import to.bitkit.utils.Logger import java.util.concurrent.Executors @@ -91,10 +91,9 @@ fun QrScanningScreen( navController: NavController, inSheet: Boolean = false, onBack: () -> Unit = { navController.popBackStack() }, + toaster: Toaster = LocalToaster.current, onScanSuccess: (String) -> Unit, ) { - val toaster = toaster - val (scanResult, setScanResult) = remember { mutableStateOf(null) } // Handle scan result diff --git a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt index 523e99b1c..841311104 100644 --- a/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/settings/DevSettingsScreen.kt @@ -15,6 +15,7 @@ import org.lightningdevkit.ldknode.Network import to.bitkit.R import to.bitkit.env.Env import to.bitkit.models.ToastText +import to.bitkit.ui.LocalToaster import to.bitkit.ui.Routes import to.bitkit.ui.activityListViewModel import to.bitkit.ui.components.settings.SectionHeader @@ -24,16 +25,16 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.settingsViewModel +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.shared.util.shareZipFile -import to.bitkit.ui.toaster import to.bitkit.viewmodels.DevSettingsViewModel @Composable fun DevSettingsScreen( navController: NavController, viewModel: DevSettingsViewModel = hiltViewModel(), + toaster: Toaster = LocalToaster.current, ) { - val toaster = toaster val activity = activityListViewModel ?: return val settings = settingsViewModel ?: return val context = LocalContext.current diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt index 959a43791..4504ac690 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SavingsProgressScreen.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp import kotlinx.coroutines.delay import to.bitkit.R import to.bitkit.models.ToastText +import to.bitkit.ui.LocalToaster import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Display import to.bitkit.ui.components.PrimaryButton @@ -34,9 +35,9 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.transfer.components.TransferAnimationView +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster import to.bitkit.ui.utils.removeAccentTags import to.bitkit.ui.utils.withAccent import to.bitkit.ui.utils.withAccentBoldBright @@ -51,8 +52,8 @@ fun SavingsProgressScreen( wallet: WalletViewModel, onContinueClick: () -> Unit = {}, onTransferUnavailable: () -> Unit = {}, + toaster: Toaster = LocalToaster.current, ) { - val toaster = toaster var progressState by remember { mutableStateOf(SavingsProgressState.PROGRESS) } // Effect to close channels & update UI diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt index be7fd0d94..3b1bbd1a3 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt @@ -29,6 +29,7 @@ import to.bitkit.ext.mockOrder import to.bitkit.models.ToastText import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies +import to.bitkit.ui.LocalToaster import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight @@ -42,9 +43,9 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.TransferEffect @@ -61,9 +62,9 @@ fun SpendingAdvancedScreen( onOrderCreated: () -> Unit = {}, currencies: CurrencyState = LocalCurrencies.current, amountInputViewModel: AmountInputViewModel = hiltViewModel(), + toaster: Toaster = LocalToaster.current, ) { val currentOnOrderCreated by rememberUpdatedState(onOrderCreated) - val toaster = toaster val state by viewModel.spendingUiState.collectAsStateWithLifecycle() val order = state.order ?: return val amountUiState by amountInputViewModel.uiState.collectAsStateWithLifecycle() diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt index 44178e916..511c2bbbe 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityDetailScreen.kt @@ -50,7 +50,6 @@ import com.synonym.bitkitcore.PaymentType import to.bitkit.R import to.bitkit.ext.create import to.bitkit.ext.ellipsisMiddle -import to.bitkit.models.ToastText import to.bitkit.ext.isSent import to.bitkit.ext.isTransfer import to.bitkit.ext.rawId @@ -59,6 +58,8 @@ import to.bitkit.ext.toActivityItemDate import to.bitkit.ext.toActivityItemTime import to.bitkit.ext.totalValue import to.bitkit.models.FeeRate.Companion.getFeeShortDescription +import to.bitkit.models.ToastText +import to.bitkit.ui.LocalToaster import to.bitkit.ui.Routes import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.BalanceHeaderView @@ -80,7 +81,6 @@ import to.bitkit.ui.sheets.BoostTransactionSheet import to.bitkit.ui.sheets.ComingSoonSheet import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster import to.bitkit.ui.utils.copyToClipboard import to.bitkit.ui.utils.getScreenTitleRes import to.bitkit.viewmodels.ActivityDetailViewModel @@ -163,7 +163,7 @@ fun ActivityDetailScreen( is ActivityDetailViewModel.ActivityLoadState.Success -> { val item = loadState.activity - val toaster = toaster + val toaster = LocalToaster.current val copyToastTitle = stringResource(R.string.common__copied) val tags by detailViewModel.tags.collectAsStateWithLifecycle() diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt index b3a4c3418..dc3b87d19 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/activity/ActivityExploreScreen.kt @@ -42,10 +42,11 @@ import com.synonym.bitkitcore.PaymentType import com.synonym.bitkitcore.TransactionDetails import to.bitkit.R import to.bitkit.ext.create -import to.bitkit.models.ToastText import to.bitkit.ext.ellipsisMiddle import to.bitkit.ext.isSent import to.bitkit.ext.totalValue +import to.bitkit.models.ToastText +import to.bitkit.ui.LocalToaster import to.bitkit.ui.Routes import to.bitkit.ui.components.BalanceHeaderView import to.bitkit.ui.components.BodySSB @@ -58,7 +59,6 @@ import to.bitkit.ui.screens.wallets.activity.components.ActivityIcon import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster import to.bitkit.ui.utils.copyToClipboard import to.bitkit.ui.utils.getBlockExplorerUrl import to.bitkit.ui.utils.getScreenTitleRes @@ -131,7 +131,7 @@ fun ActivityExploreScreen( is ActivityDetailViewModel.ActivityLoadState.Success -> { val item = loadState.activity - val toaster = toaster + val toaster = LocalToaster.current val context = LocalContext.current val txDetails by detailViewModel.txDetails.collectAsStateWithLifecycle() diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt index d9ff266d7..d4af62400 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveAmountScreen.kt @@ -31,6 +31,7 @@ import to.bitkit.R import to.bitkit.models.NodeLifecycleState import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies +import to.bitkit.ui.LocalToaster import to.bitkit.ui.blocktankViewModel import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.Caption13Up @@ -45,10 +46,10 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster import to.bitkit.ui.walletViewModel import to.bitkit.utils.Logger import to.bitkit.viewmodels.AmountInputViewModel @@ -61,8 +62,8 @@ fun ReceiveAmountScreen( onBack: () -> Unit, currencies: CurrencyState = LocalCurrencies.current, amountInputViewModel: AmountInputViewModel = hiltViewModel(), + toaster: Toaster = LocalToaster.current, ) { - val toaster = toaster val wallet = walletViewModel ?: return val blocktank = blocktankViewModel ?: return val lightningState by wallet.lightningState.collectAsStateWithLifecycle() diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt index c18a26847..ba5cbe9c6 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendAmountScreen.kt @@ -36,6 +36,7 @@ import to.bitkit.models.safe import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalBalances import to.bitkit.ui.LocalCurrencies +import to.bitkit.ui.LocalToaster import to.bitkit.ui.components.BottomSheetPreview import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.FillWidth @@ -52,10 +53,10 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.shared.modifiers.clickableAlpha import to.bitkit.ui.shared.modifiers.sheetHeight +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster import to.bitkit.viewmodels.AmountInputUiState import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.LnurlParams @@ -74,8 +75,8 @@ fun SendAmountScreen( onEvent: (SendEvent) -> Unit, currencies: CurrencyState = LocalCurrencies.current, amountInputViewModel: AmountInputViewModel = hiltViewModel(), + toaster: Toaster = LocalToaster.current, ) { - val toaster = toaster val amountInputUiState: AmountInputUiState by amountInputViewModel.uiState.collectAsStateWithLifecycle() val currentOnEvent by rememberUpdatedState(onEvent) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt index b443f1d82..0e5403243 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendRecipientScreen.kt @@ -58,8 +58,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import to.bitkit.R -import to.bitkit.models.ToastText import to.bitkit.ext.startActivityAppSettings +import to.bitkit.models.ToastText +import to.bitkit.ui.LocalToaster import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.BodyMSB import to.bitkit.ui.components.BottomSheetPreview @@ -70,12 +71,12 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.SheetTopBar import to.bitkit.ui.screens.scanner.QrCodeAnalyzer import to.bitkit.ui.shared.modifiers.sheetHeight +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.Shapes import to.bitkit.ui.theme.TRANSITION_SCREEN_MS -import to.bitkit.ui.toaster import to.bitkit.ui.utils.withAccent import to.bitkit.utils.AppError import to.bitkit.utils.Logger @@ -90,9 +91,8 @@ private const val TAG = "SendRecipientScreen" fun SendRecipientScreen( onEvent: (SendEvent) -> Unit, modifier: Modifier = Modifier, + toaster: Toaster = LocalToaster.current, ) { - val toaster = toaster - // Context & lifecycle val context = LocalContext.current val lifecycleOwner = LocalLifecycleOwner.current diff --git a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt index 721f0f325..cd971d717 100644 --- a/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/BlocktankRegtestScreen.kt @@ -29,6 +29,7 @@ import androidx.navigation.NavController import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.models.ToastText +import to.bitkit.ui.LocalToaster import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption import to.bitkit.ui.components.Caption13Up @@ -37,8 +38,8 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster import to.bitkit.ui.walletViewModel import to.bitkit.utils.Logger @@ -47,10 +48,10 @@ import to.bitkit.utils.Logger fun BlocktankRegtestScreen( navController: NavController, viewModel: BlocktankRegtestViewModel = hiltViewModel(), + toaster: Toaster = LocalToaster.current, ) { val coroutineScope = rememberCoroutineScope() val wallet = walletViewModel ?: return - val toaster = toaster val walletState by wallet.walletState.collectAsStateWithLifecycle() ScreenColumn { diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index e68f4b7cc..aa14e0dc0 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -26,6 +26,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import to.bitkit.R import to.bitkit.models.ToastText +import to.bitkit.ui.LocalToaster import to.bitkit.ui.Routes import to.bitkit.ui.components.settings.SettingsButtonRow import to.bitkit.ui.navigateToAboutSettings @@ -39,16 +40,16 @@ import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.settingsViewModel import to.bitkit.ui.shared.modifiers.clickableAlpha +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.theme.AppThemeSurface -import to.bitkit.ui.toaster private const val DEV_MODE_TAP_THRESHOLD = 5 @Composable fun SettingsScreen( navController: NavController, + toaster: Toaster = LocalToaster.current, ) { - val toaster = toaster val settings = settingsViewModel ?: return val isDevModeEnabled by settings.isDevModeEnabled.collectAsStateWithLifecycle() var enableDevModeTapCount by remember { mutableIntStateOf(0) } diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt index 7cdb1a559..4057c44bc 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/AddressViewerScreen.kt @@ -32,6 +32,7 @@ import to.bitkit.ext.setClipboardText import to.bitkit.models.AddressModel import to.bitkit.models.ToastText import to.bitkit.models.formatToModernDisplay +import to.bitkit.ui.LocalToaster import to.bitkit.ui.components.BodyS import to.bitkit.ui.components.ButtonSize import to.bitkit.ui.components.Caption @@ -45,10 +46,10 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.shared.modifiers.clickableAlpha +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.theme.AppShapes import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster import to.bitkit.ui.utils.BlockExplorerType import to.bitkit.ui.utils.getBlockExplorerUrl @@ -56,9 +57,9 @@ import to.bitkit.ui.utils.getBlockExplorerUrl fun AddressViewerScreen( navController: NavController, viewModel: AddressViewerViewModel = hiltViewModel(), + toaster: Toaster = LocalToaster.current, ) { val context = LocalContext.current - val toaster = toaster val uiState by viewModel.uiState.collectAsStateWithLifecycle() diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 2d8a6de80..79895a812 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -32,6 +32,7 @@ import to.bitkit.R import to.bitkit.models.ElectrumProtocol import to.bitkit.models.ElectrumServerPeer import to.bitkit.models.ToastText +import to.bitkit.ui.LocalToaster import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.FillHeight @@ -46,19 +47,19 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScanNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster @Composable fun ElectrumConfigScreen( savedStateHandle: SavedStateHandle, navController: NavController, viewModel: ElectrumConfigViewModel = hiltViewModel(), + toaster: Toaster = LocalToaster.current, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current - val toaster = toaster // Handle result from Scanner LaunchedEffect(savedStateHandle) { diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 27511b6f9..2be9dba8d 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -26,6 +26,7 @@ import androidx.navigation.NavController import kotlinx.coroutines.flow.filterNotNull import to.bitkit.R import to.bitkit.models.ToastText +import to.bitkit.ui.LocalToaster import to.bitkit.ui.components.BodyM import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.FillHeight @@ -38,18 +39,18 @@ import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.ScanNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.screens.scanner.SCAN_RESULT_KEY +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster @Composable fun RgsServerScreen( savedStateHandle: SavedStateHandle, navController: NavController, viewModel: RgsServerViewModel = hiltViewModel(), + toaster: Toaster = LocalToaster.current, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val toaster = toaster // Handle result from Scanner LaunchedEffect(savedStateHandle) { diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt index 75c3e540c..10c38b07c 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/ChannelDetailScreen.kt @@ -56,6 +56,7 @@ import to.bitkit.ext.amountOnClose import to.bitkit.ext.createChannelDetails import to.bitkit.ext.setClipboardText import to.bitkit.models.ToastText +import to.bitkit.ui.LocalToaster import to.bitkit.ui.Routes import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.CaptionB @@ -71,9 +72,9 @@ import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn import to.bitkit.ui.settings.lightning.components.ChannelStatusView import to.bitkit.ui.shared.modifiers.clickableAlpha +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.toaster import to.bitkit.ui.utils.getBlockExplorerUrl import to.bitkit.ui.walletViewModel import java.time.Instant @@ -85,9 +86,9 @@ import java.util.Locale fun ChannelDetailScreen( navController: NavController, viewModel: LightningConnectionsViewModel, + toaster: Toaster = LocalToaster.current, ) { val context = LocalContext.current - val toaster = toaster val wallet = walletViewModel ?: return val selectedChannel by viewModel.selectedChannel.collectAsStateWithLifecycle() From 50bbe71c4b189d2e33b4d9155f8331a6914428dd Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 14:12:36 +0100 Subject: [PATCH 26/35] refactor: remove toastException lambdas Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/ui/ContentView.kt | 7 ------ .../screens/transfer/SpendingAmountScreen.kt | 25 +++++++++++-------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index fd364b18e..ecd30b101 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -626,13 +626,6 @@ private fun RootNavHost( viewModel = transferViewModel, onBackClick = { navController.popBackStack() }, onOrderCreated = { navController.navigate(Routes.SpendingConfirm) }, - toastException = { appViewModel.toaster.error(it) }, - toast = { title, body -> - appViewModel.toaster.error( - title = ToastText(title), - body = ToastText(body) - ) - }, ) } composableWithDefaultTransitions { diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt index 7e015b9cc..83e1a47af 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt @@ -12,7 +12,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Devices.NEXUS_5 @@ -21,8 +20,11 @@ import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R +import to.bitkit.models.ToastText import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies +import to.bitkit.ui.LocalToaster +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.FillWidth @@ -54,15 +56,13 @@ fun SpendingAmountScreen( viewModel: TransferViewModel, onBackClick: () -> Unit = {}, onOrderCreated: () -> Unit = {}, - toastException: (Throwable) -> Unit, - toast: (title: String, description: String) -> Unit, + toaster: Toaster = LocalToaster.current, currencies: CurrencyState = LocalCurrencies.current, amountInputViewModel: AmountInputViewModel = hiltViewModel(), ) { val uiState by viewModel.spendingUiState.collectAsStateWithLifecycle() val isNodeRunning by viewModel.isNodeRunning.collectAsStateWithLifecycle() val amountUiState by amountInputViewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current LaunchedEffect(Unit) { viewModel.updateLimits() @@ -72,8 +72,11 @@ fun SpendingAmountScreen( viewModel.transferEffects.collect { effect -> when (effect) { TransferEffect.OnOrderCreated -> onOrderCreated() - is TransferEffect.ToastError -> toast(effect.title, effect.body) - is TransferEffect.ToastException -> toastException(effect.e) + is TransferEffect.ToastError -> toaster.error( + title = ToastText(effect.title), + body = ToastText(effect.body), + ) + is TransferEffect.ToastException -> toaster.error(effect.e) } } } @@ -88,10 +91,12 @@ fun SpendingAmountScreen( val quarter = uiState.balanceAfterFeeQuarter() val max = uiState.maxAllowedToSend if (quarter > max) { - toast( - context.getString(R.string.lightning__spending_amount__error_max__title), - context.getString(R.string.lightning__spending_amount__error_max__description) - .replace("{amount}", "$max"), + toaster.error( + title = ToastText(R.string.lightning__spending_amount__error_max__title), + body = ToastText( + R.string.lightning__spending_amount__error_max__description, + mapOf("amount" to "$max"), + ), ) } val cappedQuarter = min(quarter, max) From 26f0f75f69346a8ca76f124459ac4dd8c1bebaff Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 14:16:45 +0100 Subject: [PATCH 27/35] refactor: make toaster private, inject in MainActivity Co-Authored-By: Claude Opus 4.5 --- .../main/java/to/bitkit/repositories/BackupRepo.kt | 1 - app/src/main/java/to/bitkit/ui/ContentView.kt | 5 +++-- app/src/main/java/to/bitkit/ui/MainActivity.kt | 12 +++++++++--- .../ui/screens/transfer/SpendingAmountScreen.kt | 2 +- .../wallets/send/SendCoinSelectionViewModel.kt | 2 +- .../ui/settings/backups/BackupNavSheetViewModel.kt | 2 +- .../lightning/LightningConnectionsViewModel.kt | 2 +- .../main/java/to/bitkit/viewmodels/AppViewModel.kt | 2 +- .../to/bitkit/viewmodels/DevSettingsViewModel.kt | 2 +- .../java/to/bitkit/viewmodels/LdkDebugViewModel.kt | 7 +++++-- 10 files changed, 23 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt index 40f158354..be8472e31 100644 --- a/app/src/main/java/to/bitkit/repositories/BackupRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/BackupRepo.kt @@ -34,7 +34,6 @@ 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 diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index ecd30b101..914d1f0c6 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -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.ToastText import to.bitkit.models.WidgetType import to.bitkit.ui.Routes.ExternalConnection import to.bitkit.ui.components.AuthCheckScreen @@ -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 @@ -209,6 +209,7 @@ fun ContentView( transferViewModel: TransferViewModel, settingsViewModel: SettingsViewModel, backupsViewModel: BackupsViewModel, + toaster: Toaster, hazeState: HazeState, modifier: Modifier = Modifier, ) { @@ -360,7 +361,7 @@ fun ContentView( LocalTransferViewModel provides transferViewModel, LocalSettingsViewModel provides settingsViewModel, LocalBackupsViewModel provides backupsViewModel, - LocalToaster provides appViewModel.toaster, + LocalToaster provides toaster, LocalDrawerState provides drawerState, LocalBalances provides balance, LocalCurrencies provides currencies, diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 9fc9b9030..636218fbf 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -60,9 +60,12 @@ import to.bitkit.viewmodels.MainScreenEffect import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel import to.bitkit.viewmodels.WalletViewModel +import javax.inject.Inject @AndroidEntryPoint class MainActivity : FragmentActivity() { + @Inject lateinit var toaster: to.bitkit.ui.shared.toast.Toaster + private val appViewModel by viewModels() private val walletViewModel by viewModels() private val blocktankViewModel by viewModels() @@ -123,6 +126,7 @@ class MainActivity : FragmentActivity() { scope = scope, appViewModel = appViewModel, walletViewModel = walletViewModel, + toaster = toaster, ) } else { val isAuthenticated by appViewModel.isAuthenticated.collectAsStateWithLifecycle() @@ -137,6 +141,7 @@ class MainActivity : FragmentActivity() { transferViewModel = transferViewModel, settingsViewModel = settingsViewModel, backupsViewModel = backupsViewModel, + toaster = toaster, hazeState = hazeState, modifier = Modifier.hazeSource(hazeState, zIndex = 0f), ) @@ -229,6 +234,7 @@ private fun OnboardingNav( scope: CoroutineScope, appViewModel: AppViewModel, walletViewModel: WalletViewModel, + toaster: to.bitkit.ui.shared.toast.Toaster, ) { NavHost( navController = startupNavController, @@ -265,7 +271,7 @@ private fun OnboardingNav( walletViewModel.setInitNodeLifecycleState() walletViewModel.createWallet(bip39Passphrase = null) }.onFailure { - appViewModel.toaster.error(it) + toaster.error(it) } } }, @@ -295,7 +301,7 @@ private fun OnboardingNav( appViewModel.resetIsAuthenticatedState() walletViewModel.restoreWallet(mnemonic, passphrase) }.onFailure { - appViewModel.toaster.error(it) + toaster.error(it) } } } @@ -310,7 +316,7 @@ private fun OnboardingNav( appViewModel.resetIsAuthenticatedState() walletViewModel.createWallet(bip39Passphrase = passphrase) }.onFailure { - appViewModel.toaster.error(it) + toaster.error(it) } } }, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt index 83e1a47af..502932230 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt @@ -24,7 +24,6 @@ import to.bitkit.models.ToastText import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies import to.bitkit.ui.LocalToaster -import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight import to.bitkit.ui.components.FillWidth @@ -40,6 +39,7 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt index 40c9bb7c5..62efaadb1 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/send/SendCoinSelectionViewModel.kt @@ -16,8 +16,8 @@ import org.lightningdevkit.ldknode.SpendableUtxo import to.bitkit.R import to.bitkit.di.BgDispatcher import to.bitkit.env.Defaults -import to.bitkit.models.ToastText import to.bitkit.ext.rawId +import to.bitkit.models.ToastText import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.LightningRepo import to.bitkit.ui.shared.toast.Toaster diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index 090adc56b..031c3d731 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -15,11 +15,11 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import to.bitkit.R import to.bitkit.data.CacheStore -import to.bitkit.models.ToastText import to.bitkit.data.SettingsStore import to.bitkit.data.keychain.Keychain import to.bitkit.models.BackupCategory import to.bitkit.models.HealthState +import to.bitkit.models.ToastText import to.bitkit.repositories.HealthRepo import to.bitkit.ui.settings.backups.BackupContract.SideEffect import to.bitkit.ui.settings.backups.BackupContract.UiState diff --git a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt index 7814e2eb1..4301c3b9f 100644 --- a/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/lightning/LightningConnectionsViewModel.kt @@ -26,12 +26,12 @@ import org.lightningdevkit.ldknode.Event import org.lightningdevkit.ldknode.OutPoint import to.bitkit.R import to.bitkit.di.BgDispatcher -import to.bitkit.models.ToastText import to.bitkit.ext.amountOnClose import to.bitkit.ext.calculateRemoteBalance import to.bitkit.ext.createChannelDetails import to.bitkit.ext.filterOpen import to.bitkit.ext.filterPending +import to.bitkit.models.ToastText import to.bitkit.repositories.ActivityRepo import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.LightningRepo diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 60b46405b..7951e3aba 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -127,7 +127,7 @@ class AppViewModel @Inject constructor( connectivityRepo: ConnectivityRepo, healthRepo: HealthRepo, timedSheetManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> TimedSheetManager, - val toaster: Toaster, + private val toaster: Toaster, @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, private val keychain: Keychain, diff --git a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt index 5c5fa87a2..375250661 100644 --- a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt @@ -12,13 +12,13 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await import to.bitkit.R import to.bitkit.data.AppDb -import to.bitkit.models.ToastText import to.bitkit.data.CacheStore import to.bitkit.data.WidgetsStore import to.bitkit.env.Env import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NewTransactionSheetDirection import to.bitkit.models.NewTransactionSheetType +import to.bitkit.models.ToastText import to.bitkit.repositories.BlocktankRepo import to.bitkit.repositories.CurrencyRepo import to.bitkit.repositories.LightningRepo diff --git a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt index e9ccb3967..624139d37 100644 --- a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt @@ -16,8 +16,8 @@ import kotlinx.coroutines.launch import org.lightningdevkit.ldknode.PeerDetails import to.bitkit.data.backup.VssBackupClient import to.bitkit.di.BgDispatcher -import to.bitkit.models.ToastText import to.bitkit.ext.of +import to.bitkit.models.ToastText import to.bitkit.repositories.LightningRepo import to.bitkit.services.NetworkGraphInfo import to.bitkit.ui.shared.toast.Toaster @@ -115,7 +115,10 @@ class LdkDebugViewModel @Inject constructor( onFileReady(file) }.onFailure { e -> Logger.error("Failed to export network graph", e, context = TAG) - toaster.error(title = ToastText("Failed to export network graph"), body = e.message?.let { ToastText(it) }) + toaster.error( + title = ToastText("Failed to export network graph"), + body = e.message?.let { ToastText(it) } + ) } _uiState.update { it.copy(isLoading = false) } } From 24658f2eb9d87d7d67578ff9f376cf4e196eb07b Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 14:17:46 +0100 Subject: [PATCH 28/35] feat: localize migration restart toast Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 4 ++-- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 7951e3aba..21d4fb032 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -448,8 +448,8 @@ class AppViewModel @Inject constructor( delay(MIGRATION_AUTH_RESET_DELAY_MS) resetIsAuthenticatedStateInternal() toaster.error( - title = ToastText("Migration Warning"), - body = ToastText("Migration completed but node restart failed. Please restart the app.") + title = ToastText(R.string.wallet__migration_restart_failed_title), + body = ToastText(R.string.wallet__migration_restart_failed_body), ) } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 415efb3db..f6ea4c4ab 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1115,6 +1115,8 @@ Lower the custom fee and try again. Please restore your wallet manually using your recovery phrase Migration Failed + Migration completed but node restart failed. Please restart the app. + Migration Warning Fee Below Minimum Limit Increase the custom fee and try again. MINIMUM From 358ee1650fc124384e7db293602540cd768b5385 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 15:01:03 +0100 Subject: [PATCH 29/35] refactor: restore ToastQueue provider for testability Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/di/ViewModelModule.kt | 9 ++++++--- app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt index 0e7e8481d..2dda689e4 100644 --- a/app/src/main/java/to/bitkit/di/ViewModelModule.kt +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -5,6 +5,8 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import kotlinx.coroutines.CoroutineScope +import to.bitkit.ui.shared.toast.ToastQueue import javax.inject.Singleton @Module @@ -12,7 +14,8 @@ import javax.inject.Singleton object ViewModelModule { @Singleton @Provides - fun provideFirebaseMessaging(): FirebaseMessaging { - return FirebaseMessaging.getInstance() - } + fun provideFirebaseMessaging(): FirebaseMessaging = FirebaseMessaging.getInstance() + + @Provides + fun provideToastQueueProvider(): (CoroutineScope) -> ToastQueue = { ToastQueue(it) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index 21d4fb032..d518fd1de 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -127,6 +127,7 @@ class AppViewModel @Inject constructor( connectivityRepo: ConnectivityRepo, healthRepo: HealthRepo, timedSheetManagerProvider: @JvmSuppressWildcards (CoroutineScope) -> TimedSheetManager, + toastQueueProvider: @JvmSuppressWildcards (CoroutineScope) -> ToastQueue, private val toaster: Toaster, @ApplicationContext private val context: Context, @BgDispatcher private val bgDispatcher: CoroutineDispatcher, @@ -1780,7 +1781,7 @@ class AppViewModel @Inject constructor( // endregion // region Toasts - private val toastQueue = ToastQueue(viewModelScope) + private val toastQueue = toastQueueProvider(viewModelScope) val currentToast: StateFlow = toastQueue.currentToast fun hideToast() = toastQueue.dismissCurrentToast() From 5237794253bfc0d7d0b73c6af824126fb6f78c88 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 15:03:35 +0100 Subject: [PATCH 30/35] feat: localize currency rates error toast Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt | 5 +++-- app/src/main/res/values/strings.xml | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt index 0db66abdb..23834b2e1 100644 --- a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt @@ -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 @@ -94,8 +95,8 @@ class CurrencyRepo @Inject constructor( .collect { isStale -> if (isStale) { toaster.error( - title = ToastText("Rates currently unavailable"), - body = ToastText("An error has occurred. Please try again later.") + title = ToastText(R.string.currency__rates_error_title), + body = ToastText(R.string.currency__rates_error_body), ) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f6ea4c4ab..01ab36cdf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,6 +84,8 @@ Usable Yes Yes, Proceed + An error has occurred. Please try again later. + Rates currently unavailable Depends on the fee Depends on the fee Depends on the fee From 00b2273d90dab8b8cd5edae4ab28fa6230ef0740 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 15:05:35 +0100 Subject: [PATCH 31/35] refactor: use ToastText.Parameterized in ExternalNodeViewModel Co-Authored-By: Claude Opus 4.5 --- .../external/ExternalNodeViewModel.kt | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt index 7ffb28f3d..7cf3b39fd 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/external/ExternalNodeViewModel.kt @@ -1,10 +1,8 @@ package to.bitkit.ui.screens.transfer.external -import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow @@ -35,7 +33,6 @@ import javax.inject.Inject @Suppress("LongParameterList") @HiltViewModel class ExternalNodeViewModel @Inject constructor( - @ApplicationContext private val context: Context, private val walletRepo: WalletRepo, private val lightningRepo: LightningRepo, private val settingsStore: SettingsStore, @@ -98,15 +95,13 @@ class ExternalNodeViewModel @Inject constructor( val maxAmount = _uiState.value.amount.max if (sats > maxAmount) { - viewModelScope.launch { - toaster.error( - title = ToastText(R.string.lightning__spending_amount__error_max__title), - body = ToastText( - context.getString(R.string.lightning__spending_amount__error_max__description) - .replace("{amount}", maxAmount.formatToModernDisplay()) - ), - ) - } + toaster.error( + title = ToastText(R.string.lightning__spending_amount__error_max__title), + body = ToastText( + R.string.lightning__spending_amount__error_max__description, + mapOf("amount" to maxAmount.formatToModernDisplay()), + ), + ) return } @@ -193,8 +188,8 @@ class ExternalNodeViewModel @Inject constructor( toaster.error( title = ToastText(R.string.lightning__error_channel_purchase), body = ToastText( - context.getString(R.string.lightning__error_channel_setup_msg) - .replace("{raw}", error) + R.string.lightning__error_channel_setup_msg, + mapOf("raw" to error), ), ) } From 4aa5deb570d81adfdee698975040406428f8a1bb Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 18:17:57 +0100 Subject: [PATCH 32/35] refactor: clean up toast API usage Co-Authored-By: Claude Opus 4.5 --- AGENTS.md | 1 + .../main/java/to/bitkit/di/ViewModelModule.kt | 2 +- .../main/java/to/bitkit/ui/MainActivity.kt | 5 +-- .../transfer/SpendingAdvancedScreen.kt | 27 ++------------ .../screens/transfer/SpendingAmountScreen.kt | 5 --- .../to/bitkit/ui/settings/SettingsScreen.kt | 6 +--- .../settings/advanced/ElectrumConfigScreen.kt | 7 ++-- .../backups/BackupNavSheetViewModel.kt | 10 +++--- .../to/bitkit/ui/shared/toast/ToastQueue.kt | 8 ++--- .../java/to/bitkit/viewmodels/AppViewModel.kt | 11 +++--- .../to/bitkit/viewmodels/LdkDebugViewModel.kt | 4 +-- .../to/bitkit/viewmodels/TransferViewModel.kt | 21 +++++------ .../to/bitkit/viewmodels/WalletViewModel.kt | 12 ++----- .../bitkit/ui/shared/toast/ToastQueueTest.kt | 36 +++++++++---------- 14 files changed, 56 insertions(+), 99 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 20c73500c..d17df7136 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -206,6 +206,7 @@ suspend fun getData(): Result = 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 diff --git a/app/src/main/java/to/bitkit/di/ViewModelModule.kt b/app/src/main/java/to/bitkit/di/ViewModelModule.kt index 2dda689e4..e01b69a03 100644 --- a/app/src/main/java/to/bitkit/di/ViewModelModule.kt +++ b/app/src/main/java/to/bitkit/di/ViewModelModule.kt @@ -17,5 +17,5 @@ object ViewModelModule { fun provideFirebaseMessaging(): FirebaseMessaging = FirebaseMessaging.getInstance() @Provides - fun provideToastQueueProvider(): (CoroutineScope) -> ToastQueue = { ToastQueue(it) } + fun provideToastQueueProvider(): (CoroutineScope) -> ToastQueue = ::ToastQueue } diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 636218fbf..c8cb9274e 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -45,6 +45,7 @@ import to.bitkit.ui.onboarding.TermsOfUseScreen import to.bitkit.ui.onboarding.WarningMultipleDevicesScreen import to.bitkit.ui.screens.MigrationLoadingScreen import to.bitkit.ui.screens.SplashScreen +import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.sheets.ForgotPinSheet import to.bitkit.ui.sheets.NewTransactionSheet import to.bitkit.ui.theme.AppThemeSurface @@ -64,7 +65,7 @@ import javax.inject.Inject @AndroidEntryPoint class MainActivity : FragmentActivity() { - @Inject lateinit var toaster: to.bitkit.ui.shared.toast.Toaster + @Inject lateinit var toaster: Toaster private val appViewModel by viewModels() private val walletViewModel by viewModels() @@ -234,7 +235,7 @@ private fun OnboardingNav( scope: CoroutineScope, appViewModel: AppViewModel, walletViewModel: WalletViewModel, - toaster: to.bitkit.ui.shared.toast.Toaster, + toaster: Toaster, ) { NavHost( navController = startupNavController, diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt index 3b1bbd1a3..a4b14560d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAdvancedScreen.kt @@ -11,10 +11,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag @@ -26,10 +23,8 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import to.bitkit.R import to.bitkit.ext.mockOrder -import to.bitkit.models.ToastText import to.bitkit.repositories.CurrencyState import to.bitkit.ui.LocalCurrencies -import to.bitkit.ui.LocalToaster import to.bitkit.ui.components.Caption13Up import to.bitkit.ui.components.Display import to.bitkit.ui.components.FillHeight @@ -43,7 +38,6 @@ import to.bitkit.ui.components.VerticalSpacer import to.bitkit.ui.scaffold.AppTopBar import to.bitkit.ui.scaffold.DrawerNavIcon import to.bitkit.ui.scaffold.ScreenColumn -import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.withAccent @@ -62,13 +56,11 @@ fun SpendingAdvancedScreen( onOrderCreated: () -> Unit = {}, currencies: CurrencyState = LocalCurrencies.current, amountInputViewModel: AmountInputViewModel = hiltViewModel(), - toaster: Toaster = LocalToaster.current, ) { val currentOnOrderCreated by rememberUpdatedState(onOrderCreated) val state by viewModel.spendingUiState.collectAsStateWithLifecycle() val order = state.order ?: return val amountUiState by amountInputViewModel.uiState.collectAsStateWithLifecycle() - var isLoading by remember { mutableStateOf(false) } val transferValues by viewModel.transferValues.collectAsStateWithLifecycle() @@ -84,18 +76,6 @@ fun SpendingAdvancedScreen( viewModel.transferEffects.collect { effect -> when (effect) { TransferEffect.OnOrderCreated -> currentOnOrderCreated() - is TransferEffect.ToastException -> { - isLoading = false - toaster.error(effect.e) - } - - is TransferEffect.ToastError -> { - isLoading = false - toaster.error( - title = ToastText(effect.title), - body = ToastText(effect.body), - ) - } } } } @@ -109,14 +89,11 @@ fun SpendingAdvancedScreen( uiState = state, transferValues = transferValues, isValid = isValid, - isLoading = isLoading, + isLoading = state.isLoading, amountInputViewModel = amountInputViewModel, currencies = currencies, onBack = onBackClick, - onContinue = { - isLoading = true - viewModel.onSpendingAdvancedContinue(amountUiState.sats) - }, + onContinue = { viewModel.onSpendingAdvancedContinue(amountUiState.sats) }, ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt index 502932230..7f4545eca 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingAmountScreen.kt @@ -72,11 +72,6 @@ fun SpendingAmountScreen( viewModel.transferEffects.collect { effect -> when (effect) { TransferEffect.OnOrderCreated -> onOrderCreated() - is TransferEffect.ToastError -> toaster.error( - title = ToastText(effect.title), - body = ToastText(effect.body), - ) - is TransferEffect.ToastException -> toaster.error(effect.e) } } } diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index aa14e0dc0..d6174e960 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -73,7 +73,6 @@ fun SettingsScreen( val newValue = !isDevModeEnabled settings.setIsDevModeEnabled(newValue) haptic.performHapticFeedback(HapticFeedbackType.LongPress) - val titleRes = if (newValue) { R.string.settings__dev_enabled_title } else { @@ -84,10 +83,7 @@ fun SettingsScreen( } else { R.string.settings__dev_disabled_message } - toaster.success( - title = ToastText(titleRes), - body = ToastText(bodyRes), - ) + toaster.success(title = ToastText(titleRes), body = ToastText(bodyRes)) enableDevModeTapCount = 0 } }, diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt index 79895a812..64ab29f04 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/ElectrumConfigScreen.kt @@ -15,7 +15,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction @@ -59,7 +58,6 @@ fun ElectrumConfigScreen( toaster: Toaster = LocalToaster.current, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current // Handle result from Scanner LaunchedEffect(savedStateHandle) { @@ -78,9 +76,8 @@ fun ElectrumConfigScreen( toaster.success( title = ToastText(R.string.settings__es__server_updated_title), body = ToastText( - context.getString(R.string.settings__es__server_updated_message) - .replace("{host}", uiState.host) - .replace("{port}", uiState.port) + R.string.settings__es__server_updated_message, + mapOf("host" to uiState.host, "port" to uiState.port), ), testTag = "ElectrumUpdatedToast", ) diff --git a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt index 031c3d731..0fe0280c3 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backups/BackupNavSheetViewModel.kt @@ -156,12 +156,10 @@ class BackupNavSheetViewModel @Inject constructor( } fun onMnemonicCopied() { - viewModelScope.launch { - toaster.success( - title = ToastText(R.string.common__copied), - body = ToastText(R.string.security__mnemonic_copied), - ) - } + toaster.success( + title = ToastText(R.string.common__copied), + body = ToastText(R.string.security__mnemonic_copied), + ) } } diff --git a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt index 673a413ed..54f55ba56 100644 --- a/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt +++ b/app/src/main/java/to/bitkit/ui/shared/toast/ToastQueue.kt @@ -50,13 +50,13 @@ class ToastQueue(private val scope: CoroutineScope) { } newQueue } - dismissCurrentToast() + dismiss() } /** * Dismiss current toast and advance to next in queue. */ - fun dismissCurrentToast() { + fun dismiss() { cancelTimer() _currentToast.value = null isPaused = false @@ -67,7 +67,7 @@ class ToastQueue(private val scope: CoroutineScope) { /** * Pause current toast timer (called on drag start). */ - fun pauseCurrentToast() { + fun pause() { if (_currentToast.value?.autoHide == true) { isPaused = true cancelTimer() @@ -77,7 +77,7 @@ class ToastQueue(private val scope: CoroutineScope) { /** * Resume current toast timer with FULL duration (called on drag end). */ - fun resumeCurrentToast() { + fun resume() { val toast = _currentToast.value if (isPaused && toast != null) { isPaused = false diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index d518fd1de..36b792828 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -107,6 +107,7 @@ import to.bitkit.ui.shared.toast.ToastQueue import to.bitkit.ui.shared.toast.Toaster import to.bitkit.ui.sheets.SendRoute import to.bitkit.ui.theme.TRANSITION_SCREEN_MS +import to.bitkit.utils.AppError import to.bitkit.utils.Logger import to.bitkit.utils.jsonLogOf import to.bitkit.utils.timedsheets.TimedSheetManager @@ -1316,7 +1317,7 @@ class AppViewModel @Inject constructor( it.copy(decodedInvoice = invoice) } }.onFailure { - toaster.error(Exception(context.getString(R.string.wallet__error_lnurl_invoice_fetch))) + toaster.error(AppError(context.getString(R.string.wallet__error_lnurl_invoice_fetch))) hideSheet() return } @@ -1329,7 +1330,7 @@ class AppViewModel @Inject constructor( val validatedAddress = runCatching { validateBitcoinAddress(address) } .getOrElse { e -> Logger.error("Invalid bitcoin send address: '$address'", e, context = TAG) - toaster.error(Exception(context.getString(R.string.wallet__error_invalid_bitcoin_address))) + toaster.error(AppError(context.getString(R.string.wallet__error_invalid_bitcoin_address))) hideSheet() return } @@ -1784,11 +1785,11 @@ class AppViewModel @Inject constructor( private val toastQueue = toastQueueProvider(viewModelScope) val currentToast: StateFlow = toastQueue.currentToast - fun hideToast() = toastQueue.dismissCurrentToast() + fun hideToast() = toastQueue.dismiss() - fun pauseToast() = toastQueue.pauseCurrentToast() + fun pauseToast() = toastQueue.pause() - fun resumeToast() = toastQueue.resumeCurrentToast() + fun resumeToast() = toastQueue.resume() // endregion // region security diff --git a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt index 624139d37..3971f17ab 100644 --- a/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/LdkDebugViewModel.kt @@ -44,7 +44,7 @@ class LdkDebugViewModel @Inject constructor( fun addPeer() { val uri = _uiState.value.nodeUri.trim() if (uri.isEmpty()) { - viewModelScope.launch { toaster.warn(title = ToastText("Please enter a node URI")) } + toaster.warn(title = ToastText("Please enter a node URI")) return } connectPeer(uri) @@ -56,7 +56,7 @@ class LdkDebugViewModel @Inject constructor( val pastedUri = clipData?.getItemAt(0)?.text?.toString()?.trim() if (pastedUri.isNullOrEmpty()) { - viewModelScope.launch { toaster.warn(title = ToastText("Clipboard is empty")) } + toaster.warn(title = ToastText("Clipboard is empty")) return } diff --git a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt index b96d6d372..601fedad4 100644 --- a/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/TransferViewModel.kt @@ -91,13 +91,9 @@ class TransferViewModel @Inject constructor( fun onConfirmAmount(satsAmount: Long) { val values = blocktankRepo.calculateLiquidityOptions(satsAmount.toULong()).getOrNull() if (values == null || values.maxLspBalanceSat == 0uL) { - setTransferEffect( - TransferEffect.ToastError( - title = context.getString(R.string.lightning__spending_amount__error_max__title), - body = context.getString( - R.string.lightning__spending_amount__error_max__description_zero - ), - ) + toaster.error( + title = ToastText(R.string.lightning__spending_amount__error_max__title), + body = ToastText(R.string.lightning__spending_amount__error_max__description_zero), ) return } @@ -121,7 +117,7 @@ class TransferViewModel @Inject constructor( delay(1.seconds) // Give time to settle the UI _spendingUiState.update { it.copy(isLoading = false) } }.onFailure { e -> - setTransferEffect(TransferEffect.ToastException(e)) + toaster.error(e) delay(1.seconds) // Give time to settle the UI _spendingUiState.update { it.copy(isLoading = false) } } @@ -170,6 +166,7 @@ class TransferViewModel @Inject constructor( fun onSpendingAdvancedContinue(receivingAmountSats: Long) { viewModelScope.launch { + _spendingUiState.update { it.copy(isLoading = true) } runCatching { val oldOrder = _spendingUiState.value.order ?: return@launch val newOrder = blocktankRepo.createOrder( @@ -181,11 +178,13 @@ class TransferViewModel @Inject constructor( order = newOrder, defaultOrder = oldOrder, isAdvanced = true, + isLoading = false, ) } setTransferEffect(TransferEffect.OnOrderCreated) }.onFailure { e -> - setTransferEffect(TransferEffect.ToastException(e)) + _spendingUiState.update { it.copy(isLoading = false) } + toaster.error(e) } } } @@ -322,7 +321,7 @@ class TransferViewModel @Inject constructor( }.onFailure { exception -> _spendingUiState.update { it.copy(isLoading = false) } Logger.error("Failure", exception) - setTransferEffect(TransferEffect.ToastException(exception)) + toaster.error(exception) } } } @@ -566,7 +565,5 @@ data class TransferValues( sealed interface TransferEffect { data object OnOrderCreated : TransferEffect - data class ToastException(val e: Throwable) : TransferEffect - data class ToastError(val title: String, val body: String) : TransferEffect } // endregion diff --git a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt index b854d5ff0..f0e23fbe1 100644 --- a/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/WalletViewModel.kt @@ -307,20 +307,14 @@ class WalletViewModel @Inject constructor( ) } .onFailure { - toaster.error( - title = ToastText(R.string.common__error), - body = ToastText(it.message ?: context.getString(R.string.common__error_body)), - ) + toaster.error(it) } } } fun updateBip21Invoice(amountSats: ULong? = walletState.value.bip21AmountSats) = viewModelScope.launch { - walletRepo.updateBip21Invoice(amountSats).onFailure { error -> - toaster.error( - title = ToastText(R.string.wallet__error_invoice_update), - body = ToastText(error.message ?: context.getString(R.string.common__error_body)), - ) + walletRepo.updateBip21Invoice(amountSats).onFailure { + toaster.error(it) } } diff --git a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt index f341814ec..ee87ce5c5 100644 --- a/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt +++ b/app/src/test/java/to/bitkit/ui/shared/toast/ToastQueueTest.kt @@ -2,10 +2,10 @@ package to.bitkit.ui.shared.toast import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import org.junit.Assert.assertEquals import org.junit.Assert.assertNull -import org.junit.Before import org.junit.Test import to.bitkit.models.Toast import to.bitkit.models.ToastText @@ -17,13 +17,13 @@ import kotlin.time.Duration.Companion.seconds class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { private lateinit var sut: ToastQueue - @Before - fun setUp() { - sut = ToastQueue(testDispatcher) + private fun testQueue(block: suspend TestScope.() -> Unit): Unit = test { + sut = ToastQueue(this) + block() } @Test - fun `enqueue shows toast immediately when queue empty`() = test { + fun `enqueue shows toast immediately when queue empty`() = testQueue { val toast = createToast() sut.enqueue(toast) @@ -32,7 +32,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { } @Test - fun `enqueue queues toast when another is displayed`() = test { + fun `enqueue queues toast when another is displayed`() = testQueue { val toast1 = createToast(title = "First") val toast2 = createToast(title = "Second") @@ -43,7 +43,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { } @Test - fun `dismiss advances to next toast in queue`() = test { + fun `dismiss advances to next toast in queue`() = testQueue { val toast1 = createToast(title = "First", autoHide = false) val toast2 = createToast(title = "Second", autoHide = false) @@ -52,13 +52,13 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { assertEquals(ToastText.Literal("Second"), sut.currentToast.value?.title) - sut.dismissCurrentToast() + sut.dismiss() assertNull(sut.currentToast.value) } @Test - fun `auto-hide timer dismisses toast after duration`() = test { + fun `auto-hide timer dismisses toast after duration`() = testQueue { val toast = createToast(autoHide = true) sut.enqueue(toast) @@ -71,26 +71,26 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { } @Test - fun `pause stops auto-hide timer`() = test { + fun `pause stops auto-hide timer`() = testQueue { val toast = createToast(autoHide = true) sut.enqueue(toast) advanceTimeBy(1000) - sut.pauseCurrentToast() + sut.pause() advanceTimeBy(5000) assertEquals(toast, sut.currentToast.value) } @Test - fun `resume restarts auto-hide timer`() = test { + fun `resume restarts auto-hide timer`() = testQueue { val toast = createToast(autoHide = true) sut.enqueue(toast) advanceTimeBy(1000) - sut.pauseCurrentToast() + sut.pause() advanceTimeBy(5000) - sut.resumeCurrentToast() + sut.resume() advanceTimeBy(2000) assertEquals(toast, sut.currentToast.value) @@ -101,7 +101,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { } @Test - fun `max queue size drops oldest when exceeded`() = test { + fun `max queue size drops oldest when exceeded`() = testQueue { val toasts = (1..6).map { createToast(title = "Toast $it") } toasts.forEach { sut.enqueue(it) } @@ -110,7 +110,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { } @Test - fun `clear removes all toasts and hides current`() = test { + fun `clear removes all toasts and hides current`() = testQueue { val toast1 = createToast(title = "First", autoHide = false) val toast2 = createToast(title = "Second", autoHide = false) @@ -122,7 +122,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { } @Test - fun `non-auto-hide toast stays until dismissed`() = test { + fun `non-auto-hide toast stays until dismissed`() = testQueue { val toast = createToast(autoHide = false) sut.enqueue(toast) @@ -130,7 +130,7 @@ class ToastQueueTest : BaseUnitTest(StandardTestDispatcher()) { assertEquals(toast, sut.currentToast.value) - sut.dismissCurrentToast() + sut.dismiss() assertNull(sut.currentToast.value) } From e030ac2cc45c31e4e777c4c03665263ab1df4f72 Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 18:35:53 +0100 Subject: [PATCH 33/35] chore: cleanup --- .../to/bitkit/repositories/CurrencyRepo.kt | 10 +++--- .../java/to/bitkit/services/CoreService.kt | 15 +++------ .../to/bitkit/services/MigrationService.kt | 26 +++++++--------- .../to/bitkit/ui/settings/SettingsScreen.kt | 27 +++++++++------- .../bitkit/viewmodels/DevSettingsViewModel.kt | 31 +++++-------------- 5 files changed, 43 insertions(+), 66 deletions(-) diff --git a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt index 23834b2e1..4e474b35f 100644 --- a/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/CurrencyRepo.kt @@ -45,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, @@ -108,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 } diff --git a/app/src/main/java/to/bitkit/services/CoreService.kt b/app/src/main/java/to/bitkit/services/CoreService.kt index 25abc125a..ad42d88ed 100644 --- a/app/src/main/java/to/bitkit/services/CoreService.kt +++ b/app/src/main/java/to/bitkit/services/CoreService.kt @@ -629,6 +629,7 @@ class ActivityService( ) } + @Suppress("CyclomaticComplexMethod") private suspend fun processOnchainPayment( kind: PaymentKind.Onchain, payment: PaymentDetails, @@ -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 @@ -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, @@ -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)) } diff --git a/app/src/main/java/to/bitkit/services/MigrationService.kt b/app/src/main/java/to/bitkit/services/MigrationService.kt index 068b4c5a8..9e6b4f013 100644 --- a/app/src/main/java/to/bitkit/services/MigrationService.kt +++ b/app/src/main/java/to/bitkit/services/MigrationService.kt @@ -1394,7 +1394,7 @@ class MigrationService @Inject constructor( } } - @Suppress("CyclomaticComplexMethod") + @Suppress("CyclomaticComplexMethod", "LongMethod") private suspend fun updateOnchainActivityMetadata( item: RNActivityItem, onchain: OnchainActivity, @@ -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 } } @@ -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 { @@ -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, ) } } @@ -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, ) } } diff --git a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt index d6174e960..89c974daa 100644 --- a/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/SettingsScreen.kt @@ -67,23 +67,26 @@ fun SettingsScreen( onBackClick = { navController.popBackStack() }, onCogTap = { haptic.performHapticFeedback(HapticFeedbackType.Confirm) - enableDevModeTapCount = enableDevModeTapCount + 1 + enableDevModeTapCount += 1 if (enableDevModeTapCount >= DEV_MODE_TAP_THRESHOLD) { val newValue = !isDevModeEnabled settings.setIsDevModeEnabled(newValue) haptic.performHapticFeedback(HapticFeedbackType.LongPress) - val titleRes = if (newValue) { - R.string.settings__dev_enabled_title - } else { - R.string.settings__dev_disabled_title - } - val bodyRes = if (newValue) { - R.string.settings__dev_enabled_message - } else { - R.string.settings__dev_disabled_message - } - toaster.success(title = ToastText(titleRes), body = ToastText(bodyRes)) + toaster.success( + title = ToastText( + when (newValue) { + true -> R.string.settings__dev_enabled_title + else -> R.string.settings__dev_disabled_title + } + ), + body = ToastText( + when (newValue) { + true -> R.string.settings__dev_enabled_message + else -> R.string.settings__dev_disabled_message + } + ) + ) enableDevModeTapCount = 0 } }, diff --git a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt index 375250661..21c1a0f6e 100644 --- a/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/DevSettingsViewModel.kt @@ -1,13 +1,11 @@ package to.bitkit.viewmodels -import android.content.Context import android.net.Uri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.firebase.messaging.FirebaseMessaging import com.synonym.bitkitcore.testNotification import dagger.hilt.android.lifecycle.HiltViewModel -import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.launch import kotlinx.coroutines.tasks.await import to.bitkit.R @@ -31,7 +29,6 @@ import javax.inject.Inject @Suppress("TooManyFunctions", "LongParameterList") @HiltViewModel class DevSettingsViewModel @Inject constructor( - @ApplicationContext private val context: Context, private val firebaseMessaging: FirebaseMessaging, private val lightningRepo: LightningRepo, private val walletRepo: WalletRepo, @@ -87,13 +84,9 @@ class DevSettingsViewModel @Inject constructor( ) } - fun resetWidgetsState() = viewModelScope.launch { - widgetsStore.reset() - } + fun resetWidgetsState() = viewModelScope.launch { widgetsStore.reset() } - fun refreshCurrencyRates() = viewModelScope.launch { - currencyRepo.triggerRefresh() - } + fun refreshCurrencyRates() = viewModelScope.launch { currencyRepo.triggerRefresh() } fun zipLogsForSharing(onReady: (Uri) -> Unit) { viewModelScope.launch { @@ -108,25 +101,15 @@ class DevSettingsViewModel @Inject constructor( } } - fun resetBackupState() = viewModelScope.launch { - cacheStore.update { it.copy(backupStatuses = mapOf()) } - } + fun resetBackupState() = viewModelScope.launch { cacheStore.update { it.copy(backupStatuses = mapOf()) } } - fun wipeWallet() = viewModelScope.launch { - walletRepo.wipeWallet() - } + fun wipeWallet() = viewModelScope.launch { walletRepo.wipeWallet() } - fun resetCacheStore() = viewModelScope.launch { - cacheStore.reset() - } + fun resetCacheStore() = viewModelScope.launch { cacheStore.reset() } - fun resetDatabase() = viewModelScope.launch { - appDb.clearAllTables() - } + fun resetDatabase() = viewModelScope.launch { appDb.clearAllTables() } - fun resetBlocktankState() = viewModelScope.launch { - blocktankRepo.resetState() - } + fun resetBlocktankState() = viewModelScope.launch { blocktankRepo.resetState() } fun wipeLogs() = Logger.reset() } From 281d62fad7f94ffeea37216de410ea40d1de408f Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sat, 17 Jan 2026 23:30:36 +0100 Subject: [PATCH 34/35] fix: pass toaster to IsOnlineTracker Co-Authored-By: Claude Opus 4.5 --- app/src/main/java/to/bitkit/ui/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index c8cb9274e..33ee8b53f 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -132,7 +132,7 @@ class MainActivity : FragmentActivity() { } else { val isAuthenticated by appViewModel.isAuthenticated.collectAsStateWithLifecycle() - IsOnlineTracker(appViewModel) + IsOnlineTracker(appViewModel, toaster) ContentView( appViewModel = appViewModel, walletViewModel = walletViewModel, From 0995434cc87835d9377633dd9c92bd01c31017bd Mon Sep 17 00:00:00 2001 From: Ovi Trif Date: Sun, 18 Jan 2026 00:36:58 +0100 Subject: [PATCH 35/35] fix: localize error --- .../to/bitkit/ui/settings/advanced/RgsServerScreen.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt index 2be9dba8d..9f995de6e 100644 --- a/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/advanced/RgsServerScreen.kt @@ -74,7 +74,9 @@ fun RgsServerScreen( } else { toaster.error( title = ToastText(R.string.wallet__ldk_start_error_title), - body = ToastText(result.exceptionOrNull()?.message ?: "Unknown error"), + body = result.exceptionOrNull()?.message + ?.let { ToastText(it) } + ?: ToastText(R.string.common__error_body), testTag = "RgsErrorToast", ) } @@ -133,7 +135,9 @@ private fun Content( value = uiState.rgsUrl, onValueChange = onChangeUrl, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - modifier = Modifier.fillMaxWidth().testTag("RGSUrl") + modifier = Modifier + .fillMaxWidth() + .testTag("RGSUrl") ) FillHeight()