Skip to content

Commit 5b7091d

Browse files
authored
Merge branch 'master' into fix/set-max-amount-transfer
2 parents fcfd2f6 + 9de66b9 commit 5b7091d

12 files changed

Lines changed: 262 additions & 6 deletions

app/src/main/java/to/bitkit/ui/ContentView.kt

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
package to.bitkit.ui
44

5+
import android.Manifest
56
import android.content.Intent
7+
import android.os.Build
8+
import androidx.activity.compose.rememberLauncherForActivityResult
9+
import androidx.activity.result.contract.ActivityResultContracts
610
import androidx.compose.foundation.layout.Box
711
import androidx.compose.foundation.layout.fillMaxSize
812
import androidx.compose.material3.DrawerState
@@ -196,6 +200,7 @@ import to.bitkit.ui.utils.AutoReadClipboardHandler
196200
import to.bitkit.ui.utils.RequestNotificationPermissions
197201
import to.bitkit.ui.utils.composableWithDefaultTransitions
198202
import to.bitkit.ui.utils.navigationWithDefaultTransitions
203+
import to.bitkit.ui.utils.rememberRequestNotificationPermission
199204
import to.bitkit.utils.Logger
200205
import to.bitkit.viewmodels.ActivityListViewModel
201206
import to.bitkit.viewmodels.AppViewModel
@@ -237,6 +242,11 @@ fun ContentView(
237242
val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle()
238243
val walletExists = walletUiState.walletExists
239244

245+
val requestNotificationPermission = rememberRequestNotificationPermission(
246+
onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) },
247+
onPreTiramisu = { navController.navigateTo(Routes.BackgroundPaymentsSettings) },
248+
)
249+
240250
// Effects on app entering fg (ON_START) / bg (ON_STOP)
241251
DisposableEffect(lifecycle) {
242252
val observer = LifecycleEventObserver { _, event ->
@@ -501,8 +511,8 @@ fun ContentView(
501511
},
502512
onEnable = {
503513
appViewModel.dismissTimedSheet()
504-
navController.navigateTo(Routes.BackgroundPaymentsSettings)
505514
settingsViewModel.setBgPaymentsIntroSeen(true)
515+
requestNotificationPermission()
506516
},
507517
)
508518
}
@@ -896,8 +906,10 @@ private fun NavGraphBuilder.home(
896906
val isRecoveryMode by walletViewModel.isRecoveryMode.collectAsStateWithLifecycle()
897907
val hazeState = rememberHazeState()
898908

909+
// Only keep notification permission state in sync; the system dialog is requested
910+
// from the background payments intro sheet, not automatically on the home screen.
899911
RequestNotificationPermissions(
900-
showPermissionDialog = !isRecoveryMode,
912+
showPermissionDialog = false,
901913
onPermissionChange = { granted ->
902914
settingsViewModel.setNotificationPreference(granted)
903915
}
@@ -1362,10 +1374,18 @@ private fun NavGraphBuilder.generalSettingsSubScreens(
13621374
}
13631375

13641376
composableWithDefaultTransitions<Routes.BackgroundPaymentsIntro> {
1377+
val notificationPermissionLauncher = rememberLauncherForActivityResult(
1378+
ActivityResultContracts.RequestPermission()
1379+
) { granted ->
1380+
settingsViewModel.setNotificationPreference(granted)
1381+
}
13651382
BackgroundPaymentsIntroScreen(
13661383
onBack = { navController.popBackStack() },
13671384
onLater = { navController.popBackStack() },
13681385
onEnable = {
1386+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
1387+
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
1388+
}
13691389
navController.navigateTo(Routes.BackgroundPaymentsSettings)
13701390
},
13711391
)

app/src/main/java/to/bitkit/ui/screens/transfer/SpendingConfirmScreen.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import to.bitkit.ui.theme.AppSwitchDefaults
6666
import to.bitkit.ui.theme.AppThemeSurface
6767
import to.bitkit.ui.theme.Colors
6868
import to.bitkit.ui.utils.RequestNotificationPermissions
69+
import to.bitkit.ui.utils.rememberNotificationToggleClick
6970
import to.bitkit.ui.utils.withAccent
7071
import to.bitkit.viewmodels.SettingsViewModel
7172
import to.bitkit.viewmodels.TransferViewModel
@@ -100,6 +101,12 @@ fun SpendingConfirmScreen(
100101
showPermissionDialog = false,
101102
)
102103

104+
val onNotificationSwitchClick = rememberNotificationToggleClick(
105+
isGranted = notificationsGranted,
106+
onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) },
107+
onOpenSystemSettings = { context.openNotificationSettings() },
108+
)
109+
103110
Box {
104111
Content(
105112
onBackClick = onBackClick,
@@ -110,7 +117,7 @@ fun SpendingConfirmScreen(
110117
onTransferToSpendingConfirm = viewModel::onTransferToSpendingConfirm,
111118
order = order,
112119
hasNotificationPermission = notificationsGranted,
113-
onSwitchClick = { context.openNotificationSettings() },
120+
onSwitchClick = onNotificationSwitchClick,
114121
isAdvanced = isAdvanced,
115122
)
116123
AnimatedVisibility(
@@ -224,6 +231,7 @@ private fun Content(
224231
isChecked = hasNotificationPermission,
225232
colors = AppSwitchDefaults.colorsPurple,
226233
onClick = onSwitchClick,
234+
switchTestTag = "SpendingConfirmNotificationSwitch",
227235
modifier = Modifier.fillMaxWidth()
228236
)
229237

app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveConfirmScreen.kt

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import to.bitkit.ui.shared.util.gradientBackground
4343
import to.bitkit.ui.theme.AppSwitchDefaults
4444
import to.bitkit.ui.theme.AppThemeSurface
4545
import to.bitkit.ui.theme.Colors
46+
import to.bitkit.ui.utils.rememberNotificationToggleClick
4647
import to.bitkit.ui.utils.withAccent
4748
import to.bitkit.viewmodels.SettingsViewModel
4849

@@ -89,14 +90,20 @@ fun ReceiveConfirmScreen(
8990
} ?: sats.toString()
9091
}
9192

93+
val onNotificationSwitchClick = rememberNotificationToggleClick(
94+
isGranted = notificationsGranted,
95+
onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) },
96+
onOpenSystemSettings = { context.openNotificationSettings() },
97+
)
98+
9299
Content(
93100
receiveSats = entry.receiveAmountSats,
94101
networkFeeFormatted = networkFeeFormatted,
95102
serviceFeeFormatted = serviceFeeFormatted,
96103
receiveAmountFormatted = receiveAmountFormatted,
97104
onLearnMoreClick = onLearnMore,
98105
isAdditional = isAdditional,
99-
onSystemSettingsClick = { context.openNotificationSettings() },
106+
onSystemSettingsClick = onNotificationSwitchClick,
100107
hasNotificationPermission = notificationsGranted,
101108
onContinueClick = { onContinue(entry.invoice) },
102109
onBackClick = onBack,
@@ -162,6 +169,7 @@ private fun Content(
162169
isChecked = hasNotificationPermission,
163170
colors = AppSwitchDefaults.colorsPurple,
164171
onClick = onSystemSettingsClick,
172+
switchTestTag = "ReceiveConfirmNotificationSwitch",
165173
modifier = Modifier.fillMaxWidth()
166174
)
167175

app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveLiquidityScreen.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ private fun Content(
154154
isChecked = hasNotificationPermission,
155155
colors = AppSwitchDefaults.colorsPurple,
156156
onClick = onSwitchClick,
157+
switchTestTag = "ReceiveLiquidityNotificationSwitch",
157158
modifier = Modifier.fillMaxWidth()
158159
)
159160

app/src/main/java/to/bitkit/ui/screens/wallets/receive/ReceiveSheet.kt

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import to.bitkit.ui.openNotificationSettings
3131
import to.bitkit.ui.screens.wallets.send.AddTagScreen
3232
import to.bitkit.ui.shared.modifiers.sheetHeight
3333
import to.bitkit.ui.utils.composableWithDefaultTransitions
34+
import to.bitkit.ui.utils.rememberNotificationToggleClick
3435
import to.bitkit.ui.walletViewModel
3536
import to.bitkit.viewmodels.AmountInputViewModel
3637
import to.bitkit.viewmodels.SettingsViewModel
@@ -150,28 +151,38 @@ fun ReceiveSheet(
150151
cjitEntryDetails.value?.let { entryDetails ->
151152
val context = LocalContext.current
152153
val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle()
154+
val onNotificationSwitchClick = rememberNotificationToggleClick(
155+
isGranted = notificationsGranted,
156+
onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) },
157+
onOpenSystemSettings = { context.openNotificationSettings() },
158+
)
153159

154160
ReceiveLiquidityScreen(
155161
entry = entryDetails,
156162
onContinue = { navController.popBackStack() },
157163
onBack = { navController.popBackStack() },
158164
hasNotificationPermission = notificationsGranted,
159-
onSwitchClick = { context.openNotificationSettings() },
165+
onSwitchClick = onNotificationSwitchClick,
160166
)
161167
}
162168
}
163169
composableWithDefaultTransitions<ReceiveRoute.LiquidityAdditional> {
164170
cjitEntryDetails.value?.let { entryDetails ->
165171
val context = LocalContext.current
166172
val notificationsGranted by settingsViewModel.notificationsGranted.collectAsStateWithLifecycle()
173+
val onNotificationSwitchClick = rememberNotificationToggleClick(
174+
isGranted = notificationsGranted,
175+
onPermissionResult = { granted -> settingsViewModel.setNotificationPreference(granted) },
176+
onOpenSystemSettings = { context.openNotificationSettings() },
177+
)
167178

168179
ReceiveLiquidityScreen(
169180
entry = entryDetails,
170181
onContinue = { navController.popBackStack() },
171182
isAdditional = true,
172183
onBack = { navController.popBackStack() },
173184
hasNotificationPermission = notificationsGranted,
174-
onSwitchClick = { context.openNotificationSettings() },
185+
onSwitchClick = onNotificationSwitchClick,
175186
)
176187
}
177188
}

