The privacy promise and policy in Settings #37
Labels
No Label
P0
P1
P2
release-blocker
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: null/Privacy-Period-Tracker#37
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
The promise is made once during onboarding and never again.
What is true now
Onboarding step 5 shows it —
PrivacyPromiseinapp/src/main/kotlin/dev/privacyllc/period/feature/onboarding/OnboardingScreen.kt, with its own illustration. After that first run it is unreachable. §4 requires the promise in onboarding, in Settings, and on a public privacy page; the milestone names "the privacy promise in onboarding and settings".What it costs
A promise a user can only see before they have entered any data is a marketing line. Seen again later, next to Export and Delete, it is a statement they can hold the product to — which is the entire point of putting it in Settings.
What to do
The promise text under §36's Privacy & Security, and a Privacy Policy row.
Traps
DOC_TRUST_MAP.mdexists to prevent, in code rather than prose. Extract it to a string resource both screens read.Why filed and not fixed
Needs the Settings screen, and the policy URL is not something an agent can invent.
Verify: The promise appears in Settings with wording identical to onboarding's because both read one string resource, and the Privacy Policy row either opens a real page or is absent.
Done in
4456f35.The promise now appears under Settings → Privacy & Security, immediately above App lock and Delete my data — so the statement and the controls that act on the data are on one screen.
One copy.
privacy_promise_titleandprivacy_promise_bodyinstrings.xml; onboarding'sPrivacyPromisereads the same two resources. The comment beside them records why the wording is narrow, so the next person to "improve" it meets the reason first.No Privacy Policy row, which this issue's verify line explicitly allows: "the Privacy Policy row either opens a real page or is absent." No hosted page exists, a link that 404s is worse than no link, and it matches the convention
SettingsScreenalready states — a row for something unbuilt is absent, not disabled. That row lands with the page.Three tests, and the second is the one worth having
Proved: replacing the resource lookup in onboarding with the literal fails exactly one test, prove-guard exit 0.
Two pre-existing defects found on the way
1. No Robolectric test in
:appcould read a string resource.core/databaseandcore/datahave carriedunitTests.isIncludeAndroidResourcessince they were written;:appnever did. The module owning almost all of the user-facing copy was the one module whose copy could not be tested — everygetString()threwNotFoundExceptionwith an id that had resolved perfectly well. Fixed.2.
checkPermissionswas reading manifests that do not ship. Turning on the above made AGP writemerged_manifest/debugUnitTest/, the guard walked the whole tree, and the build failed onREORDER_TASKS— a test-runner permission no user ever sees. Verified the shipping manifests are clean:The tempting fix — allowlisting it — would have permitted it in the real manifest too and quietly undone the guard. It now reads only
debugandrelease, and refuses to pass unless it read both: checking debug while release went unread is the failure that matters, since the Play listing and the Data Safety form describe the release manifest.Strictly stricter than before, and proved twice: a forbidden permission in the app manifest still fails it (
FORBIDDEN permission … SCHEDULE_EXACT_ALARM), and a missing release manifest now fails with "expected a merged manifest for each of [debug, release] but read [debug] … This is not a pass" where it previously passed.GUARDS.md§8 also gains a third prove-guard edge found while proving that: aPROVE_GUARD_FAIL_PATTERNmatching nothing produces the same "caught it, and only it" verdict as one matching exactly once, since the script only refuses on more than one. The empty--- what failed ---block is the tell.