fix(onboarding): enforce GPL license gate for upgrade users #202

Closed
JMR-dev wants to merge 1 commits from fix-172-license-gate-upgrade-users into main
JMR-dev commented 2026-07-03 08:19:08 +00:00 (Migrated from github.com)

Finding (from the post-batch security review, refs #172)

Low severity. The GPL-3.0 license gate added in #172 is skipped for upgrade users. The license screen (ONBOARDING_LICENSE) is only the onboarding graph's start destination, but AppViewModel.startDestination chose mailbox-vs-onboarding purely by account count. So a user upgrading from a pre-#172 install (accounts present, licenseAccepted = false) went straight to MAILBOX and never saw the license — contradicting #172's own intent (the AppSettingsTest comment: pre-#172 installs should route through ONBOARDING_LICENSE).

Separately, the pendingCompose mailto/share deep-link navigated unconditionally, unlike pendingOpenMessageId which already guards on start != Routes.ONBOARDING — so a deep-link could also jump past the gate.

Fix

  1. AppViewModel.startDestination now combines account presence with the (already-resolved from #172) licenseAccepted: it returns ONBOARDING whenever the license is unaccepted, even when accounts exist. Only a user who has both accepted the license and has an account lands on MAILBOX. Same hold-until-known + take(1) "resolve once at launch" contract as before.
  2. pendingCompose now carries the same if (start != Routes.ONBOARDING) guard as pendingOpenMessageId (still consumes the request either way so it isn't replayed).
  3. Post-accept routing (necessary corollary of #1). Forcing upgrade users into the license flow exposed a dead-end: LicenseScreen's onAgree sent everyone to ONBOARDING_WELCOME ("add your first account"), which has no path forward for a user who already has accounts. AppViewModel now also exposes hasAccounts, and onboardingGraph's onAgree uses it — an upgrade user (has accounts) goes straight to their inbox via the existing finishOnboarding(null) (which pops the whole onboarding graph, leaving the one-time gate behind); a fresh install (no accounts) continues into the welcome/add-account flow exactly as before. Without this, the fix would trade "skips the license" for "trapped after accepting it."

Tests

AppViewModelTest updated/extended to cover the full 2x2 of (accounts × licenseAccepted):

  • no accounts + either license state → ONBOARDING
  • accounts + accepted → MAILBOX
  • accounts + unaccepted → ONBOARDING (the regression this fixes — the upgrade license gate)
  • hasAccounts reflects account presence

The post-accept nav wiring (agree → mailbox for upgrade users) is graph-level; it's left to CI's E2E rather than an emulator run here.

Preflight (JDK 21, --max-workers=8, no emulator) — all green

  • :app:assembleDebug
  • :app:testDebugUnitTest (AppViewModelTest: 6 pass)
  • :app:lintDebug
  • :app:ktlintCheck :app:detekt
  • :app:compileDebugAndroidTestKotlin

🤖 Generated with Claude Code

## Finding (from the post-batch security review, refs #172) **Low severity.** The GPL-3.0 license gate added in #172 is *skipped for upgrade users*. The license screen (`ONBOARDING_LICENSE`) is only the onboarding graph's start destination, but `AppViewModel.startDestination` chose mailbox-vs-onboarding purely by **account count**. So a user upgrading from a pre-#172 install (accounts present, `licenseAccepted = false`) went straight to `MAILBOX` and never saw the license — contradicting #172's own intent (the `AppSettingsTest` comment: pre-#172 installs should route through `ONBOARDING_LICENSE`). Separately, the `pendingCompose` mailto/share deep-link navigated unconditionally, unlike `pendingOpenMessageId` which already guards on `start != Routes.ONBOARDING` — so a deep-link could also jump past the gate. ## Fix 1. **`AppViewModel.startDestination`** now `combine`s account presence with the (already-resolved from #172) `licenseAccepted`: it returns `ONBOARDING` whenever the license is unaccepted, even when accounts exist. Only a user who has **both** accepted the license **and** has an account lands on `MAILBOX`. Same hold-until-known + `take(1)` "resolve once at launch" contract as before. 2. **`pendingCompose`** now carries the same `if (start != Routes.ONBOARDING)` guard as `pendingOpenMessageId` (still consumes the request either way so it isn't replayed). 3. **Post-accept routing (necessary corollary of #1).** Forcing upgrade users into the license flow exposed a dead-end: `LicenseScreen`'s `onAgree` sent *everyone* to `ONBOARDING_WELCOME` ("add your first account"), which has no path forward for a user who already has accounts. `AppViewModel` now also exposes `hasAccounts`, and `onboardingGraph`'s `onAgree` uses it — an upgrade user (has accounts) goes straight to their inbox via the existing `finishOnboarding(null)` (which pops the whole onboarding graph, leaving the one-time gate behind); a fresh install (no accounts) continues into the welcome/add-account flow exactly as before. Without this, the fix would trade "skips the license" for "trapped after accepting it." ## Tests `AppViewModelTest` updated/extended to cover the full 2x2 of (accounts × licenseAccepted): - no accounts + either license state → `ONBOARDING` - accounts + accepted → `MAILBOX` - **accounts + unaccepted → `ONBOARDING`** (the regression this fixes — the upgrade license gate) - `hasAccounts` reflects account presence The post-accept nav wiring (agree → mailbox for upgrade users) is graph-level; it's left to CI's E2E rather than an emulator run here. ## Preflight (JDK 21, `--max-workers=8`, no emulator) — all green - `:app:assembleDebug` - `:app:testDebugUnitTest` (AppViewModelTest: 6 pass) - `:app:lintDebug` - `:app:ktlintCheck :app:detekt` - `:app:compileDebugAndroidTestKotlin` 🤖 Generated with [Claude Code](https://claude.com/claude-code)

Pull request closed

Please reopen this pull request to perform a merge.
Sign in to join this conversation.