app/src/main/java/to/bitkit/ui/utils/RequestNotificationPermissions.kt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,51 @@ fun RequestNotificationPermissions(
7272
}
7373
}
7474
}
75+
76+
@Composable
77+
fun rememberRequestNotificationPermission(
78+
onPermissionResult: (Boolean) -> Unit,
79+
onPreTiramisu: () -> Unit,
80+
): () -> Unit {
81+
val currentOnPermissionResult by rememberUpdatedState(onPermissionResult)
82+
val currentOnPreTiramisu by rememberUpdatedState(onPreTiramisu)
83+
84+
val launcher = rememberLauncherForActivityResult(
85+
ActivityResultContracts.RequestPermission()
86+
) { granted ->
87+
currentOnPermissionResult(granted)
88+
}
89+
90+
return remember(launcher) {
91+
{
92+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
93+
launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
94+
} else {
95+
// Pre-13 has no runtime permission dialog; defer to the caller-provided fallback.
96+
currentOnPreTiramisu()
97+
}
98+
}
99+
}
100+
}
101+
102+
@Composable
103+
fun rememberNotificationToggleClick(
104+
isGranted: Boolean,
105+
onPermissionResult: (Boolean) -> Unit,
106+
onOpenSystemSettings: () -> Unit,
107+
): () -> Unit {
108+
val requestPermission = rememberRequestNotificationPermission(
109+
onPermissionResult = onPermissionResult,
110+
onPreTiramisu = onOpenSystemSettings,
111+
)
112+
val currentIsGranted by rememberUpdatedState(isGranted)
113+
val currentOnOpenSystemSettings by rememberUpdatedState(onOpenSystemSettings)
114+
115+
return remember(requestPermission) {
116+
{
117+
// Already granted: the runtime request is a no-op, so send the user to system
118+
// settings where they can actually turn notifications off.
119+
if (currentIsGranted) currentOnOpenSystemSettings() else requestPermission()
120+
}
121+
}
122+
}

