From 742e4a7a8ec9610dd121d4696ad8188b52b3ce21 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 11 Jun 2026 08:02:48 -0300 Subject: [PATCH 1/6] fix: request notification permission on enable --- app/src/main/java/to/bitkit/ui/ContentView.kt | 20 +++++++++++++++++-- changelog.d/next/1004.fixed.md | 1 + 2 files changed, 19 insertions(+), 2 deletions(-) create mode 100644 changelog.d/next/1004.fixed.md diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index e054b9b65..b8ecf8321 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -2,7 +2,11 @@ package to.bitkit.ui +import android.Manifest import android.content.Intent +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.DrawerState @@ -237,6 +241,12 @@ fun ContentView( val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() val walletExists = walletUiState.walletExists + val notificationPermissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + settingsViewModel.setNotificationPreference(granted) + } + // Effects on app entering fg (ON_START) / bg (ON_STOP) DisposableEffect(lifecycle) { val observer = LifecycleEventObserver { _, event -> @@ -501,8 +511,12 @@ fun ContentView( }, onEnable = { appViewModel.dismissTimedSheet() - navController.navigateTo(Routes.BackgroundPaymentsSettings) settingsViewModel.setBgPaymentsIntroSeen(true) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationPermissionLauncher.launch( + Manifest.permission.POST_NOTIFICATIONS + ) + } }, ) } @@ -896,8 +910,10 @@ private fun NavGraphBuilder.home( val isRecoveryMode by walletViewModel.isRecoveryMode.collectAsStateWithLifecycle() val hazeState = rememberHazeState() + // Only keep notification permission state in sync; the system dialog is requested + // from the background payments intro sheet, not automatically on the home screen. RequestNotificationPermissions( - showPermissionDialog = !isRecoveryMode, + showPermissionDialog = false, onPermissionChange = { granted -> settingsViewModel.setNotificationPreference(granted) } diff --git a/changelog.d/next/1004.fixed.md b/changelog.d/next/1004.fixed.md new file mode 100644 index 000000000..e533520d5 --- /dev/null +++ b/changelog.d/next/1004.fixed.md @@ -0,0 +1 @@ +The system notification permission dialog is now only requested when you tap Enable on the background payments prompt, instead of appearing automatically on every app open. From e1d1ae8cce204023d01379556785bbf024365f87 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 11 Jun 2026 08:34:48 -0300 Subject: [PATCH 2/6] fix: navigate to payment settings on android pre 13 --- app/src/main/java/to/bitkit/ui/ContentView.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index b8ecf8321..c73b49529 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -516,6 +516,10 @@ fun ContentView( notificationPermissionLauncher.launch( Manifest.permission.POST_NOTIFICATIONS ) + } else { + // Pre-13 has no runtime permission dialog; open the + // in-app background payments settings instead. + navController.navigateTo(Routes.BackgroundPaymentsSettings) } }, ) From f8f20a30c172343ad6c32f224f818c96d58f3e96 Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Thu, 11 Jun 2026 09:21:22 -0300 Subject: [PATCH 3/6] fix: trigger OS dialog from intro flow --- app/src/main/java/to/bitkit/ui/ContentView.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index c73b49529..a26487558 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -1382,10 +1382,18 @@ private fun NavGraphBuilder.generalSettingsSubScreens( } composableWithDefaultTransitions { + val notificationPermissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + settingsViewModel.setNotificationPreference(granted) + } BackgroundPaymentsIntroScreen( onBack = { navController.popBackStack() }, onLater = { navController.popBackStack() }, onEnable = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + } navController.navigateTo(Routes.BackgroundPaymentsSettings) }, ) From 79b223089cfd8e21062c488fbe130edafb80fd5e Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 12 Jun 2026 08:11:35 -0300 Subject: [PATCH 4/6] fix: apply OS permission dialog on bg notification switch --- app/src/main/java/to/bitkit/ui/ContentView.kt | 20 ++----- .../screens/transfer/SpendingConfirmScreen.kt | 9 ++- .../wallets/receive/ReceiveConfirmScreen.kt | 9 ++- .../wallets/receive/ReceiveLiquidityScreen.kt | 1 + .../screens/wallets/receive/ReceiveSheet.kt | 13 +++- .../utils/RequestNotificationPermissions.kt | 26 ++++++++ journeys/notification-permission/README.md | 59 +++++++++++++++++++ ...ceive-cjit-confirm-notification-toggle.xml | 23 ++++++++ ...ive-cjit-liquidity-notification-toggle.xml | 25 ++++++++ ...r-spending-confirm-notification-toggle.xml | 25 ++++++++ 10 files changed, 192 insertions(+), 18 deletions(-) create mode 100644 journeys/notification-permission/README.md create mode 100644 journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml create mode 100644 journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml create mode 100644 journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml diff --git a/app/src/main/java/to/bitkit/ui/ContentView.kt b/app/src/main/java/to/bitkit/ui/ContentView.kt index a26487558..bcbf41a2a 100644 --- a/app/src/main/java/to/bitkit/ui/ContentView.kt +++ b/app/src/main/java/to/bitkit/ui/ContentView.kt @@ -200,6 +200,7 @@ import to.bitkit.ui.utils.AutoReadClipboardHandler import to.bitkit.ui.utils.RequestNotificationPermissions import to.bitkit.ui.utils.composableWithDefaultTransitions import to.bitkit.ui.utils.navigationWithDefaultTransitions +import to.bitkit.ui.utils.rememberRequestNotificationPermission import to.bitkit.utils.Logger import to.bitkit.viewmodels.ActivityListViewModel import to.bitkit.viewmodels.AppViewModel @@ -241,11 +242,10 @@ fun ContentView( val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() val walletExists = walletUiState.walletExists - val notificationPermissionLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.RequestPermission() - ) { granted -> - settingsViewModel.setNotificationPreference(granted) - } + val requestNotificationPermission = rememberRequestNotificationPermission( + onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) }, + onPreTiramisu = { navController.navigateTo(Routes.BackgroundPaymentsSettings) }, + ) // Effects on app entering fg (ON_START) / bg (ON_STOP) DisposableEffect(lifecycle) { @@ -512,15 +512,7 @@ fun ContentView( onEnable = { appViewModel.dismissTimedSheet() settingsViewModel.setBgPaymentsIntroSeen(true) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - notificationPermissionLauncher.launch( - Manifest.permission.POST_NOTIFICATIONS - ) - } else { - // Pre-13 has no runtime permission dialog; open the - // in-app background payments settings instead. - navController.navigateTo(Routes.BackgroundPaymentsSettings) - } + requestNotificationPermission() }, ) } diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt index f5da7dc42..26f9ad29c 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt @@ -66,6 +66,7 @@ import to.bitkit.ui.theme.AppSwitchDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.RequestNotificationPermissions +import to.bitkit.ui.utils.rememberRequestNotificationPermission import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel @@ -100,6 +101,11 @@ fun SpendingConfirmScreen( showPermissionDialog = false, ) + val requestNotificationPermission = rememberRequestNotificationPermission( + onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) }, + onPreTiramisu = { context.openNotificationSettings() }, + ) + Box { Content( onBackClick = onBackClick, @@ -110,7 +116,7 @@ fun SpendingConfirmScreen( onTransferToSpendingConfirm = viewModel::onTransferToSpendingConfirm, order = order, hasNotificationPermission = notificationsGranted, - onSwitchClick = { context.openNotificationSettings() }, + onSwitchClick = requestNotificationPermission, isAdvanced = isAdvanced, ) AnimatedVisibility( @@ -224,6 +230,7 @@ private fun Content( isChecked = hasNotificationPermission, colors = AppSwitchDefaults.colorsPurple, onClick = onSwitchClick, + switchTestTag = "SpendingConfirmNotificationSwitch", modifier = Modifier.fillMaxWidth() ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt index 9722288f3..51532e7e5 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt @@ -43,6 +43,7 @@ import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppSwitchDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors +import to.bitkit.ui.utils.rememberRequestNotificationPermission import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.SettingsViewModel @@ -89,6 +90,11 @@ fun ReceiveConfirmScreen( } ?: sats.toString() } + val requestNotificationPermission = rememberRequestNotificationPermission( + onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) }, + onPreTiramisu = { context.openNotificationSettings() }, + ) + Content( receiveSats = entry.receiveAmountSats, networkFeeFormatted = networkFeeFormatted, @@ -96,7 +102,7 @@ fun ReceiveConfirmScreen( receiveAmountFormatted = receiveAmountFormatted, onLearnMoreClick = onLearnMore, isAdditional = isAdditional, - onSystemSettingsClick = { context.openNotificationSettings() }, + onSystemSettingsClick = requestNotificationPermission, hasNotificationPermission = notificationsGranted, onContinueClick = { onContinue(entry.invoice) }, onBackClick = onBack, @@ -162,6 +168,7 @@ private fun Content( isChecked = hasNotificationPermission, colors = AppSwitchDefaults.colorsPurple, onClick = onSystemSettingsClick, + switchTestTag = "ReceiveConfirmNotificationSwitch", modifier = Modifier.fillMaxWidth() ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt index 40b7895e3..07861ca9d 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt @@ -154,6 +154,7 @@ private fun Content( isChecked = hasNotificationPermission, colors = AppSwitchDefaults.colorsPurple, onClick = onSwitchClick, + switchTestTag = "ReceiveLiquidityNotificationSwitch", modifier = Modifier.fillMaxWidth() ) diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index 3151c4189..0162278c8 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -31,6 +31,7 @@ import to.bitkit.ui.openNotificationSettings import to.bitkit.ui.screens.wallets.send.AddTagScreen import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.utils.composableWithDefaultTransitions +import to.bitkit.ui.utils.rememberRequestNotificationPermission import to.bitkit.ui.walletViewModel import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.SettingsViewModel @@ -150,13 +151,17 @@ fun ReceiveSheet( cjitEntryDetails.value?.let { entryDetails -> val context = LocalContext.current val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() + val requestNotificationPermission = rememberRequestNotificationPermission( + onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) }, + onPreTiramisu = { context.openNotificationSettings() }, + ) ReceiveLiquidityScreen( entry = entryDetails, onContinue = { navController.popBackStack() }, onBack = { navController.popBackStack() }, hasNotificationPermission = notificationsGranted, - onSwitchClick = { context.openNotificationSettings() }, + onSwitchClick = requestNotificationPermission, ) } } @@ -164,6 +169,10 @@ fun ReceiveSheet( cjitEntryDetails.value?.let { entryDetails -> val context = LocalContext.current val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() + val requestNotificationPermission = rememberRequestNotificationPermission( + onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) }, + onPreTiramisu = { context.openNotificationSettings() }, + ) ReceiveLiquidityScreen( entry = entryDetails, @@ -171,7 +180,7 @@ fun ReceiveSheet( isAdditional = true, onBack = { navController.popBackStack() }, hasNotificationPermission = notificationsGranted, - onSwitchClick = { context.openNotificationSettings() }, + onSwitchClick = requestNotificationPermission, ) } } diff --git a/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt b/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt index 978554396..97041bceb 100644 --- a/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt +++ b/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt @@ -72,3 +72,29 @@ fun RequestNotificationPermissions( } } } + +@Composable +fun rememberRequestNotificationPermission( + onPermissionResult: (Boolean) -> Unit, + onPreTiramisu: () -> Unit, +): () -> Unit { + val currentOnPermissionResult by rememberUpdatedState(onPermissionResult) + val currentOnPreTiramisu by rememberUpdatedState(onPreTiramisu) + + val launcher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { granted -> + currentOnPermissionResult(granted) + } + + return remember(launcher) { + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + launcher.launch(Manifest.permission.POST_NOTIFICATIONS) + } else { + // Pre-13 has no runtime permission dialog; defer to the caller-provided fallback. + currentOnPreTiramisu() + } + } + } +} diff --git a/journeys/notification-permission/README.md b/journeys/notification-permission/README.md new file mode 100644 index 000000000..a5c6cadaf --- /dev/null +++ b/journeys/notification-permission/README.md @@ -0,0 +1,59 @@ +# Notification-permission journeys + +These journeys exercise the notification-permission request triggered by the +"Set up in background" toggle that backs Bitkit's background-payments setup. + +The behaviour was changed on `fix/limit-system-notification-permission`: tapping the +toggle now goes through the shared `rememberRequestNotificationPermission` helper +(`ui/utils/RequestNotificationPermissions.kt`) instead of jumping straight to the +system notification settings. + +## What the fix does +- **Android 13+ (API 33, TIRAMISU)**: tapping the toggle launches the OS + `POST_NOTIFICATIONS` runtime permission dialog. Granting it flips the toggle to + checked; the result is persisted via `SettingsViewModel.setNotificationPreference`. +- **Pre-13 (API < 33)**: there is no runtime dialog, so the toggle falls back to the + caller's `onPreTiramisu` action — the in-app background-payments settings on the + intro sheet, the system notification settings on the transfer/receive toggles. + +The same helper backs four entry points; these journeys cover the three the user can +reach directly: +- **Transfer → Spending confirm** (`SpendingConfirmScreen`) +- **Receive → CJIT confirm** (`ReceiveConfirmScreen`) +- **Receive → CJIT liquidity** (`ReceiveLiquidityScreen`, via "Learn more") + +## Mandatory setup +1. **Use an API 33+ device** to verify the runtime-dialog path. On API < 33 the dialog + never appears — only the system-settings fallback is exercised. +2. **Start from a fresh notification-permission state.** The OS only shows the + `POST_NOTIFICATIONS` dialog while the permission is in the "ask" state. Once granted + or denied it will not show again, and the journey will silently pass for the wrong + reason. Reset before each run: + `adb shell pm revoke to.bitkit.dev android.permission.POST_NOTIFICATIONS` + (or reinstall / clear app data). +3. **Node must be connected to the LSP (Blocktank).** Both the Transfer→Spending and + Receive→CJIT confirm screens need a real order quoted by Blocktank before the toggle + screen renders. With the hosted staging backend this is `api.stag0.blocktank.to`. +4. **Transfer→Spending also needs a positive on-chain Savings balance** so a real max can + be quoted. Fund + mine via the `blocktank-api:lsp` skill, then wait for the balance to + sync. + +## Gotchas +- **The permission dialog is one-shot** — see setup #2. Always revoke/reset first. +- **Blocktank must be reachable.** If `api.stag0.blocktank.to:443` is down, CJIT/order + creation hangs on a spinner at "Continue" and the confirm screen never appears, so the + toggle is unreachable. Verify the host first: + `curl -s -m 8 -o /dev/null -w '%{http_code}\n' https://api.stag0.blocktank.to/blocktank/api/v2/info` + (`000` = down). This is infra, not the toggle. +- The system permission dialog is **OS UI**, not Compose — locate its buttons with + `android screen --annotate` (text "Allow" / "Don't allow"), not `android layout` tags. +- On grant, the toggle reflects `notificationsGranted`; it only flips to checked once the + `ON_RESUME` re-check or the launcher callback fires. + +## Test tags +- Transfer→Spending toggle switch: `SpendingConfirmNotificationSwitch` +- Receive→CJIT confirm toggle switch: `ReceiveConfirmNotificationSwitch` +- Receive→CJIT liquidity toggle switch: `ReceiveLiquidityNotificationSwitch` +- Spending amount screen: `SpendingAmount`, continue `SpendingAmountContinue`, + available/max `SpendingAmountAvailable` / `SpendingAmountMax`. +- The toggle label on all screens is "Set up in background". diff --git a/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml b/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml new file mode 100644 index 000000000..46144c174 --- /dev/null +++ b/journeys/notification-permission/receive-cjit-confirm-notification-toggle.xml @@ -0,0 +1,23 @@ + + + Verifies that the "Set up in background" toggle on the Receive → CJIT confirm screen + (ReceiveConfirmScreen) launches the Android POST_NOTIFICATIONS runtime permission + dialog on API 33+, and that granting it checks the toggle. + + Precondition: API 33+ onboarded dev wallet with the node connected to the LSP so a + CJIT order can be quoted, and POST_NOTIFICATIONS in the "ask" state (revoke or + reinstall first — the dialog is one-shot). Start on the wallet home screen. + + + Tap the "Receive" button on the home screen + Tap the "Spending" tab in the Receive sheet + Tap "Receive Lightning funds" + On the amount screen, enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad + Tap "Continue" and wait for the CJIT order to be created and the confirm screen ("To set up your spending balance...") to appear + Verify the "Set up in background" toggle (testTag "ReceiveConfirmNotificationSwitch") is visible and unchecked + Tap the "Set up in background" toggle + Verify the Android system notification permission dialog appears (text like "Allow Bitkit to send you notifications?") + Tap "Allow" + Verify the "Set up in background" toggle is now checked + + diff --git a/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml b/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml new file mode 100644 index 000000000..1486c08a0 --- /dev/null +++ b/journeys/notification-permission/receive-cjit-liquidity-notification-toggle.xml @@ -0,0 +1,25 @@ + + + Verifies that the "Set up in background" toggle on the Receive → CJIT liquidity screen + (ReceiveLiquidityScreen, reached via "Learn more" from the confirm screen) launches the + Android POST_NOTIFICATIONS runtime permission dialog on API 33+, and that granting it + checks the toggle. + + Precondition: API 33+ onboarded dev wallet with the node connected to the LSP so a CJIT + order can be quoted, and POST_NOTIFICATIONS in the "ask" state (revoke or reinstall + first — the dialog is one-shot). Start on the wallet home screen. + + + Tap the "Receive" button on the home screen + Tap the "Spending" tab in the Receive sheet + Tap "Receive Lightning funds" + On the amount screen, enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad + Tap "Continue" and wait for the confirm screen to appear + Tap "Learn more" to open the liquidity screen + Verify the liquidity screen with the lightning channel and the "Set up in background" toggle (testTag "ReceiveLiquidityNotificationSwitch") is visible and unchecked + Tap the "Set up in background" toggle + Verify the Android system notification permission dialog appears (text like "Allow Bitkit to send you notifications?") + Tap "Allow" + Verify the "Set up in background" toggle is now checked + + diff --git a/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml b/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml new file mode 100644 index 000000000..71fde7c48 --- /dev/null +++ b/journeys/notification-permission/transfer-spending-confirm-notification-toggle.xml @@ -0,0 +1,25 @@ + + + Verifies that the "Set up in background" toggle on the Transfer → Spending confirm + screen (SpendingConfirmScreen) launches the Android POST_NOTIFICATIONS runtime + permission dialog on API 33+, and that granting it checks the toggle. + + Precondition: API 33+ onboarded dev wallet with a POSITIVE on-chain Savings balance and + the node connected to the LSP (so a real max can be quoted), and POST_NOTIFICATIONS in + the "ask" state (revoke or reinstall first — the dialog is one-shot). The spending max + starts at 0 behind a spinner — wait for it to populate. Start on the wallet home screen. + + + Tap the Spending balance card on the home screen + Tap "Transfer from Savings" + If a transfer intro screen appears, tap "Get Started" + On the spending amount screen (testTag "SpendingAmount"), wait until the available/maximum amount (testTag "SpendingAmountAvailable") finishes loading and shows a positive value + Enter an amount within the maximum using the number pad + Tap continue (testTag "SpendingAmountContinue") and wait for the spending confirm screen to appear + Verify the "Set up in background" toggle (testTag "SpendingConfirmNotificationSwitch") is visible and unchecked + Tap the "Set up in background" toggle + Verify the Android system notification permission dialog appears (text like "Allow Bitkit to send you notifications?") + Tap "Allow" + Verify the "Set up in background" toggle is now checked + + From c36f744c940666ec71c97f244bef2937c7b5ccac Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 12 Jun 2026 09:42:40 -0300 Subject: [PATCH 5/6] fix: navigate to settings on switch off --- .../screens/transfer/SpendingConfirmScreen.kt | 9 ++++---- .../wallets/receive/ReceiveConfirmScreen.kt | 9 ++++---- .../screens/wallets/receive/ReceiveSheet.kt | 16 ++++++++------ .../utils/RequestNotificationPermissions.kt | 22 +++++++++++++++++++ journeys/notification-permission/README.md | 11 ++++++---- 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt index 26f9ad29c..a543c7c37 100644 --- a/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt @@ -66,7 +66,7 @@ import to.bitkit.ui.theme.AppSwitchDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors import to.bitkit.ui.utils.RequestNotificationPermissions -import to.bitkit.ui.utils.rememberRequestNotificationPermission +import to.bitkit.ui.utils.rememberNotificationToggleClick import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.SettingsViewModel import to.bitkit.viewmodels.TransferViewModel @@ -101,9 +101,10 @@ fun SpendingConfirmScreen( showPermissionDialog = false, ) - val requestNotificationPermission = rememberRequestNotificationPermission( + val onNotificationSwitchClick = rememberNotificationToggleClick( + isGranted = notificationsGranted, onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) }, - onPreTiramisu = { context.openNotificationSettings() }, + onOpenSystemSettings = { context.openNotificationSettings() }, ) Box { @@ -116,7 +117,7 @@ fun SpendingConfirmScreen( onTransferToSpendingConfirm = viewModel::onTransferToSpendingConfirm, order = order, hasNotificationPermission = notificationsGranted, - onSwitchClick = requestNotificationPermission, + onSwitchClick = onNotificationSwitchClick, isAdvanced = isAdvanced, ) AnimatedVisibility( diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt index 51532e7e5..c1f10c1d2 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt @@ -43,7 +43,7 @@ import to.bitkit.ui.shared.util.gradientBackground import to.bitkit.ui.theme.AppSwitchDefaults import to.bitkit.ui.theme.AppThemeSurface import to.bitkit.ui.theme.Colors -import to.bitkit.ui.utils.rememberRequestNotificationPermission +import to.bitkit.ui.utils.rememberNotificationToggleClick import to.bitkit.ui.utils.withAccent import to.bitkit.viewmodels.SettingsViewModel @@ -90,9 +90,10 @@ fun ReceiveConfirmScreen( } ?: sats.toString() } - val requestNotificationPermission = rememberRequestNotificationPermission( + val onNotificationSwitchClick = rememberNotificationToggleClick( + isGranted = notificationsGranted, onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) }, - onPreTiramisu = { context.openNotificationSettings() }, + onOpenSystemSettings = { context.openNotificationSettings() }, ) Content( @@ -102,7 +103,7 @@ fun ReceiveConfirmScreen( receiveAmountFormatted = receiveAmountFormatted, onLearnMoreClick = onLearnMore, isAdditional = isAdditional, - onSystemSettingsClick = requestNotificationPermission, + onSystemSettingsClick = onNotificationSwitchClick, hasNotificationPermission = notificationsGranted, onContinueClick = { onContinue(entry.invoice) }, onBackClick = onBack, diff --git a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt index 0162278c8..75a330420 100644 --- a/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt +++ b/app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt @@ -31,7 +31,7 @@ import to.bitkit.ui.openNotificationSettings import to.bitkit.ui.screens.wallets.send.AddTagScreen import to.bitkit.ui.shared.modifiers.sheetHeight import to.bitkit.ui.utils.composableWithDefaultTransitions -import to.bitkit.ui.utils.rememberRequestNotificationPermission +import to.bitkit.ui.utils.rememberNotificationToggleClick import to.bitkit.ui.walletViewModel import to.bitkit.viewmodels.AmountInputViewModel import to.bitkit.viewmodels.SettingsViewModel @@ -151,9 +151,10 @@ fun ReceiveSheet( cjitEntryDetails.value?.let { entryDetails -> val context = LocalContext.current val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() - val requestNotificationPermission = rememberRequestNotificationPermission( + val onNotificationSwitchClick = rememberNotificationToggleClick( + isGranted = notificationsGranted, onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) }, - onPreTiramisu = { context.openNotificationSettings() }, + onOpenSystemSettings = { context.openNotificationSettings() }, ) ReceiveLiquidityScreen( @@ -161,7 +162,7 @@ fun ReceiveSheet( onContinue = { navController.popBackStack() }, onBack = { navController.popBackStack() }, hasNotificationPermission = notificationsGranted, - onSwitchClick = requestNotificationPermission, + onSwitchClick = onNotificationSwitchClick, ) } } @@ -169,9 +170,10 @@ fun ReceiveSheet( cjitEntryDetails.value?.let { entryDetails -> val context = LocalContext.current val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle() - val requestNotificationPermission = rememberRequestNotificationPermission( + val onNotificationSwitchClick = rememberNotificationToggleClick( + isGranted = notificationsGranted, onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) }, - onPreTiramisu = { context.openNotificationSettings() }, + onOpenSystemSettings = { context.openNotificationSettings() }, ) ReceiveLiquidityScreen( @@ -180,7 +182,7 @@ fun ReceiveSheet( isAdditional = true, onBack = { navController.popBackStack() }, hasNotificationPermission = notificationsGranted, - onSwitchClick = requestNotificationPermission, + onSwitchClick = onNotificationSwitchClick, ) } } diff --git a/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt b/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt index 97041bceb..fe43ccd1f 100644 --- a/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt +++ b/app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt @@ -98,3 +98,25 @@ fun rememberRequestNotificationPermission( } } } + +@Composable +fun rememberNotificationToggleClick( + isGranted: Boolean, + onPermissionResult: (Boolean) -> Unit, + onOpenSystemSettings: () -> Unit, +): () -> Unit { + val requestPermission = rememberRequestNotificationPermission( + onPermissionResult = onPermissionResult, + onPreTiramisu = onOpenSystemSettings, + ) + val currentIsGranted by rememberUpdatedState(isGranted) + val currentOnOpenSystemSettings by rememberUpdatedState(onOpenSystemSettings) + + return remember(requestPermission) { + { + // Already granted: the runtime request is a no-op, so send the user to system + // settings where they can actually turn notifications off. + if (currentIsGranted) currentOnOpenSystemSettings() else requestPermission() + } + } +} diff --git a/journeys/notification-permission/README.md b/journeys/notification-permission/README.md index a5c6cadaf..4b2faa413 100644 --- a/journeys/notification-permission/README.md +++ b/journeys/notification-permission/README.md @@ -9,12 +9,15 @@ toggle now goes through the shared `rememberRequestNotificationPermission` helpe system notification settings. ## What the fix does -- **Android 13+ (API 33, TIRAMISU)**: tapping the toggle launches the OS +- **Toggle OFF (permission already granted)**: re-requesting a granted permission is a + no-op, so tapping opens the **system notification settings** (`openNotificationSettings`) + where the user can actually turn notifications off — the behaviour these toggles had + before the refactor. +- **Toggle ON, Android 13+ (API 33, TIRAMISU)**: tapping launches the OS `POST_NOTIFICATIONS` runtime permission dialog. Granting it flips the toggle to checked; the result is persisted via `SettingsViewModel.setNotificationPreference`. -- **Pre-13 (API < 33)**: there is no runtime dialog, so the toggle falls back to the - caller's `onPreTiramisu` action — the in-app background-payments settings on the - intro sheet, the system notification settings on the transfer/receive toggles. +- **Toggle ON, pre-13 (API < 33)**: there is no runtime dialog, so it falls back to the + system notification settings. The same helper backs four entry points; these journeys cover the three the user can reach directly: From cfbe3373a8efc7b9c49fd22709d623dbafc0831a Mon Sep 17 00:00:00 2001 From: jvsena42 Date: Fri, 12 Jun 2026 09:59:13 -0300 Subject: [PATCH 6/6] test: update journeys --- .../toggle-off-opens-system-settings.xml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 journeys/notification-permission/toggle-off-opens-system-settings.xml diff --git a/journeys/notification-permission/toggle-off-opens-system-settings.xml b/journeys/notification-permission/toggle-off-opens-system-settings.xml new file mode 100644 index 000000000..c90ca54c4 --- /dev/null +++ b/journeys/notification-permission/toggle-off-opens-system-settings.xml @@ -0,0 +1,24 @@ + + + Verifies that when the "Set up in background" toggle is already ON (POST_NOTIFICATIONS + granted), tapping it opens the system notification settings instead of re-requesting the + permission (which would be a no-op). Uses the Receive → CJIT confirm screen, but the + behaviour is shared by the transfer and liquidity toggles. + + Precondition: API 33+ onboarded dev wallet with the node connected to the LSP so a CJIT + order can be quoted, and POST_NOTIFICATIONS ALREADY GRANTED so the toggle starts checked + (grant via: adb shell pm grant to.bitkit.dev android.permission.POST_NOTIFICATIONS). + Start on the wallet home screen. + + + Tap the "Receive" button on the home screen + Tap the "Spending" tab in the Receive sheet + Tap "Receive Lightning funds" + On the amount screen, enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad + Tap "Continue" and wait for the confirm screen to appear + Verify the "Set up in background" toggle (testTag "ReceiveConfirmNotificationSwitch") is visible and checked + Tap the "Set up in background" toggle + Verify the Android system notification settings screen for Bitkit opens (no in-app navigation, no permission dialog) + Press back to return to Bitkit and verify the confirm screen is still shown + +