The onboarding graph (ui/LibreMailApp.kt, onboardingGraph()) currently starts at Routes.ONBOARDING_WELCOME (navigation(startDestination = Routes.ONBOARDING_WELCOME, route = Routes.ONBOARDING)), shown whenever AppViewModel.startDestination sees no accounts. This
ticket inserts a new screen ahead of it: the user must see the GPL-3.0 license and tap Agree before reaching the welcome screen (or anything else in the app), or Decline to
exit without using the app.
The repo already has the full license text at LICENSE (674 lines, standard GPLv3) — nothing is
currently bundled for in-app display, so that text needs a runtime-accessible copy.
This directly changes the sequencing #151 depends on: #151 moves the notification-permission
request to fire once "the onboarding flow's first screen (OnboardingWelcomeScreen)" has
appeared. Once this ticket lands, the welcome screen is the second screen, not the first —
the notification prompt should fire when that (now second) screen loads, per this ticket's own
spec ("Notifications ask comes once the next screen loads"). #151's wording will need a one-line
correction to stop calling the welcome screen "the first screen" once this merges — flagging here
so it isn't missed; not fixing it in this ticket since #151 is a separate, already-scoped unit of
work.
Scope
Add Routes.ONBOARDING_LICENSE and make it the onboardingGraph's startDestination
(ahead of Routes.ONBOARDING_WELCOME).
New ui/onboarding/LicenseScreen.kt: shows the full GPL-3.0 text (scrollable) with Agree and Decline buttons. Bundle the text for runtime display — e.g. res/raw/license.txt or an asset — sourced from the root LICENSE file. Risk to flag in
the PR: this creates two copies of the license text that can drift out of sync; either
wire a build step that copies LICENSE into the bundled resource at build time, or leave a
clear comment on both files pointing at each other so a manual license update isn't missed.
Agree is disabled until the user has scrolled to the end of the license text. Use the
same rememberScrollState() + Modifier.verticalScroll pattern already used elsewhere in
this codebase (e.g. AppPasswordSetupScreen.kt, ManualSetupScreen.kt, ReportReviewScreen.kt) rather than introducing a new list-based pattern just for this
screen. Gate on scrollState.value >= scrollState.maxValue: this also correctly
self-resolves the "whole license fits on one large screen, no scrolling needed" case, since maxValue is 0 when nothing needs scrolling and 0 >= 0 is trivially true — no special
casing required.
Decline exits the app.MainActivity is the app's only activity (a single-activity
Compose app), so finish() should suffice; use finishAffinity() instead if that ever
proves insufficient (e.g. if a launched sub-activity, like the AppAuth/OAuth redirect
activity, is still on the back stack). No confirmation dialog specified — a direct exit on
tap is the simplest reading of the request; add one only if it turns out to be needed.
Persist acceptance (e.g. SettingsRepository.licenseAccepted: Boolean, following the
existing key/AppSettings-field/setter pattern). This is necessary, not optional: since AppViewModel.startDestination sends any account-less launch back into Routes.ONBOARDING,
without a persisted flag a user who agrees but then exits before adding an account (or the
process dies mid-onboarding) would be forced to agree again on relaunch. Gate onboardingGraph's start destination on this flag (already-accepted skips straight to ONBOARDING_WELCOME).
Hard-gate navigation: no way to reach ONBOARDING_WELCOME without tapping Agree — no back
gesture/button skip (there's nothing to go back to, since this is now first), and no way
around the scroll-to-end requirement.
Remove/relocate the current NotificationPermissionEffect() trigger point to align with #151's fix once both land: the ask fires when OnboardingWelcomeScreen (now the second
screen) loads, never on this new license screen.
Acceptance criteria
On a fresh install, the GPL-3.0 license screen is the very first thing shown — before the
welcome screen, before any permission prompt.
Agree stays disabled until the user has scrolled to the end of the license text.
Tapping Decline exits the app without navigating anywhere else.
The app cannot be navigated past this screen any other way.
The notification-permission prompt is not shown on the license screen; it appears once the next
screen (welcome) has loaded (coordinates with #151).
Relaunching after agreeing, but before an account has been added, does not show the license
screen again.
Sequencing note for #151 (notification prompt timing) — see Context above.
## Context
The onboarding graph (`ui/LibreMailApp.kt`, `onboardingGraph()`) currently starts at
`Routes.ONBOARDING_WELCOME` (`navigation(startDestination = Routes.ONBOARDING_WELCOME, route =
Routes.ONBOARDING)`), shown whenever `AppViewModel.startDestination` sees no accounts. This
ticket inserts a new screen **ahead of it**: the user must see the GPL-3.0 license and tap
**Agree** before reaching the welcome screen (or anything else in the app), or **Decline** to
exit without using the app.
The repo already has the full license text at `LICENSE` (674 lines, standard GPLv3) — nothing is
currently bundled for in-app display, so that text needs a runtime-accessible copy.
This directly changes the sequencing #151 depends on: #151 moves the notification-permission
request to fire once "the onboarding flow's first screen (`OnboardingWelcomeScreen`)" has
appeared. Once this ticket lands, the welcome screen is the **second** screen, not the first —
the notification prompt should fire when *that* (now second) screen loads, per this ticket's own
spec ("Notifications ask comes once the next screen loads"). #151's wording will need a one-line
correction to stop calling the welcome screen "the first screen" once this merges — flagging here
so it isn't missed; not fixing it in this ticket since #151 is a separate, already-scoped unit of
work.
## Scope
- [ ] Add `Routes.ONBOARDING_LICENSE` and make it the `onboardingGraph`'s `startDestination`
(ahead of `Routes.ONBOARDING_WELCOME`).
- [ ] New `ui/onboarding/LicenseScreen.kt`: shows the full GPL-3.0 text (scrollable) with
**Agree** and **Decline** buttons. Bundle the text for runtime display — e.g.
`res/raw/license.txt` or an asset — sourced from the root `LICENSE` file. **Risk to flag in
the PR**: this creates two copies of the license text that can drift out of sync; either
wire a build step that copies `LICENSE` into the bundled resource at build time, or leave a
clear comment on both files pointing at each other so a manual license update isn't missed.
- [ ] **Agree is disabled until the user has scrolled to the end of the license text.** Use the
same `rememberScrollState()` + `Modifier.verticalScroll` pattern already used elsewhere in
this codebase (e.g. `AppPasswordSetupScreen.kt`, `ManualSetupScreen.kt`,
`ReportReviewScreen.kt`) rather than introducing a new list-based pattern just for this
screen. Gate on `scrollState.value >= scrollState.maxValue`: this also correctly
self-resolves the "whole license fits on one large screen, no scrolling needed" case, since
`maxValue` is `0` when nothing needs scrolling and `0 >= 0` is trivially true — no special
casing required.
- [ ] **Decline exits the app.** `MainActivity` is the app's only activity (a single-activity
Compose app), so `finish()` should suffice; use `finishAffinity()` instead if that ever
proves insufficient (e.g. if a launched sub-activity, like the AppAuth/OAuth redirect
activity, is still on the back stack). No confirmation dialog specified — a direct exit on
tap is the simplest reading of the request; add one only if it turns out to be needed.
- [ ] Persist acceptance (e.g. `SettingsRepository.licenseAccepted: Boolean`, following the
existing key/`AppSettings`-field/setter pattern). This is necessary, not optional: since
`AppViewModel.startDestination` sends *any* account-less launch back into `Routes.ONBOARDING`,
without a persisted flag a user who agrees but then exits before adding an account (or the
process dies mid-onboarding) would be forced to agree again on relaunch. Gate
`onboardingGraph`'s start destination on this flag (already-accepted skips straight to
`ONBOARDING_WELCOME`).
- [ ] Hard-gate navigation: no way to reach `ONBOARDING_WELCOME` without tapping Agree — no back
gesture/button skip (there's nothing to go back to, since this is now first), and no way
around the scroll-to-end requirement.
- [ ] Remove/relocate the current `NotificationPermissionEffect()` trigger point to align with
#151's fix once both land: the ask fires when `OnboardingWelcomeScreen` (now the second
screen) loads, never on this new license screen.
## Acceptance criteria
- On a fresh install, the GPL-3.0 license screen is the very first thing shown — before the
welcome screen, before any permission prompt.
- Agree stays disabled until the user has scrolled to the end of the license text.
- Tapping Decline exits the app without navigating anywhere else.
- The app cannot be navigated past this screen any other way.
- The notification-permission prompt is not shown on the license screen; it appears once the next
screen (welcome) has loaded (coordinates with #151).
- Relaunching after agreeing, but before an account has been added, does not show the license
screen again.
## Relevant files
- `ui/LibreMailApp.kt` (`onboardingGraph`, ~lines 290-294), `ui/navigation/Routes.kt`,
`ui/AppViewModel.kt`, `data/settings/SettingsRepository.kt`, root `LICENSE`,
new `ui/onboarding/LicenseScreen.kt`.
## Dependencies
Sequencing note for #151 (notification prompt timing) — see Context above.
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.
Context
The onboarding graph (
ui/LibreMailApp.kt,onboardingGraph()) currently starts atRoutes.ONBOARDING_WELCOME(navigation(startDestination = Routes.ONBOARDING_WELCOME, route = Routes.ONBOARDING)), shown wheneverAppViewModel.startDestinationsees no accounts. Thisticket inserts a new screen ahead of it: the user must see the GPL-3.0 license and tap
Agree before reaching the welcome screen (or anything else in the app), or Decline to
exit without using the app.
The repo already has the full license text at
LICENSE(674 lines, standard GPLv3) — nothing iscurrently bundled for in-app display, so that text needs a runtime-accessible copy.
This directly changes the sequencing #151 depends on: #151 moves the notification-permission
request to fire once "the onboarding flow's first screen (
OnboardingWelcomeScreen)" hasappeared. Once this ticket lands, the welcome screen is the second screen, not the first —
the notification prompt should fire when that (now second) screen loads, per this ticket's own
spec ("Notifications ask comes once the next screen loads"). #151's wording will need a one-line
correction to stop calling the welcome screen "the first screen" once this merges — flagging here
so it isn't missed; not fixing it in this ticket since #151 is a separate, already-scoped unit of
work.
Scope
Routes.ONBOARDING_LICENSEand make it theonboardingGraph'sstartDestination(ahead of
Routes.ONBOARDING_WELCOME).ui/onboarding/LicenseScreen.kt: shows the full GPL-3.0 text (scrollable) withAgree and Decline buttons. Bundle the text for runtime display — e.g.
res/raw/license.txtor an asset — sourced from the rootLICENSEfile. Risk to flag inthe PR: this creates two copies of the license text that can drift out of sync; either
wire a build step that copies
LICENSEinto the bundled resource at build time, or leave aclear comment on both files pointing at each other so a manual license update isn't missed.
same
rememberScrollState()+Modifier.verticalScrollpattern already used elsewhere inthis codebase (e.g.
AppPasswordSetupScreen.kt,ManualSetupScreen.kt,ReportReviewScreen.kt) rather than introducing a new list-based pattern just for thisscreen. Gate on
scrollState.value >= scrollState.maxValue: this also correctlyself-resolves the "whole license fits on one large screen, no scrolling needed" case, since
maxValueis0when nothing needs scrolling and0 >= 0is trivially true — no specialcasing required.
MainActivityis the app's only activity (a single-activityCompose app), so
finish()should suffice; usefinishAffinity()instead if that everproves insufficient (e.g. if a launched sub-activity, like the AppAuth/OAuth redirect
activity, is still on the back stack). No confirmation dialog specified — a direct exit on
tap is the simplest reading of the request; add one only if it turns out to be needed.
SettingsRepository.licenseAccepted: Boolean, following theexisting key/
AppSettings-field/setter pattern). This is necessary, not optional: sinceAppViewModel.startDestinationsends any account-less launch back intoRoutes.ONBOARDING,without a persisted flag a user who agrees but then exits before adding an account (or the
process dies mid-onboarding) would be forced to agree again on relaunch. Gate
onboardingGraph's start destination on this flag (already-accepted skips straight toONBOARDING_WELCOME).ONBOARDING_WELCOMEwithout tapping Agree — no backgesture/button skip (there's nothing to go back to, since this is now first), and no way
around the scroll-to-end requirement.
NotificationPermissionEffect()trigger point to align with#151's fix once both land: the ask fires when
OnboardingWelcomeScreen(now the secondscreen) loads, never on this new license screen.
Acceptance criteria
welcome screen, before any permission prompt.
screen (welcome) has loaded (coordinates with #151).
screen again.
Relevant files
ui/LibreMailApp.kt(onboardingGraph, ~lines 290-294),ui/navigation/Routes.kt,ui/AppViewModel.kt,data/settings/SettingsRepository.kt, rootLICENSE,new
ui/onboarding/LicenseScreen.kt.Dependencies
Sequencing note for #151 (notification prompt timing) — see Context above.