diff --git a/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt b/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt index 6bb2e2b027..fd70ca71e8 100644 --- a/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt +++ b/app/src/main/java/to/bitkit/androidServices/LightningNodeService.kt @@ -35,6 +35,7 @@ import to.bitkit.models.NewTransactionSheetDetails import to.bitkit.models.NotificationDetails import to.bitkit.repositories.LightningRepo import to.bitkit.repositories.WalletRepo +import to.bitkit.services.NodeEventHandler import to.bitkit.services.NodeServiceFgState import to.bitkit.ui.ID_NOTIFICATION_NODE import to.bitkit.ui.MainActivity @@ -80,18 +81,20 @@ class LightningNodeService : Service() { private var hasStartedNode = false + private val nodeEventHandler: NodeEventHandler = { event -> + Logger.debug("LDK-node event received in $TAG: ${jsonLogOf(event)}", context = TAG) + handlePaymentReceived(event) + if (event is Event.ChannelReady) handleChannelReady(event) + handlePendingPaymentResolved(event) + } + private fun setupService() { if (hasStartedNode) return hasStartedNode = true serviceScope.launch { lightningRepo.start( - eventHandler = { event -> - Logger.debug("LDK-node event received in $TAG: ${jsonLogOf(event)}", context = TAG) - handlePaymentReceived(event) - if (event is Event.ChannelReady) handleChannelReady(event) - handlePendingPaymentResolved(event) - } + eventHandler = nodeEventHandler, ).onSuccess { walletRepo.setWalletExistsState() walletRepo.refreshBip21() @@ -238,8 +241,9 @@ class LightningNodeService : Service() { override fun onDestroy() { Logger.debug("onDestroy", context = TAG) nodeServiceFgState.setForegroundServiceRunning(false) - // Safe to call even if already stopped — guarded by lifecycleMutex + isStoppedOrStopping() - serviceScope.launch { lightningRepo.stop() } + // Drop our event handler so it isn't retained by the repo singleton across service restarts. + lightningRepo.removeEventHandler(nodeEventHandler) + stopNodeIfBackgrounded() super.onDestroy() } @@ -247,10 +251,18 @@ class LightningNodeService : Service() { override fun onTimeout(startId: Int, fgsType: Int) { Logger.warn("Reached foreground service timeout for type '$fgsType'", context = TAG) stopForegroundService(startId) - serviceScope.launch { lightningRepo.stop() } + stopNodeIfBackgrounded() super.onTimeout(startId, fgsType) } + private fun stopNodeIfBackgrounded() { + if (App.currentActivity?.value == null) { + serviceScope.launch { lightningRepo.stop() } + } else { + Logger.debug("Skipping node stop: activity is active", context = TAG) + } + } + override fun onBind(intent: Intent?): IBinder? = null companion object { diff --git a/app/src/main/java/to/bitkit/data/SettingsStore.kt b/app/src/main/java/to/bitkit/data/SettingsStore.kt index eba55786fd..ab21f8f496 100644 --- a/app/src/main/java/to/bitkit/data/SettingsStore.kt +++ b/app/src/main/java/to/bitkit/data/SettingsStore.kt @@ -142,7 +142,7 @@ data class SettingsData( val enableSendAmountWarning: Boolean = false, val backupVerified: Boolean = false, val notificationsGranted: Boolean = false, - val showNotificationDetails: Boolean = true, + val keepBitkitActiveInBackground: Boolean = false, val dismissedSuggestions: List = emptyList(), val balanceWarningIgnoredMillis: Long = 0, val backupWarningIgnoredMillis: Long = 0, diff --git a/app/src/main/java/to/bitkit/domain/commands/ReceivedNotificationContent.kt b/app/src/main/java/to/bitkit/domain/commands/ReceivedNotificationContent.kt index dd7309f761..822d6a54a5 100644 --- a/app/src/main/java/to/bitkit/domain/commands/ReceivedNotificationContent.kt +++ b/app/src/main/java/to/bitkit/domain/commands/ReceivedNotificationContent.kt @@ -28,11 +28,7 @@ class ReceivedNotificationContent @Inject constructor( suspend fun build(sats: Long): NotificationDetails { val settings = settingsStore.data.first() val title = context.getString(R.string.notification__received__title) - val body = if (settings.showNotificationDetails) { - formatAmount(sats, settings) - } else { - context.getString(R.string.notification__received__body_hidden) - } + val body = formatAmount(sats, settings) return NotificationDetails(title, body) } diff --git a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt index 36a99b9fef..d3b72aea80 100644 --- a/app/src/main/java/to/bitkit/repositories/LightningRepo.kt +++ b/app/src/main/java/to/bitkit/repositories/LightningRepo.kt @@ -431,6 +431,10 @@ class LightningRepo @Inject constructor( result } + fun removeEventHandler(handler: NodeEventHandler) { + _eventHandlers.remove(handler) + } + private suspend fun onEvent(event: Event) { handleLdkEvent(event) recordProbeOutcome(event) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index d98a6e7734..bc848aff16 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -246,6 +246,7 @@ fun ContentView( val isRecoveryMode by walletViewModel.isRecoveryMode.collectAsStateWithLifecycle() val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() + val keepActiveInBackground by settingsViewModel.keepBitkitActiveInBackground.collectAsStateWithLifecycle() val walletExists = walletUiState.walletExists val requestNotificationPermission = rememberRequestNotificationPermission( @@ -273,11 +274,10 @@ fun ContentView( } Lifecycle.Event.ON_STOP -> { - if (walletExists && !isRecoveryMode && !notificationsGranted) { - // App backgrounded without notification permission - stop node + val keptAliveByService = notificationsGranted && keepActiveInBackground + if (walletExists && !isRecoveryMode && !keptAliveByService) { walletViewModel.stop() } - // If notificationsGranted=true, service keeps node running } else -> Unit diff --git a/app/src/main/java/to/bitkit/ui/MainActivity.kt b/app/src/main/java/to/bitkit/ui/MainActivity.kt index 7693d14467..8336b7aded 100644 --- a/app/src/main/java/to/bitkit/ui/MainActivity.kt +++ b/app/src/main/java/to/bitkit/ui/MainActivity.kt @@ -31,7 +31,6 @@ import dagger.hilt.android.AndroidEntryPoint import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.rememberHazeState import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.serialization.Serializable import to.bitkit.R @@ -117,9 +116,8 @@ class MainActivity : FragmentActivity() { val scope = rememberCoroutineScope() val isRecoveryMode by walletViewModel.isRecoveryMode.collectAsStateWithLifecycle() val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() - val walletExists by walletViewModel.walletState - .map { it.walletExists } - .collectAsStateWithLifecycle(initialValue = walletViewModel.walletExists) + val keepActive by settingsViewModel.keepBitkitActiveInBackground.collectAsStateWithLifecycle() + val walletExists = walletViewModel.walletExists val isShowingMigrationLoading by walletViewModel.isShowingMigrationLoading.collectAsStateWithLifecycle() val restoreState by walletViewModel.restoreState.collectAsStateWithLifecycle() val hazeState = rememberHazeState(blurEnabled = true) @@ -128,11 +126,14 @@ class MainActivity : FragmentActivity() { walletExists, isRecoveryMode, notificationsGranted, + keepActive, restoreState, ) { - val canStartService = walletExists && notificationsGranted && restoreState.isIdle() + val canStartService = walletExists && notificationsGranted && keepActive && restoreState.isIdle() if (canStartService && !isRecoveryMode) { tryStartForegroundService() + } else { + stopForegroundService() } } @@ -264,9 +265,7 @@ class MainActivity : FragmentActivity() { override fun onDestroy() { super.onDestroy() if (!settingsViewModel.notificationsGranted.value) { - runCatching { - stopService(Intent(this, LightningNodeService::class.java)) - } + stopForegroundService() } } @@ -285,6 +284,14 @@ class MainActivity : FragmentActivity() { Logger.error("Failed to start LightningNodeService", error, context = "MainActivity") } } + + private fun stopForegroundService() { + runCatching { + stopService(Intent(this, LightningNodeService::class.java)) + }.onFailure { error -> + Logger.error("Failed to stop LightningNodeService", error, context = "MainActivity") + } + } } internal fun Intent?.launchKey(): String? { diff --git a/app/src/main/java/to/bitkit/ui/components/NotificationPreview.kt b/app/src/main/java/to/bitkit/ui/components/NotificationPreview.kt index 2169aa50b6..10d0f9f634 100644 --- a/app/src/main/java/to/bitkit/ui/components/NotificationPreview.kt +++ b/app/src/main/java/to/bitkit/ui/components/NotificationPreview.kt @@ -1,6 +1,5 @@ package to.bitkit.ui.components -import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -11,11 +10,13 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import to.bitkit.R @@ -23,51 +24,63 @@ import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.theme.Shapes +/** Degrees to rotate the right chevron so it points downward (expand-more) in the preview. */ +private const val CHEVRON_EXPAND_ROTATION_DEGREES = 90f + @Composable fun NotificationPreview( enabled: Boolean, title: String, description: String, - showDetails: Boolean, modifier: Modifier = Modifier, + time: String = "5m", ) { Box(modifier = modifier) { Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically, modifier = Modifier - .clip(Shapes.medium) - .background(Colors.White80) - .padding(9.dp) + .clip(Shapes.extraSmall) + .background(Colors.White16) + .padding(16.dp) ) { Image( painter = painterResource(R.drawable.ic_notification), contentDescription = null, - modifier = Modifier - .size(38.dp) + modifier = Modifier.size(24.dp) ) Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.SpaceBetween + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.weight(1f) ) { - BodySSB(text = title, color = Colors.Black) - val textDescription = when (showDetails) { - true -> description - else -> stringResource(R.string.notification__received__body_hidden) - } - AnimatedContent(targetState = textDescription) { text -> - Footnote(text = text, color = Colors.Gray3) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + BodySSB(text = title, color = Colors.White) + Caption(text = "•", color = Colors.White64) + Caption(text = time, color = Colors.White64) } + + BodyS(text = description, color = Colors.White80) } - Caption("3m ago", color = Colors.Gray2) + Icon( + painter = painterResource(R.drawable.ic_chevron_right), + contentDescription = null, + tint = Colors.White64, + modifier = Modifier + .size(16.dp) + .rotate(CHEVRON_EXPAND_ROTATION_DEGREES) + ) } if (!enabled) { Box( modifier = Modifier .matchParentSize() - .clip(Shapes.medium) + .clip(Shapes.extraSmall) .background(Colors.Black70) ) } @@ -79,24 +92,21 @@ fun NotificationPreview( private fun Preview() { AppThemeSurface { Column( + verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .fillMaxSize() - .padding(8.dp), - verticalArrangement = Arrangement.Center + .padding(16.dp) ) { NotificationPreview( enabled = true, title = "Payment Received", - description = "₿ 21 000", - showDetails = true, + description = "₿ 21 000 ($21.00)", modifier = Modifier.fillMaxWidth() ) - VerticalSpacer(16.dp) NotificationPreview( enabled = false, title = "Payment Received", - description = "₿ 21 000", - showDetails = false, + description = "₿ 21 000 ($21.00)", modifier = Modifier.fillMaxWidth() ) } diff --git a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt index 9242f686f3..db90c67454 100644 --- a/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt +++ b/app/src/main/java/to/bitkit/ui/settings/backgroundPayments/BackgroundPaymentsSettings.kt @@ -22,8 +22,6 @@ import to.bitkit.ui.components.NotificationPreview import to.bitkit.ui.components.SecondaryButton import to.bitkit.ui.components.Text13Up import to.bitkit.ui.components.VerticalSpacer -import to.bitkit.ui.components.settings.SettingsButtonRow -import to.bitkit.ui.components.settings.SettingsButtonValue import to.bitkit.ui.components.settings.SettingsSwitchRow import to.bitkit.ui.openNotificationSettings import to.bitkit.ui.scaffold.AppTopBar @@ -41,7 +39,7 @@ fun BackgroundPaymentsSettings( ) { val context = LocalContext.current val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() - val showNotificationDetails by settingsViewModel.showNotificationDetails.collectAsStateWithLifecycle() + val keepActive by settingsViewModel.keepBitkitActiveInBackground.collectAsStateWithLifecycle() RequestNotificationPermissions( onPermissionChange = settingsViewModel::setNotificationPreference, @@ -50,20 +48,20 @@ fun BackgroundPaymentsSettings( Content( hasPermission = notificationsGranted, - showDetails = showNotificationDetails, + keepActive = keepActive, onBack = onBack, onSystemSettingsClick = context::openNotificationSettings, - toggleNotificationDetails = settingsViewModel::toggleNotificationDetails, + onKeepActiveClick = { settingsViewModel.setKeepBitkitActiveInBackground(!keepActive) }, ) } @Composable private fun Content( hasPermission: Boolean, - showDetails: Boolean, + keepActive: Boolean, onBack: () -> Unit, onSystemSettingsClick: () -> Unit, - toggleNotificationDetails: () -> Unit, + onKeepActiveClick: () -> Unit, ) { Column( modifier = Modifier.screen() @@ -105,40 +103,48 @@ private fun Content( ) } - NotificationPreview( + SettingsSwitchRow( + title = stringResource(R.string.settings__bg__keep_active_title), + isChecked = keepActive, + onClick = onKeepActiveClick, enabled = hasPermission, - title = stringResource(R.string.notification__received__title), - description = "₿ 21 000", - showDetails = showDetails, - modifier = Modifier.fillMaxWidth() + ) + + BodyM( + text = stringResource(R.string.settings__bg__keep_active_desc), + color = Colors.White64, + modifier = Modifier.padding(vertical = 16.dp), ) VerticalSpacer(32.dp) Text13Up( - text = stringResource(R.string.settings__bg__privacy_header), + text = stringResource(R.string.settings__bg__notifications_header), color = Colors.White64 ) - SettingsButtonRow( - stringResource(R.string.settings__bg__include_amount), - value = SettingsButtonValue.BooleanValue(showDetails), - onClick = toggleNotificationDetails, + VerticalSpacer(16.dp) + + SecondaryButton( + stringResource(R.string.settings__bg__customize), + icon = { Image(painter = painterResource(R.drawable.ic_bell), contentDescription = null) }, + onClick = onSystemSettingsClick, ) VerticalSpacer(32.dp) Text13Up( - text = stringResource(R.string.settings__bg__notifications_header), + text = stringResource(R.string.common__preview), color = Colors.White64 ) VerticalSpacer(16.dp) - SecondaryButton( - stringResource(R.string.settings__bg__customize), - icon = { Image(painter = painterResource(R.drawable.ic_bell), contentDescription = null) }, - onClick = onSystemSettingsClick, + NotificationPreview( + enabled = hasPermission, + title = stringResource(R.string.notification__received__title), + description = "₿ 21 000 ($21.00)", + modifier = Modifier.fillMaxWidth() ) } } @@ -150,10 +156,10 @@ private fun Preview1() { AppThemeSurface { Content( hasPermission = true, - showDetails = true, + keepActive = true, onBack = {}, onSystemSettingsClick = {}, - toggleNotificationDetails = {}, + onKeepActiveClick = {}, ) } } @@ -164,10 +170,10 @@ private fun Preview2() { AppThemeSurface { Content( hasPermission = false, - showDetails = false, + keepActive = false, onBack = {}, onSystemSettingsClick = {}, - toggleNotificationDetails = {}, + onKeepActiveClick = {}, ) } } diff --git a/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt b/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt index c2c92a0473..6af5924f53 100644 --- a/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt +++ b/app/src/main/java/to/bitkit/viewmodels/SettingsViewModel.kt @@ -64,12 +64,12 @@ class SettingsViewModel @Inject constructor( } } - val showNotificationDetails = settingsStore.data.map { it.showNotificationDetails } + val keepBitkitActiveInBackground = settingsStore.data.map { it.keepBitkitActiveInBackground } .asStateFlow(initialValue = false) - fun toggleNotificationDetails() { + fun setKeepBitkitActiveInBackground(value: Boolean) { viewModelScope.launch { - settingsStore.update { it.copy(showNotificationDetails = !it.showNotificationDetails) } + settingsStore.update { it.copy(keepBitkitActiveInBackground = value) } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7a3a3a97e..1d9ff7c5ee 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -817,6 +817,8 @@ Enable Turn on notifications to get paid, even when your Bitkit app is closed. GET PAID\n<accent>PASSIVELY</accent> + Keeping Bitkit active results in more reliable payments. Android may show a persistent notification while this is enabled. + Keep Bitkit active in background Notifications Off On diff --git a/app/src/test/java/to/bitkit/domain/commands/NotifyChannelReadyHandlerTest.kt b/app/src/test/java/to/bitkit/domain/commands/NotifyChannelReadyHandlerTest.kt index 69f5fcdfef..4dfb328b98 100644 --- a/app/src/test/java/to/bitkit/domain/commands/NotifyChannelReadyHandlerTest.kt +++ b/app/src/test/java/to/bitkit/domain/commands/NotifyChannelReadyHandlerTest.kt @@ -46,7 +46,7 @@ class NotifyChannelReadyHandlerTest : BaseUnitTest() { fun setUp() { whenever(context.getString(R.string.notification__received__title)).thenReturn("Payment Received") whenever(context.getString(any(), any())).thenReturn("Received amount") - whenever(settingsStore.data).thenReturn(flowOf(SettingsData(showNotificationDetails = true))) + whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) whenever(currencyRepo.convertSatsToFiat(any(), anyOrNull())).thenReturn( Result.success( ConvertedAmount( @@ -156,30 +156,6 @@ class NotifyChannelReadyHandlerTest : BaseUnitTest() { verify(activityRepo).insertActivityFromCjit(cjitEntry, channel) } - @Test - fun `notification hides details when showNotificationDetails is false`() = test { - val event = mock { - on { channelId } doReturn "channel-1" - } - val channel = createChannelDetails().copy( - channelId = "channel-1", - outboundCapacityMsat = 3000_000u, - ) - val cjitEntry = IcJitEntry.mock() - whenever(lightningRepo.getChannels()).thenReturn(listOf(channel)) - whenever(blocktankRepo.getCjitEntry(channel)).thenReturn(cjitEntry) - whenever(activityRepo.insertActivityFromCjit(any(), any())).thenReturn(Result.success(true)) - whenever(settingsStore.data).thenReturn(flowOf(SettingsData(showNotificationDetails = false))) - whenever(context.getString(R.string.notification__received__body_hidden)).thenReturn("Hidden") - - val result = sut(NotifyChannelReady.Command(event = event, includeNotification = true)) - - assertTrue(result.isSuccess) - val showNotification = result.getOrThrow() - assertTrue(showNotification is NotifyChannelReady.Result.ShowNotification) - assertEquals("Hidden", showNotification.notification.body) - } - @Test fun `returns Duplicate when activity already exists`() = test { val event = mock { diff --git a/app/src/test/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandlerTest.kt b/app/src/test/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandlerTest.kt index 007a5b4688..1c935765f6 100644 --- a/app/src/test/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandlerTest.kt +++ b/app/src/test/java/to/bitkit/domain/commands/NotifyPaymentReceivedHandlerTest.kt @@ -41,7 +41,7 @@ class NotifyPaymentReceivedHandlerTest : BaseUnitTest() { fun setUp() { whenever(context.getString(R.string.notification__received__title)).thenReturn("Payment Received") whenever(context.getString(any(), any())).thenReturn("Received amount") - whenever(settingsStore.data).thenReturn(flowOf(SettingsData(showNotificationDetails = true))) + whenever(settingsStore.data).thenReturn(flowOf(SettingsData())) whenever(currencyRepo.convertSatsToFiat(any(), anyOrNull())).thenReturn( Result.success( ConvertedAmount( diff --git a/app/src/test/java/to/bitkit/domain/commands/ReceivedNotificationContentTest.kt b/app/src/test/java/to/bitkit/domain/commands/ReceivedNotificationContentTest.kt index 7452ef6d3c..d93231094d 100644 --- a/app/src/test/java/to/bitkit/domain/commands/ReceivedNotificationContentTest.kt +++ b/app/src/test/java/to/bitkit/domain/commands/ReceivedNotificationContentTest.kt @@ -89,18 +89,9 @@ class ReceivedNotificationContentTest : BaseUnitTest() { assertEquals(expected, result.body) } - @Test - fun `hides the amount when notification details are disabled`() = test { - whenever(settingsStore.data).thenReturn(flowOf(SettingsData(showNotificationDetails = false))) - - val result = sut.build(48_064L) - - assertEquals(context.getString(R.string.notification__received__body_hidden), result.body) - } - private fun stubSettings(primaryDisplay: PrimaryDisplay) { whenever(settingsStore.data).thenReturn( - flowOf(SettingsData(showNotificationDetails = true, primaryDisplay = primaryDisplay)), + flowOf(SettingsData(primaryDisplay = primaryDisplay)), ) } } diff --git a/app/src/test/java/to/bitkit/viewmodels/SettingsViewModelTest.kt b/app/src/test/java/to/bitkit/viewmodels/SettingsViewModelTest.kt index 350e4d9081..f494a0aa5a 100644 --- a/app/src/test/java/to/bitkit/viewmodels/SettingsViewModelTest.kt +++ b/app/src/test/java/to/bitkit/viewmodels/SettingsViewModelTest.kt @@ -159,6 +159,24 @@ class SettingsViewModelTest : BaseUnitTest() { verify(privatePaykitRepo).disableSharingAndPruneUnsavedContactState(contacts.value.map { it.publicKey }) } + @Test + fun `keepBitkitActiveInBackground defaults to false`() = test { + assertFalse(sut.keepBitkitActiveInBackground.value) + } + + @Test + fun `setKeepBitkitActiveInBackground persists the new value`() = test { + sut.setKeepBitkitActiveInBackground(true) + advanceUntilIdle() + + assertTrue(settingsData.value.keepBitkitActiveInBackground) + + sut.setKeepBitkitActiveInBackground(false) + advanceUntilIdle() + + assertFalse(settingsData.value.keepBitkitActiveInBackground) + } + private fun createViewModel() = SettingsViewModel( settingsStore = settingsStore, pubkyRepo = pubkyRepo, diff --git a/changelog.d/next/fg-service-optional.added.md b/changelog.d/next/fg-service-optional.added.md new file mode 100644 index 0000000000..e35b274d0d --- /dev/null +++ b/changelog.d/next/fg-service-optional.added.md @@ -0,0 +1 @@ +Background payments now include a "Keep Bitkit active in background" option for more reliable payments.