changelog.d/next/1004.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Notification-permission journeys
2+
3+
These journeys exercise the notification-permission request triggered by the
4+
"Set up in background" toggle that backs Bitkit's background-payments setup.
5+
6+
The behaviour was changed on `fix/limit-system-notification-permission`: tapping the
7+
toggle now goes through the shared `rememberRequestNotificationPermission` helper
8+
(`ui/utils/RequestNotificationPermissions.kt`) instead of jumping straight to the
9+
system notification settings.
10+
11+
## What the fix does
12+
- **Toggle OFF (permission already granted)**: re-requesting a granted permission is a
13+
no-op, so tapping opens the **system notification settings** (`openNotificationSettings`)
14+
where the user can actually turn notifications off — the behaviour these toggles had
15+
before the refactor.
16+
- **Toggle ON, Android 13+ (API 33, TIRAMISU)**: tapping launches the OS
17+
`POST_NOTIFICATIONS` runtime permission dialog. Granting it flips the toggle to
18+
checked; the result is persisted via `SettingsViewModel.setNotificationPreference`.
19+
- **Toggle ON, pre-13 (API < 33)**: there is no runtime dialog, so it falls back to the
20+
system notification settings.
21+
22+
The same helper backs four entry points; these journeys cover the three the user can
23+
reach directly:
24+
- **Transfer → Spending confirm** (`SpendingConfirmScreen`)
25+
- **Receive → CJIT confirm** (`ReceiveConfirmScreen`)
26+
- **Receive → CJIT liquidity** (`ReceiveLiquidityScreen`, via "Learn more")
27+
28+
## Mandatory setup
29+
1. **Use an API 33+ device** to verify the runtime-dialog path. On API < 33 the dialog
30+
never appears — only the system-settings fallback is exercised.
31+
2. **Start from a fresh notification-permission state.** The OS only shows the
32+
`POST_NOTIFICATIONS` dialog while the permission is in the "ask" state. Once granted
33+
or denied it will not show again, and the journey will silently pass for the wrong
34+
reason. Reset before each run:
35+
`adb shell pm revoke to.bitkit.dev android.permission.POST_NOTIFICATIONS`
36+
(or reinstall / clear app data).
37+
3. **Node must be connected to the LSP (Blocktank).** Both the Transfer→Spending and
38+
Receive→CJIT confirm screens need a real order quoted by Blocktank before the toggle
39+
screen renders. With the hosted staging backend this is `api.stag0.blocktank.to`.
40+
4. **Transfer→Spending also needs a positive on-chain Savings balance** so a real max can
41+
be quoted. Fund + mine via the `blocktank-api:lsp` skill, then wait for the balance to
42+
sync.
43+
44+
## Gotchas
45+
- **The permission dialog is one-shot** — see setup #2. Always revoke/reset first.
46+
- **Blocktank must be reachable.** If `api.stag0.blocktank.to:443` is down, CJIT/order
47+
creation hangs on a spinner at "Continue" and the confirm screen never appears, so the
48+
toggle is unreachable. Verify the host first:
49+
`curl -s -m 8 -o /dev/null -w '%{http_code}\n' https://api.stag0.blocktank.to/blocktank/api/v2/info`
50+
(`000` = down). This is infra, not the toggle.
51+
- The system permission dialog is **OS UI**, not Compose — locate its buttons with
52+
`android screen --annotate` (text "Allow" / "Don't allow"), not `android layout` tags.
53+
- On grant, the toggle reflects `notificationsGranted`; it only flips to checked once the
54+
`ON_RESUME` re-check or the launcher callback fires.
55+
56+
## Test tags
57+
- Transfer→Spending toggle switch: `SpendingConfirmNotificationSwitch`
58+
- Receive→CJIT confirm toggle switch: `ReceiveConfirmNotificationSwitch`
59+
- Receive→CJIT liquidity toggle switch: `ReceiveLiquidityNotificationSwitch`
60+
- Spending amount screen: `SpendingAmount`, continue `SpendingAmountContinue`,
61+
available/max `SpendingAmountAvailable` / `SpendingAmountMax`.
62+
- The toggle label on all screens is "Set up in background".
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<journey name="receive cjit confirm toggle requests notification permission">
2+
<description>
3+
Verifies that the "Set up in background" toggle on the Receive → CJIT confirm screen
4+
(ReceiveConfirmScreen) launches the Android POST_NOTIFICATIONS runtime permission
5+
dialog on API 33+, and that granting it checks the toggle.
6+
7+
Precondition: API 33+ onboarded dev wallet with the node connected to the LSP so a
8+
CJIT order can be quoted, and POST_NOTIFICATIONS in the "ask" state (revoke or
9+
reinstall first — the dialog is one-shot). Start on the wallet home screen.
10+
</description>
11+
<actions>
12+
<action>Tap the "Receive" button on the home screen</action>
13+
<action>Tap the "Spending" tab in the Receive sheet</action>
14+
<action>Tap "Receive Lightning funds"</action>
15+
<action>On the amount screen, enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad</action>
16+
<action>Tap "Continue" and wait for the CJIT order to be created and the confirm screen ("To set up your spending balance...") to appear</action>
17+
<action>Verify the "Set up in background" toggle (testTag "ReceiveConfirmNotificationSwitch") is visible and unchecked</action>
18+
<action>Tap the "Set up in background" toggle</action>
19+
<action>Verify the Android system notification permission dialog appears (text like "Allow Bitkit to send you notifications?")</action>
20+
<action>Tap "Allow"</action>
21+
<action>Verify the "Set up in background" toggle is now checked</action>
22+
</actions>
23+
</journey>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<journey name="receive cjit liquidity toggle requests notification permission">
2+
<description>
3+
Verifies that the "Set up in background" toggle on the Receive → CJIT liquidity screen
4+
(ReceiveLiquidityScreen, reached via "Learn more" from the confirm screen) launches the
5+
Android POST_NOTIFICATIONS runtime permission dialog on API 33+, and that granting it
6+
checks the toggle.
7+
8+
Precondition: API 33+ onboarded dev wallet with the node connected to the LSP so a CJIT
9+
order can be quoted, and POST_NOTIFICATIONS in the "ask" state (revoke or reinstall
10+
first — the dialog is one-shot). Start on the wallet home screen.
11+
</description>
12+
<actions>
13+
<action>Tap the "Receive" button on the home screen</action>
14+
<action>Tap the "Spending" tab in the Receive sheet</action>
15+
<action>Tap "Receive Lightning funds"</action>
16+
<action>On the amount screen, enter an amount above the CJIT minimum (e.g. 100 000 sats) using the number pad</action>
17+
<action>Tap "Continue" and wait for the confirm screen to appear</action>
18+
<action>Tap "Learn more" to open the liquidity screen</action>
19+
<action>Verify the liquidity screen with the lightning channel and the "Set up in background" toggle (testTag "ReceiveLiquidityNotificationSwitch") is visible and unchecked</action>
20+
<action>Tap the "Set up in background" toggle</action>
21+
<action>Verify the Android system notification permission dialog appears (text like "Allow Bitkit to send you notifications?")</action>
22+
<action>Tap "Allow"</action>
23+
<action>Verify the "Set up in background" toggle is now checked</action>
24+
</actions>
25+
</journey>

0 commit comments

Comments
 (0)