diff --git a/app/src/main/java/to/bitkit/data/SettingsStore.kt b/app/src/main/java/to/bitkit/data/SettingsStore.kt index 702e14522a..891ee27210 100644 --- a/app/src/main/java/to/bitkit/data/SettingsStore.kt +++ b/app/src/main/java/to/bitkit/data/SettingsStore.kt @@ -179,6 +179,7 @@ data class SettingsData( val isBiometricEnabled: Boolean = false, val isPinForPaymentsEnabled: Boolean = false, val isDevModeEnabled: Boolean = Env.isDebug, + val disableAllToasts: Boolean = false, val isSavingsSwapEnabled: Boolean = false, val showWidgets: Boolean = true, val lastUsedTags: List = emptyList(), 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 86d8965344..b0f7eb9b23 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 @@ -49,6 +49,7 @@ fun DevSettingsScreen( val settings = settingsViewModel ?: return val context = LocalContext.current val isPaykitEnabled by settings.isPaykitEnabled.collectAsStateWithLifecycle() + val disableAllToasts by settings.disableAllToasts.collectAsStateWithLifecycle() val isSavingsSwapEnabled by settings.isSavingsSwapEnabled.collectAsStateWithLifecycle() var showPaykitWarning by remember { mutableStateOf(false) } @@ -69,6 +70,13 @@ fun DevSettingsScreen( SettingsButtonRow("VSS") { navController.navigateTo(Routes.VssDebug) } SettingsButtonRow("Probing Tool") { navController.navigateTo(Routes.ProbingTool) } + SettingsSwitchRow( + title = "Disable All Toasts", + isChecked = disableAllToasts, + onClick = { settings.setDisableAllToasts(!disableAllToasts) }, + switchTestTag = "DisableAllToastsToggle", + ) + SectionHeader("SWAPS") SettingsButtonRow("Swaps") { navController.navigateTo(Routes.SwapsSettings) } SettingsSwitchRow( diff --git a/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt b/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt index d3da3c1cc6..74bddeff88 100644 --- a/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt +++ b/app/src/main/java/to/bitkit/ui/settings/support/SupportScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -43,6 +44,7 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController +import kotlinx.coroutines.launch import to.bitkit.BuildConfig import to.bitkit.R import to.bitkit.env.Env @@ -80,6 +82,7 @@ fun SupportScreen( val isDevModeEnabled by settings.isDevModeEnabled.collectAsStateWithLifecycle() var devModeTapCount by remember { mutableIntStateOf(0) } val haptic = LocalHapticFeedback.current + val scope = rememberCoroutineScope() Content( onBack = { navController.popBackStack() }, @@ -107,27 +110,30 @@ fun SupportScreen( if (devModeTapCount >= DEV_MODE_TAP_THRESHOLD) { val newValue = !isDevModeEnabled - settings.setIsDevModeEnabled(newValue) haptic.performHapticFeedback(HapticFeedbackType.LongPress) - app.toast( - type = Toast.ToastType.SUCCESS, - title = context.getString( - if (newValue) { - R.string.settings__dev_enabled_title - } else { - R.string.settings__dev_disabled_title - } - ), - description = context.getString( - if (newValue) { - R.string.settings__dev_enabled_message - } else { - R.string.settings__dev_disabled_message - } - ), - testTag = if (newValue) "DevModeEnabledToast" else "DevModeDisabledToast", - ) + scope.launch { + // Toast only after the write, so the disable-all-toasts gate already sees the new value + settings.setIsDevModeEnabled(newValue) + app.toast( + type = Toast.ToastType.SUCCESS, + title = context.getString( + if (newValue) { + R.string.settings__dev_enabled_title + } else { + R.string.settings__dev_disabled_title + } + ), + description = context.getString( + if (newValue) { + R.string.settings__dev_enabled_message + } else { + R.string.settings__dev_disabled_message + } + ), + testTag = if (newValue) "DevModeEnabledToast" else "DevModeDisabledToast", + ) + } devModeTapCount = 0 } }, diff --git a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt index c95e9d15a7..27db8f7f17 100644 --- a/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/AppViewModel.kt @@ -462,6 +462,12 @@ class AppViewModel @Inject constructor( private val toastManager = toastManagerProvider(viewModelScope) + // Toasts are hidden only while dev mode and the "Disable All Toasts" dev setting are both on + private val areToastsDisabled = settingsStore.data + .map { it.isDevModeEnabled && it.disableAllToasts } + .distinctUntilChanged() + .stateIn(viewModelScope, SharingStarted.Eagerly, false) + init { viewModelScope.launch { ToastEventBus.events.collect { @@ -4958,7 +4964,10 @@ class AppViewModel @Inject constructor( // endregion // region Toasts - val currentToast: StateFlow = toastManager.currentToast + // Hidden at display time, not enqueue time, so a toast raised together with a dev mode change follows the new state + val currentToast: StateFlow = combine(toastManager.currentToast, areToastsDisabled) { toast, disabled -> + toast.takeUnless { disabled } + }.stateIn(viewModelScope, SharingStarted.Eagerly, null) fun toast( type: Toast.ToastType, diff --git a/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt index a8f5723591..f2cb0a95d4 100644 --- a/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt @@ -192,9 +192,16 @@ class SettingsViewModel @Inject constructor( val isDevModeEnabled = settingsStore.data.map { it.isDevModeEnabled } .asStateFlow(initialValue = false) - fun setIsDevModeEnabled(value: Boolean) { + suspend fun setIsDevModeEnabled(value: Boolean) { + settingsStore.update { it.copy(isDevModeEnabled = value) } + } + + val disableAllToasts = settingsStore.data.map { it.disableAllToasts } + .asStateFlow(initialValue = false) + + fun setDisableAllToasts(value: Boolean) { viewModelScope.launch { - settingsStore.update { it.copy(isDevModeEnabled = value) } + settingsStore.update { it.copy(disableAllToasts = value) } } } diff --git a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt index 27b8dc61e9..b12f17bce2 100644 --- a/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/AppViewModelSendFlowTest.kt @@ -239,6 +239,7 @@ class AppViewModelSendFlowTest : BaseUnitTest() { private val refreshContactPaykitReceivers = mock() private val clipboardManager = mock() private val toastManager = mock() + private val toastState = MutableStateFlow(null) private val balanceState = MutableStateFlow(BalanceState()) private val connectivityState = MutableStateFlow(ConnectivityState.CONNECTED) @@ -287,6 +288,24 @@ class AppViewModelSendFlowTest : BaseUnitTest() { App.currentActivity = null } + @Test + fun `current toast is hidden only when dev mode and disable all toasts are both on`() = test { + val toast = Toast(Toast.ToastType.SUCCESS, "Toast", null, true, Toast.VISIBILITY_TIME_DEFAULT, null) + toastState.value = toast + + settingsData.value = SettingsData(isDevModeEnabled = true, disableAllToasts = true) + runCurrent() + assertNull(sut.currentToast.value) + + settingsData.value = SettingsData(isDevModeEnabled = false, disableAllToasts = true) + runCurrent() + assertEquals(toast, sut.currentToast.value) + + settingsData.value = SettingsData(isDevModeEnabled = true, disableAllToasts = false) + runCurrent() + assertEquals(toast, sut.currentToast.value) + } + @Test fun `session recovery failure during construction shows a toast`() = runTest(StandardTestDispatcher(testDispatcher.scheduler)) { @@ -441,7 +460,7 @@ class AppViewModelSendFlowTest : BaseUnitTest() { whenever { lightningRepo.getFeeRateForSpeed(any(), anyOrNull()) } .thenReturn(Result.success(2u)) whenever(lightningRepo.canSend(any())).thenReturn(true) - whenever(toastManager.currentToast).thenReturn(MutableStateFlow(null)) + whenever(toastManager.currentToast).thenReturn(toastState) } private fun stubSettingsStore() { diff --git a/app/src/test/java/to/bitkit/viewmodels/SettingsViewModelTest.kt b/app/src/test/java/to/bitkit/viewmodels/SettingsViewModelTest.kt index eecae3a4e4..a835d447d2 100644 --- a/app/src/test/java/to/bitkit/viewmodels/SettingsViewModelTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/SettingsViewModelTest.kt @@ -1,14 +1,17 @@ package to.bitkit.viewmodels import android.content.Context +import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.test.advanceUntilIdle import org.junit.Before import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.doSuspendableAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @@ -222,6 +225,29 @@ class SettingsViewModelTest : BaseUnitTest() { assertFalse(sut.isUpdatingContactPayments.value) } + @Test + fun `setting dev mode completes only after the settings write finishes`() = test { + settingsData.value = SettingsData(isDevModeEnabled = false) + val writeGate = CompletableDeferred() + whenever { settingsStore.update(any()) }.doSuspendableAnswer { + writeGate.await() + val transform = it.getArgument<(SettingsData) -> SettingsData>(0) + settingsData.value = transform(settingsData.value) + } + + val job = launch { sut.setIsDevModeEnabled(true) } + advanceUntilIdle() + + assertTrue(job.isActive) + assertFalse(settingsData.value.isDevModeEnabled) + + writeGate.complete(Unit) + advanceUntilIdle() + + assertFalse(job.isActive) + assertTrue(settingsData.value.isDevModeEnabled) + } + private fun createViewModel() = SettingsViewModel( context = context, settingsStore = settingsStore, diff --git a/changelog.d/next/1371.added.md b/changelog.d/next/1371.added.md new file mode 100644 index 0000000000..715c504525 --- /dev/null +++ b/changelog.d/next/1371.added.md @@ -0,0 +1 @@ +Bitkit can now hide all toasts from Dev Settings.