Closer/Future.md

275 lines
28 KiB
Markdown
Raw Normal View History

# Future — ideas & improvements backlog
Non-blocking ideas: things that work today but could be better, plus feature ideas. Actual bugs
(broken/incorrect behavior) live in `ClaudeReport.md`, not here.
## Backup, restore & E2EE (follow-ons to R24)
- **Option B — relay-and-delete for messages.** Now that E2EE backup + restore exist (R24), make the Room
`conversation_cache` the **source of truth** and flip Firestore `messages` to a transient relay
(TTL / delete-after-delivery), plus **back up media into the snapshot** (since `chat_media` would then be
relay-deleted). This is the payoff the R24 backup unblocks. Rewire the chat read to local-first + live-merge
(additive, flag-guarded — dedupe RoomFirestore by message id).
- **Backup trigger hardening.** Backups currently run opportunistically from `HomeViewModel.loadHome` (throttled).
Move to **WorkManager** (deferred, retried, network/battery-constrained) + an app-background trigger for
reliability; add a **resumable, paginated initial backfill** for very large existing histories.
- **Settings visibility + control.** "Last backed up: {time} · {N} messages" indicator + manual "Back up now" /
"Restore history", and an **opt-out** toggle for users who want zero server retention. Include a dedicated
**"Recent restore activity"** list — the R24-b `restore_self_alert` entries already land in `notification_queue`
as the raw audit; this surfaces them so the owner can review restores on their account in one place.
- **Email-verification challenge for partner-assisted restore (strongest anti-account-takeover control).** Before
a restore request is honored, send a code to the account's **registered email** and require the recipient to
enter it. A phished-password attacker who lacks inbox access can't complete it (the couple-email match that
defeats the on-screen identity check does NOT defeat this). Needs mail infra (SendGrid / a Firebase Auth action)
— deferred for that reason. R24-b shipped the on-screen identity + confirm + owner self-alert as the pragmatic
interim.
- **Restore-request lifecycle cleanup.** A `restore_requests` doc left at `READY` (partner wrapped the key but the
recipient never completed) leaves an ECIES `keybox` — ciphertext sealed only to the recipient, useless to
anyone else, but untidy. Add a **scheduled cleanup** of expired requests (and their keyboxes). R24-b already
enforces expiry at fulfil time and deletes any stale request before a re-request.
- **Owner-alert precision.** The R24-b "was this you?" self-alert also reaches the *requesting* device (harmless).
Optionally exclude it via a client-written token hint on the request (would add a rules-allowed field).
- **Couple-key rotation / forward secrecy.** A couple-key compromise exposes all history incl. backups (no FS
today). Add rotation (both devices re-key) — hard but the right long-term hardening.
- **Server-independent anti-rollback freshness.** A malicious server could serve a stale manifest to hide recent
messages; today mitigated by the `generation` counter + a Phase-1 Firestore cross-check. Add a signed/monotonic
freshness signal for the Option-B world.
## Today widget (Batch 3.2 follow-ons)
The Glance "Today" widget shipped (R29): content-free daily-question state + streak, app-lock →
generic mode, taps into the daily question via `MainActivity`'s `app_route` extra. Verified: provider
registers, the `HomeViewModel``TodayWidgetUpdater``today_widget` DataStore bridge writes, headline
logic unit-tested, no crash. Remaining:
- **On-home-screen render check.** Binding a widget to the launcher isn't adb-scriptable; confirm the
Glance composition renders (light/dark) on a device, or add an instrumented test that binds via
`AppWidgetManager`.
- **Remote Config kill switch (`widget_enabled`).** Deferred: the app has **no** RemoteConfig wrapper yet
despite `firebase-config-ktx` being a dep. Build one shared wrapper once (fetch/activate + defaults),
then apply the kill switches the plan wants across the widget, solo mode, and deep links. Until then the
widget degrades safely on its own (generic card + taps into the app).
- **Day-rollover refresh.** Covered today by `updatePeriodMillis=30m` + app-open updates; a WorkManager
day-boundary trigger (plan's suggestion) would make the "Tonight's question is ready" flip crisper.
## Solo / pre-pairing (Batch 2.2 follow-ons)
The core of the pre-pairing experience shipped (R29): unpaired users can answer the daily question solo
(`DailyQuestionViewModel.submitAnswer` already saves to `LocalAnswerRepository` and skips the Firestore
sync when there's no couple), reach it via the Home secondary card, and see an honest `UnpairedLockedCard`
on the Play hub instead of a silent bounce to invite. On pairing, `SoloAnswerMigrator` offers a one-time
"bring your answers along?" consent dialog (`PairingSuccessScreen`) that writes the solo answers into
**Couple Lore** (encrypted) or discards them. Remaining, deliberately deferred:
- **Read-only date-ideas browse when unpaired.** The Date Match / Plan Date tiles still route unpaired users
to invite (they need a couple for swipes). A standalone read-only date-ideas list (the seed is client-side
and `date_ideas` rules already allow any authed read) would let a solo user browse while they wait. Pairs
well with the Firestore-backed catalog work (Batch 4.2).
- **Remote Config kill switch `solo_mode_enabled`.** Not yet wired; `firebase-config-ktx` is already a dep.
- **Convert the remaining silent bounces to `UnpairedLockedCard`.** Wheel / Messages / Answer-history / the
daily-question "Discuss" button still redirect unpaired users to `CREATE_INVITE` without explanation; reuse
the new `ui/components/UnpairedLockedCard.kt` on those surfaces for consistency.
- **Migrate-on-pairing robustness.** `SoloAnswerMigrator` is best-effort per entry (a not-yet-ready couple
key leaves the answer local for a retry), but there's no later retry trigger beyond the pairing-success
screen — add one if telemetry shows keys aren't ready at that moment.
## UI
_(No open UI defects. The P0 onboarding/auth crash filed here 2026-06-28 was fixed + verified live and moved to
`ClaudeReport.md` as **O-ONBOARD-001** — root cause was `painterResource` on the `<bitmap>` `ic_launcher_foreground`
(not the background, as originally guessed); fixed both `OnboardingScreen.CtaSlide` + `AuthVisuals.AuthLogoMark` to use
the raster `closer_launcher_foreground`. The "Add to Bucket List mixed dark/light" item is also fully resolved — dialog
tokens R16, add-FAB `Color(0xFFB98AF4)`→`primary` R18.)_
## QA
### From R31 (2026-07-09) — QA-environment observations (not app defects)
- **Add the RevenueCat `test_` SDK key to `local.properties` on the QA emulators.** The paywall currently can't load real offerings there (`RC_API_KEY` absent → `InvalidCredentialsError` → graceful "Couldn't load plans / Try again"). Now that RevenueCat is configured (Test Store + `closer_premium` entitlement, corrected this cycle), seeding the `test_…` key would let QA drive the *live* paywall (plan pills, purchase flow via the Test Store) instead of only the gate + the error state. The gate itself (free → Paywall) is verified; this unlocks the money-adjacent surface short of a real device.
- Minor: reinstalling the debug APK rotates the FCM token; the app re-registers a fresh one on foreground but stale tokens linger for ~33h until `pruneTokens` clears them on a real send. `qa/qa_push.js` now selects the newest token (was `docs[0]`), so the entrypoint smoke no longer false-FAILs after a reinstall.
### From R29 games review (2026-07-07) — reconciled 2026-07-08 after R30 (C1C4) + the functions v2 deploy
**✅ Shipped since filed (R30 batches C1C4 + `f2321d35`; server-side pieces live as of the 2026-07-08 deploy):**
server daily-question pool **seeded** and selection made **server-authoritative, mode-aware, deterministic**
(mirrors the client's `DailyModeResolver` weekday modes + epochDay offset — kills the DQ-MISMATCH class and
cross-timezone partner splits at the root) · **banner lifecycle BANNER-LIFE-001** (entering a session consumes
its banner via `GamePromptController.consumeForSession`; stale banners replaceable — C1) · **retention set:**
HowWell role-swap rematch CTA ("Your turn — let {name} guess you") · `NextBeatCard` return-tomorrow beat under
every game's results · Desire Sync per-couple seen-question memory (`SeenDesireQuestionStore`) · deterministic
per-couple Date Match deck shuffle + skip-already-swiped · `daily_fun_mc` excluded from the HowWell prediction
pool · Challenges "Day N unlocks tomorrow 🌙" teaser instead of spoiling day-N+1 (C2 `statusDay`).
Still open:
- **Generic session-game engine** — ThisOrThat/HowWell/DesireSync embedded VMs are ~80% clones of one ~250-line state machine (load/create/join/submit/retry/observe/abandon) and their data sources are structural copies (`EncryptedAnswerMapDataSource<T>`). Migrate one game first (ToT), full live regression, then the rest.
- **Generic replay VM/screen** — 3× ~60-line clones (ToT/DS/HW replay).
- **Shared waiting/length-picker/error composables** — each game re-implements WaitingForPartner/length chips/error screens despite shared components existing.
- **Premium-check invocation helper** — CouplePremiumChecker is invoked 4 different ways across ~10 call sites with inconsistent failure fallback.
- **GameConstants** — divergent ADVANCE_DELAY_MS (420 vs 380), QUESTION_COUNT, SPIN_DURATION_MS per file; follow SessionLength's pattern.
- **DispatcherProvider injection** — Dispatchers.IO/Default hardcoded everywhere (testability).
- **Error-surfacing standardization** — BucketList/MessagesInbox/PlayHub/AnswerHistory/WeeklyRecap/Onboarding VMs swallow load errors (blank screens); adopt uiState.error + ErrorState for reads, _events snackbar for writes (ConversationViewModel pattern).
- **Retention analytics** — first-answer / reveal-viewed / waiting-abandoned / notif-tap events: the one piece of the R29 retention list not built.
- **Content retag** — `daily_fun_mc` rows still carry `sex='neutral'` in the asset DB; after C3's HowWell-side exclusion this is purely Desire Sync pool hygiene (benign today via the binary filter). 48/150 sexual_preferences items are non-binary configs (Wheel-only) — intended?
- **npm audit** — 9 moderate remain after the 2026-07-08 firebase-functions v7 bump (retry-request/teeny-request via @google-cloud/storage, i.e. **firebase-admin** transitive — untouched by the functions bump); revisit on the next firebase-admin major. _(Re-verified 2026-07-11: still 9 moderate, unchanged set; deliberately not running `npm audit fix` — no lockfile churn on the production backend for documented moderates.)_
- **`scheduledOutcomesReminder` scaling cap** — the daily cron scans `couples` with a flat `.limit(200)` and no pagination (`functions/src/couples/scheduledOutcomesReminder.ts:35`), so couples beyond the first 200 silently never get 30/60/90-day outcome nudges. Fine pre-launch; **before the userbase approaches ~200 couples**, paginate with the same `orderBy(__name__)+startAfter` page loop `assignDailyQuestion` already uses (see `assignDailyQuestion.ts` PAGE_SIZE pattern).
- **Compose stability: adopt `ImmutableList` for `*UiState` list/set fields (codebase-wide).** Strong-skipping (K2 default) is on, and the Home refactor added `@Immutable` to the Home card models — but `List<>`/`Set<>` params still only reference-skip. Add the `kotlinx.collections.immutable` dependency (JetBrains, first-party) and switch `*UiState` list/set fields to `ImmutableList`/`ImmutableSet` (with `.toImmutableList()` at each VM construction site) so repeated card/list params become structurally skippable. **Do it consistently across all ~20 screens in one pass — not per-screen — to avoid a snowflake.** `ui/home/HomeModels.kt` is already partly there (`@Immutable` on `HomeAction`/`PendingActionCard`/`HomeCategorySummary`), so Home is the natural reference implementation.
Improvement & feature ideas surfaced while QA-testing as a consumer (each works today — none are defects).
- **Tier 3: Compose screenshot diff for visual regression.** The static scanner in `scripts/theme-scan.sh`
catches ~80% of light/dark theme mismatches, but it cannot detect *compositional* failures: a theme-token
color used on the wrong surface, a gradient with hardcoded light stops, or subtle contrast collapse.
Implement a screenshot pipeline with Roborazzi / Shot / papAROS that renders every `AppRoute` in both
light and dark, pixel-diffs them, and fails on unexpected white backgrounds or invisible text. When done,
run it in CI against every UI PR.
- **🟡 STARTED (R20) — Instrumented / on-device test coverage (was 0 androidTest).** First cut shipped:
`app/src/androidTest/.../ui/FirstRunRenderSmokeTest.kt` — an on-device **Compose render smoke** of the first-run
screens (`CtaSlide` + `AuthLogoMark`, light + dark), the exact `painterResource` logo sites that crashed every fresh
install in **O-ONBOARD-001**. It's the net for the "composes fine, crashes on first paint" class the JVM unit tests +
static scanners structurally can't catch. **Proven (R20):** passes green on-device, and FAILS with the original
`IllegalArgumentException: Only VectorDrawables…` when the bug is reintroduced. Infra added: `testInstrumentationRunner`
+ `ui-test-junit4` (build.gradle.kts); also un-blocked the androidTest source set (the stale `CanonicalVectorCaptureInstrumentTest`
couldn't compile against `private deriveKey` → made it `@VisibleForTesting internal`). Run: `./gradlew :app:connectedDebugAndroidTest`.
**Still to grow:** it's Hilt-/Firebase-free leaf-composable rendering only — extend toward a fuller
sign-in → pair → answer daily Q → open a game → send a message flow (needs a Hilt test runner + fakes), and wire
`connectedDebugAndroidTest` into the per-round gate / CI alongside `qa/entrypoint_smoke.sh`. *Prompted by:* the QA-plan
render-coverage gap (O-ONBOARD-001 escaped because nothing rendered a composable).
- **✅ DONE — Consistent brand glyphs across game cards + waiting surfaces.** G-set + G2 (17 glyphs) in
`res/drawable-nodpi/glyph_*.xml`; **13 wired + verified live:** every Play-hub card (This or That, How Well, Desire
Sync, Connection Challenges, Memory Lane, Date Match, Plan Date, Question Packs, Bucket List, Past Games — Spin the
Wheel keeps its full illustration), WaitingForPartner per-game glyph, and Settings (Subscription/Security/Privacy/
Delete). 4 unused have no clean slot (notif uses `ic_notification_closer`; Today uses hero art; quiet-hours uses its
illustration; no export-data row exists). Full map in `ClaudeBrandingReview.md`. _(This-or-That backdrop redesign is
Codex C-DARK-UI-001.)_
- **Minor proactive-notification gaps (low priority).** ~~No push when a partner *joins* your active game
(`partner_joined_game`)~~ → **BUILT + LIVE (deployed 2026-07-08 with the functions v2 round):** the non-starter
opening an active session writes `joinedByUsers` (rule-gated), `onGameSessionUpdate` notifies the starter
"<Name> joined your game" with the joiner's avatar (one-time `joinNotifiedAt`); foreground shows the standardized
in-app banner. (Same deploy also stopped the false "X finished — see your results!" push for abandoned/quit
sessions — a real completion requires both uids in `completedByUsers`.) **Still open:** no push when a partner
*ends/abandons* a game (`game_ended`/`game_abandoned`) — the other partner sees it in-session /
on WaitingForPartner, so nothing's broken, just less proactive. *Prompted by:* Pass E (R8) inventory.
- **Clarify Connection Challenges day-progress when partners are out of step.** If one partner catches up a *missed* day ("Pick it back up") while the other doesn't, the two devices show different **"Day N of 7"** (seen R10: QA Day 4 vs Sam Day 3) even though the 🔥 streak stays in sync on both. Not broken (plausibly individual-pace-through-the-series by design), but two people in the same shared challenge seeing different day numbers is confusing — consider a shared "you're on Day N together" framing or a clearer caught-up/ahead indicator. *Prompted by:* Pass B (R10) Connection Challenges playthrough.
### Security hardening (defense-in-depth — not vulnerabilities; rules already hold)
> **Canonical security doc: [`SECURITY.md`](SECURITY.md)** (2026-06-29) — full threat model, what's
> protected vs. exposed (metadata), known caveats, and the **prioritized hardening roadmap**. The P0
> there (**enforce App Check on the backend**, independent audit, release hardening), P1 (encrypt
> profile metadata, opt-in telemetry, recovery-phrase UX, biometric re-lock), and P2 (cert pinning,
> multi-device keys, data export, key rotation) are the authoritative list; the two items below are the
> older notes that fed into it.
- **Enforce App Check on Firestore (currently OFF).** Round 7 raw-API test: an authenticated request with **no App
Check token** (raw Firestore REST) returned `200` for a member — so rules are the *sole* gate. Rules correctly deny
non-members/cross-couple (all `403`), so this is not a live hole, but enabling App Check enforcement on Firestore
would block non-app clients entirely (defense-in-depth). *Prompted by:* R7 D3 raw-API angle.
- **✅ DONE (R21) — Biometric app-lock now re-arms on background/timeout (was: only cold-start/process-death).**
`MainActivity` observes the lifecycle: while the lock is on and the session is unlocked, it records when the app is
backgrounded and **re-locks if it returns after >60s away** (`BIOMETRIC_RELOCK_GRACE_MS`) — so a picked-up, already-open
phone re-prompts, not only on Activity recreation. The grace window avoids re-locking on quick task-switches (the
biometric prompt, photo picker, share sheet). Code-complete + compiles; **live re-lock not yet driven** — emulators have
no enrolled biometric/PIN, so verify on a physical device. (SECURITY.md rec #7.)
> Artwork to generate (ChatGPT prompts, house-style-matched) lives in `ClaudeBrandingReview.md`, not here.
## Backend — Cloud Functions v2 follow-ups (migrated + deployed 2026-07-08)
All 34 functions run as **2nd gen** in `closer-app-22014` (plus `onUserDelete`, intentionally v1 — gen 2 has
no `auth.user().onDelete`). Deploy-day learnings + deferred items:
- **Restore full vCPU + concurrency at launch (quota).** The project's default **Cloud Run regional CPU quota**
can't fit ~35 services at v2's default 1 vCPU/instance — deploys failed container healthchecks with
*"Quota exceeded for total allowable CPU per project per region"*, so `functions/src/options.ts` pins
`cpu: 'gcf_gen1'` (gen1 fractional tiers, 256MiB → ⅙ vCPU), `concurrency: 1`, `maxInstances: 5`. Costless at
dev scale. **At launch:** request the Cloud Run CPU quota increase (console → IAM & Admin → Quotas,
us-central1), then drop the `cpu`/`concurrency` overrides to restore 1 vCPU + concurrency 80, revisit
`maxInstances`, and consider `minInstances: 1` on the cold-start-sensitive callables (`checkDeviceIntegrity`,
`createInviteCallable`, `acceptInviteCallable` — app startup + pairing are first impressions).
- **Deploy-failure recovery pattern (keep for ops).** A background-trigger create that fails mid-deploy (quota,
Eventarc propagation) leaves an **orphaned `https` service**; retries then error *"Changing from an HTTPS
function to a background triggered function is not allowed."* Fix: `firebase functions:delete <name>` the
stray, then redeploy it. Diagnose with `firebase functions:list` — healthy Firestore triggers show
`google.cloud.firestore.document.v1.*`, strays show `https`. (First-ever gen-2 deploy also needs Eventarc
service-agent IAM to propagate — the "retry in a few minutes" failures were this.)
- **✅ DONE (2026-07-08) — Node runtime bumped 20 → 22.** All 35 functions redeployed in-place on **nodejs22**
(incl. the gen-1 `onUserDelete` — accepted cleanly); both `backend-ci.yml` node pins bumped to match. Verified:
`functions:list` runtime column, callable cold-boot smoke (UNAUTHENTICATED JSON = our code running), clean logs.
nodejs24 was evaluated and rejected: **2nd-gen only**, and the runtime is codebase-wide (gen-1 `onUserDelete`
blocks it). **Next forced bump:** Node 22 is deprecated 2027-04-30 / decommissioned 2027-10-31 — by then either
nodejs24 supports gen 1, or `onUserDelete` needs migrating off the gen-1 auth trigger first.
- **RevenueCat webhook deploy (gated on the signing secret).** The v2 `revenueCatWebhook` is code-complete
and its export is now **live** in `functions/src/index.ts`, but it is **not deployed yet** — deploy waits on
the secret. Auth is **HMAC-SHA256** (RevenueCat's webhook signature verification): the handler reads
`X-RevenueCat-Webhook-Signature: t=<ts>,v1=<hex>` and verifies `HMAC-SHA256("<ts>.<rawBody>")` against
`REVENUECAT_WEBHOOK_SECRET`, constant-time, with a ±5-min timestamp replay guard. Because that
`defineSecret` is validated codebase-wide at deploy (a missing secret fails *every* function's deploy, even
with `--only`), the secret must exist first. To deploy: ① in the RevenueCat dashboard create the webhook +
enable signature verification, copy the signing secret, ② `firebase functions:secrets:set REVENUECAT_WEBHOOK_SECRET`,
**delete the old dormant v1 `revenueCatWebhook` first** (still deployed; same-name v1→v2 is blocked
in-place), ④ `firebase deploy --only functions:revenueCatWebhook`, ⑤ point RevenueCat at the function URL and
send a test event. (The `closer_premium` entitlement already exists in the RevenueCat project.)
## Roadmap — features & strategy (consolidated from the old `FUTURE.md`, 2026-06-28)
**Unbuilt games (Play Hub is live; these remain):**
- **Would You Rather / Truth or Dare** — tiered sweet→spicy, consent-gated; reuses the deck + match engine. Strong **premium "spicy" tier** lever.
- **Daily Sync / Rose-Bud-Thorn** — one-tap emotional check-in, see partner's; small/new, free retention driver.
**"2026, not 2019" differentiators (strategic):**
- **AI-personalized prompts** — server-side (latest Claude) generate/weight prompts from the couple's history/season/unexplored topics; add a `generatePrompts` callable (Cloud Functions already in place). Biggest modern lever; pairs with content-metadata routing below.
- **Async + real-time hybrid** — partner-presence ("they're here now") for live co-play, everything still async-friendly (long-distance).
- **Multi-modal answers** — voice/photo answers (esp. Memory Lane + daily check-ins).
- **Home/lock-screen widgets / live updates** — glanceable today's-prompt + partner status (Glance/Wear surface).
- **Gentle gamification** — forgiving "gentle streaks" (grace days) + shared wins, not loss-aversion/guilt.
- **Consent-first spicy content** — Intimacy/Dare tiers opt-in, double-confirmed, reveal-only-on-overlap.
**Cross-platform:** Android-first is fine for MVP; iOS is the strategic gap (couples split devices). Decision + plan live in `ClaudeiOSPlan.md` — don't start before release/trust polish.
## Product polish (consolidated from the old `FUTURE.md`)
- **Skeleton/loading states over bare spinners (P8).** `LoadingState` exists but many screens still use a bare `CircularProgressIndicator`. Add skeletons for question lists, game histories, home modules, paywall offerings, sync/reveal waits. Acceptance: no primary route shows an isolated spinner on an otherwise blank screen.
- **Paywall / store value framing (P12).** Paywall has benefits/restore/legal/RevenueCat; needs stronger value framing, real offering/trial clarity, screenshots/previews, and test coverage before release. (Overlaps Pass K/O.)
- **Content metadata & personalization (P13).** The bank is clean; the next leap is *routing*, not more questions — tag mood/depth/relationship-stage/conflict-safe/intimacy-level/time-needed and extend the selection APIs so prompts adapt to skipped topics, relationship length, and recent answers.
- **Copy convention + i18n gate.** The app renders copy from hardcoded Compose literals; a half-built `strings.xml` catalog (55 unwired entries) was purged (2026-07-09), and voice copy is being centralized into typed Kotlin catalogs (`ui/brand/CloserBrandCopy.kt` = privacy rotator, `ui/brand/CloserCopy.kt` = product/paywall voice) — one reviewable home, compile-safe, no `stringResource()` friction. Convention: **voice copy → catalog; generic chrome ("Continue"/"Restore") stays inline.** Deliberately NOT localized. **Hard pre-localization gate:** the day translation is on the roadmap, migrate the Kotlin copy catalogs → `res/values/strings.xml` in one pass (retrofitting i18n across hardcoded Compose after launch is far more expensive). First slice done: paywall + subscription; remaining voice copy (onboarding, invite share, age-gate) migrates incrementally as those screens are touched. (Also surfaced: the paywall vs. subscription **benefit lists diverge** — noted in `CloserCopy.kt` for a copy decision.)
## Release / pre-ship (consolidated + re-verified 2026-06-28)
- **Real release config before any store submission.** Confirmed still open: `app/build.gradle.kts` `versionCode = 1` / `versionName = "0.1.0"` and `core/navigation/ExternalLinks.kt` legal URLs are placeholder TODOs (`https://closer.app/privacy|terms|subscription-terms`). _Already done:_ a release-blocking Gradle check now fails the build if `RC_API_KEY` is unset/placeholder (`build.gradle.kts:106110`), so the old "add a gradle guard" sub-item is closed. Remaining: set real version, real legal/support URLs, real RC key + verify offerings/purchase/restore on internal testing. (Tracked by **Pass O** release-readiness.)
## Help & support surface (consolidated — old "Notes to Consider")
A future Help/Support screen could include: contact support · report a bug · send feedback · FAQ · subscription/billing help · pairing help · recovery-phrase/account help · app version + build number · optional "copy diagnostics" button.
<!--
Completed (2026-06-27, Future.md backend pass — deployed + verified live):
- subscription_entitlement_changed push — new Cloud Function `onEntitlementChanged` (users/{uid}/entitlements/premium
onWrite, edge-triggered inactive→active) notifies the PARTNER "X upgraded — you both have Premium now"; skips if the
partner already had premium. Client type `SUBSCRIPTION_CHANGED` added (routes to Subscription). Verified: QA premium
ON → Sam queued the push; Sam ON while QA already premium → no redundant notify. Deployed to closer-app-22014.
- users/{uid} update-rule field allowlist — `allow update` now uses `affectedKeys().hasOnly([...12 model/aux fields])`,
blocking junk keys + `hasPremium`. Verified raw-API: allowlisted field PATCH 200, junk field 403, hasPremium 403.
(Keep the list synced with User.kt + FirestoreUserDataSource — guard noted in firestore.rules.) Deployed.
Completed (2026-06-27, Future.md fix pass):
- Daily-reveal isRevealed retry — on reveal load, if local says revealed but the server doc's isRevealed is still
false (a prior best-effort markRevealed failed offline/transiently), re-issue markRevealed idempotently so
onAnswerRevealed fires and the partner gets the "opened your answers" push (AnswerRevealViewModel.load()).
- Brand glyphs — G-set copied to res/drawable/; Date Match + Plan Date cards wired to glyph_date_card_heart
(remaining surfaces tracked above + in ClaudeBrandingReview.md).
- Memory Lane "title runs into preview" — investigated, NOT a bug: the capsule list renders title (own line, ellipsized)
+ open-date; sealed content is never previewed. The R12 "run-on" was adb-typed test data landing in the title field.
Completed (2026-06-25) and removed from the backlog:
- Inclusive sex/gender options in onboarding (Female/Male/Non-binary/Prefer not to say) + honest copy
(Desire Sync is already gender-neutral, so no tailoring fallback was needed).
- Turn-aware Home "waiting to play" copy ("Your turn to play.").
- Partner-action/results pushes exempt from the weekly promotional rate-limit ceiling.
- Suppress the redundant results / "partner finished" push when the recipient is already on that
game's screen (new ActiveGameSessionMonitor mirroring ActiveThreadMonitor).
- Friendlier paywall error state: retry-with-backoff, offline-aware message, Continue hidden until plans load.
- Wire iOS illustrations into Android empty states — already wired (history, invite, daily-question,
together-empty, partner-activation all show their illustration).
- Rotating privacy messages on sign-up + forgot-password (login already had it).
-->