Skip to content

fix: drive the notification prompt from live permission state - #147

Merged
jvsena42 merged 2 commits into
mainfrom
fix/notification-permission-flow
Jul 30, 2026
Merged

fix: drive the notification prompt from live permission state#147
jvsena42 merged 2 commits into
mainfrom
fix/notification-permission-flow

Conversation

@jvsena42

Copy link
Copy Markdown
Owner

Summary

The "Enable notifications" snackbar never went away once the permission was granted through the system dialog.

MainActivity computed hasNotificationPermission once, eagerly, while building the setContent argument list — a plain Boolean reading no snapshot state, so it never recomposed. The snackbar's LaunchedEffect was keyed on that frozen value, and Material3's showSnackbar defaults to an indefinite duration when an actionLabel is present, so nothing ever cancelled the suspended call.

Reproduced on main: POST_NOTIFICATIONS: granted=true with the snackbar still on screen 26s later.

The fix replaces three uncoordinated call sites (MainActivity.onCreate, MainScreen, and a second competing requester in ScreenNode) with one state holder, rememberNotificationPermissionState(), and renders the prompt as plain state-driven UI instead of showSnackbar — the pattern AddDescriptorSnackbar already uses, whose existing comment explains why showSnackbar is wrong for persistent prompts. Visibility is now a function of the permission, so a grant retracts the prompt with no coroutine to cancel.

Other gaps found in the same flow

  • The node died when notifications were declined. Service start was gated on the permission callback, so denying left FlorestaService never started — no sync, and no Electrum server for connected wallets. Confirmed on main: network/peers/difficulty stuck at , 0 peers. A dataSync foreground service starts fine without POST_NOTIFICATIONS; only its notification is withheld.
  • The dialog fired before any UI existed, so the snackbar rendered underneath it — and ScreenNode fired a second, competing request whose result was discarded. Nothing opens the dialog now except the user tapping the action.
  • "Enable" dead-ended after Android's automatic "don't ask again": the request returned denied instantly with no dialog. It now switches to "Open settings". On Android 12 and below, where there is no runtime permission, it starts in that mode.
  • After a denial the message escalates to warn that wallets lose access to the Electrum server.
  • Dismissal survives rotation (rememberSaveable); the descriptor prompt is held back while this one shows so the two cannot overlap; strings moved to strings.xml; dead Notifications.kt and unused helper functions removed.

Verified on device (emulator, x86_64, Android 16 / API 36)

No dialog on launch permission_allow_button absent
Tap Enable → Allow granted=true, snackbar_enable_notifications gone, same pid
Grant from Settings prompt cleared in the same process (pid unchanged), exercising the ON_RESUME re-check
Deny ×2 action becomes "Open settings", flags=[USER_SET|USER_FIXED]
Node while denied isForeground=true, progress 0.998945 → 0.998949
Electrum while denied server.version["Floresta 0.5.1","1.4"], blockchain.headers.subscribe → height 960288

Note on the denial copy: as measured above, the node and Electrum server do keep working with the permission refused — that chain was accurate against the pre-fix code, where the service was gated. The warning is deliberately worded as a caution (an invisible foreground service is likelier to be reaped by OEM battery management), not as a description of current behavior.

Not verified: the API 29–32 path is reasoned from the code, not executed — only API 35/36 system images are installed locally.

Checklist

  • Commits are atomic and follow conventional style (feat:, fix:, chore:, ...)
  • Unit tests added/updated for business-logic changes — covered by the two new journeys instead; the logic is a Compose state holder driven by system permission callbacks
  • Journeys and journeys/README.md testTags updated for UI changes
  • ./gradlew detekt, ./gradlew lintDebug, and ./gradlew test pass locally
  • I have self-reviewed my own diff

Preview

New snackbar_enable_notifications, shown above the bottom nav on every tab:

  1. Initial — "Enable notifications to see when the node is running" · action Enable · ✕
  2. After a denial — "Notifications are disabled, wallets can't access your Electrum server" · action Enable · ✕
  3. After Android applies "don't ask again" — same message · action Open settings · ✕

Screenshots of all three states were captured locally at shots/1-initial-prompt.png, 2-after-denial.png, 3-open-settings.png — attaching images isn't possible from the CLI, so drop them in if you want them inline.

🤖 Generated with Claude Code

jvsena42 and others added 2 commits July 30, 2026 15:18
The "Enable notifications" snackbar never went away once the permission was
granted. MainActivity computed hasNotificationPermission once, eagerly, while
building the setContent argument list: a plain Boolean reading no snapshot
state, so it never recomposed. The snackbar's LaunchedEffect was keyed on that
frozen value, and Material3's showSnackbar defaults to an indefinite duration
when an actionLabel is present, so nothing ever cancelled the suspended call.

Replace the three uncoordinated call sites with a single state holder,
rememberNotificationPermissionState(), and render the prompt as plain
state-driven UI rather than through showSnackbar — the pattern already used by
AddDescriptorSnackbar. Visibility is now a function of the permission, so a
grant retracts the prompt with no coroutine to cancel.

Also fixed in the same flow:

- The node no longer dies when notifications are declined. Service start was
  gated on the permission callback, so denying left FlorestaService never
  started: no sync, and no Electrum server for connected wallets. A dataSync
  foreground service starts fine without POST_NOTIFICATIONS; only its
  notification is withheld.
- No dialog is opened on launch any more. MainActivity fired one before
  setContent (so the snackbar rendered underneath it) and ScreenNode fired a
  second, competing one whose result was discarded. The dialog now opens only
  when the user taps the action.
- The action no longer dead-ends after Android's automatic "don't ask again":
  it switches to "Open settings" and opens the app's settings page. On
  Android 12 and below, where there is no runtime permission, it starts in
  that mode.
- After a denial the message escalates to warn that wallets lose access to the
  Electrum server.
- Prompt dismissal survives rotation (rememberSaveable), and the descriptor
  prompt is held back while this one shows so the two cannot overlap.
- Strings moved to strings.xml; dead Notifications.kt and the now-unused
  helper functions removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Nothing in journeys/ touched notifications, so the snackbar regression had no
guard. Add two journeys and document the snackbar_enable_notifications tag.

enable_notifications_snackbar.xml is the regression repro: no dialog on launch,
tap Enable, grant, and assert the prompt retracts itself without being
dismissed. notification_permission_denied.xml covers the denial path, including
that the node keeps running and serving Electrum with the permission refused,
and that the action escalates to "Open settings" after Android applies
"don't ask again".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jvsena42
jvsena42 enabled auto-merge (rebase) July 30, 2026 18:20
@jvsena42
jvsena42 merged commit f0eb765 into main Jul 30, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant