The privacy promise and policy in Settings #37

Closed
opened 2026-08-18 20:37:16 -05:00 by null · 1 comment
Owner

The promise is made once during onboarding and never again.

What is true now

Onboarding step 5 shows it — PrivacyPromise in app/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

  • One copy of the wording, not two. The onboarding screen already has it, and a second literal string is how two versions of a promise come to exist — the exact failure DOC_TRUST_MAP.md exists to prevent, in code rather than prose. Extract it to a string resource both screens read.
  • The wording is deliberate and load-bearing: we never sell your data. It does not claim no third party ever processes anything, because Play Billing and an ad SDK eventually will, and a promise the implementation cannot keep is worse than a narrower one that holds. Do not "strengthen" it.
  • A Privacy Policy row needs somewhere to point. If the hosted page does not exist yet, the row waits — a policy link that 404s is worse than no link.

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.

The promise is made once during onboarding and never again. ## What is true now Onboarding step 5 shows it — `PrivacyPromise` in `app/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 - **One copy of the wording, not two.** The onboarding screen already has it, and a second literal string is how two versions of a promise come to exist — the exact failure `DOC_TRUST_MAP.md` exists to prevent, in code rather than prose. Extract it to a string resource both screens read. - **The wording is deliberate and load-bearing**: we never *sell* your data. It does not claim no third party ever processes anything, because Play Billing and an ad SDK eventually will, and a promise the implementation cannot keep is worse than a narrower one that holds. Do not "strengthen" it. - A Privacy Policy row needs somewhere to point. If the hosted page does not exist yet, the row waits — a policy link that 404s is worse than no link. ## 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.
null added this to the Batch 06 — Privacy and Security milestone 2026-08-18 20:37:16 -05:00
null added the
P2
label 2026-08-18 20:37:16 -05:00
null closed this issue 2026-08-19 22:00:10 -05:00
Author
Owner

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_title and privacy_promise_body in strings.xml; onboarding's PrivacyPromise reads 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 SettingsScreen already 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

  • the promise exists and says the thing it is for
  • the promise does not claim more than the app can keep — fails if it gains "no third party", "never shared", "end-to-end" or similar. Play Billing and an ad SDK eventually process something, and a promise the implementation cannot keep is worse than a narrower one that holds. This is the trap the issue names, made mechanical.
  • the promise appears as a literal in no Kotlin source — comments stripped first per GUARDS.md §2, or the KDoc explaining the rule would fail it

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 :app could read a string resource. core/database and core/data have carried unitTests.isIncludeAndroidResources since they were written; :app never did. The module owning almost all of the user-facing copy was the one module whose copy could not be tested — every getString() threw NotFoundException with an id that had resolved perfectly well. Fixed.

2. checkPermissions was reading manifests that do not ship. Turning on the above made AGP write merged_manifest/debugUnitTest/, the guard walked the whole tree, and the build failed on REORDER_TASKS — a test-runner permission no user ever sees. Verified the shipping manifests are clean:

debug  : ACCESS_NETWORK_STATE FOREGROUND_SERVICE POST_NOTIFICATIONS
         RECEIVE_BOOT_COMPLETED USE_BIOMETRIC USE_FINGERPRINT WAKE_LOCK
release: (identical)
REORDER_TASKS: 0 occurrences in either

The tempting fix — allowlisting it — would have permitted it in the real manifest too and quietly undone the guard. It now reads only debug and release, 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: a PROVE_GUARD_FAIL_PATTERN matching 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.

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_title` and `privacy_promise_body` in `strings.xml`; onboarding's `PrivacyPromise` reads 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 `SettingsScreen` already 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 - the promise exists and says the thing it is for - **the promise does not claim more than the app can keep** — fails if it gains "no third party", "never shared", "end-to-end" or similar. Play Billing and an ad SDK eventually process something, and a promise the implementation cannot keep is worse than a narrower one that holds. This is the trap the issue names, made mechanical. - the promise appears as a literal in no Kotlin source — comments stripped first per GUARDS.md §2, or the KDoc explaining the rule would fail it **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 `:app` could read a string resource.** `core/database` and `core/data` have carried `unitTests.isIncludeAndroidResources` since they were written; `:app` never did. The module owning almost all of the user-facing copy was the one module whose copy could not be tested — every `getString()` threw `NotFoundException` with an id that had resolved perfectly well. Fixed. **2. `checkPermissions` was reading manifests that do not ship.** Turning on the above made AGP write `merged_manifest/debugUnitTest/`, the guard walked the whole tree, and the build failed on `REORDER_TASKS` — a test-runner permission no user ever sees. Verified the shipping manifests are clean: ``` debug : ACCESS_NETWORK_STATE FOREGROUND_SERVICE POST_NOTIFICATIONS RECEIVE_BOOT_COMPLETED USE_BIOMETRIC USE_FINGERPRINT WAKE_LOCK release: (identical) REORDER_TASKS: 0 occurrences in either ``` The tempting fix — allowlisting it — would have permitted it in the **real** manifest too and quietly undone the guard. It now reads only `debug` and `release`, 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: a `PROVE_GUARD_FAIL_PATTERN` matching **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.
Sign in to join this conversation.
No Label
P0
P1
P2
release-blocker
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: null/Privacy-Period-Tracker#37
No description provided.