NotificationPermissionEffect() was invoked at the top of the Compose tree in MainActivity.onCreate's setContent (sibling of AppLockGateHost/LibreMailApp). Its LaunchedEffect(Unit) fired on the very first composition, so the system POST_NOTIFICATIONS dialog could appear the instant the app icon was tapped — overlapping the cold-start/splash transition, before any onboarding context was on screen.
Fix
Moved the effect out of MainActivity and into OnboardingWelcomeScreen (the onboarding start destination), calling NotificationPermissionEffect() at the top of that composable. The request now fires once the welcome screen is composed and visible, with the welcome content behind the dialog.
Approach: scoped effect (not a NavHost-destination signal) — the welcome screen already owns "I am visible" via its composition, so the effect lives where the context does and stays entirely self-contained.
Already-onboarded users unaffected: they launch straight into the mailbox and never compose the welcome screen, so the request never runs for them. No per-launch re-prompt is introduced.
Preserved unchanged: the API 33+ (TIRAMISU) gate and the already-granted checkSelfPermission no-op are carried over verbatim; the effect body is identical, only its host moved.
Updated docs/play-permissions.md to point the request-flow reference at the new location.
Validation
Local gate passed with JDK 21: :app:assembleDebug :app:testDebugUnitTest :app:lintDebug :app:ktlintCheck :app:detekt :app:compileDebugAndroidTestKotlin — BUILD SUCCESSFUL. No existing androidTest asserted the old timing, so none needed updating.
## Root cause
`NotificationPermissionEffect()` was invoked at the top of the Compose tree in `MainActivity.onCreate`'s `setContent` (sibling of `AppLockGateHost`/`LibreMailApp`). Its `LaunchedEffect(Unit)` fired on the very first composition, so the system POST_NOTIFICATIONS dialog could appear the instant the app icon was tapped — overlapping the cold-start/splash transition, before any onboarding context was on screen.
## Fix
Moved the effect out of `MainActivity` and into `OnboardingWelcomeScreen` (the onboarding start destination), calling `NotificationPermissionEffect()` at the top of that composable. The request now fires once the welcome screen is composed and visible, with the welcome content behind the dialog.
- **Approach:** scoped effect (not a NavHost-destination signal) — the welcome screen already owns "I am visible" via its composition, so the effect lives where the context does and stays entirely self-contained.
- **Already-onboarded users unaffected:** they launch straight into the mailbox and never compose the welcome screen, so the request never runs for them. No per-launch re-prompt is introduced.
- **Preserved unchanged:** the API 33+ (`TIRAMISU`) gate and the already-granted `checkSelfPermission` no-op are carried over verbatim; the effect body is identical, only its host moved.
- Updated `docs/play-permissions.md` to point the request-flow reference at the new location.
## Validation
Local gate passed with JDK 21: `:app:assembleDebug :app:testDebugUnitTest :app:lintDebug :app:ktlintCheck :app:detekt :app:compileDebugAndroidTestKotlin` — BUILD SUCCESSFUL. No existing androidTest asserted the old timing, so none needed updating.
Closes #151
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Root cause
NotificationPermissionEffect()was invoked at the top of the Compose tree inMainActivity.onCreate'ssetContent(sibling ofAppLockGateHost/LibreMailApp). ItsLaunchedEffect(Unit)fired on the very first composition, so the system POST_NOTIFICATIONS dialog could appear the instant the app icon was tapped — overlapping the cold-start/splash transition, before any onboarding context was on screen.Fix
Moved the effect out of
MainActivityand intoOnboardingWelcomeScreen(the onboarding start destination), callingNotificationPermissionEffect()at the top of that composable. The request now fires once the welcome screen is composed and visible, with the welcome content behind the dialog.TIRAMISU) gate and the already-grantedcheckSelfPermissionno-op are carried over verbatim; the effect body is identical, only its host moved.docs/play-permissions.mdto point the request-flow reference at the new location.Validation
Local gate passed with JDK 21:
:app:assembleDebug :app:testDebugUnitTest :app:lintDebug :app:ktlintCheck :app:detekt :app:compileDebugAndroidTestKotlin— BUILD SUCCESSFUL. No existing androidTest asserted the old timing, so none needed updating.Closes #151
🤖 Generated with Claude Code