Commit Graph

727 Commits

Author SHA1 Message Date
null 4040abbf28 refactor(functions): B1 migrate 13 Firestore triggers to v2 + harden
Migrate all Firestore triggers off the v1 API to firebase-functions/v2/firestore
(onDocumentCreated/Updated/Written); context.params→event.params, snap→event.data,
change.before/after→event.data.before/after. Region stays us-central1 (global option).

Hardening folded in (all reuse in-repo patterns):
- Adopt the shared sendPushToUser()/getUserTokens() helper in every trigger, removing
  ~7 copied token readers and the copied send/prune blocks. FCM tokens are no longer
  logged in plaintext anywhere here (redacted inside push.ts).
- console.* → firebase-functions/logger (structured).
- Idempotency: new claimOnce() (atomic create-if-absent marker under
  couples/{id}/notif_marks) dedupes at-least-once redelivery on the non-idempotent
  senders (onAnswerWritten/Revealed, onMessageWritten, onCoupleLeave, onEntitlementChanged,
  onDateReflectionWritten/Revealed, onDateHistoryCreated). Fail-open. onGameSessionUpdate/
  onGamePartFinished/notifyOnDateMatch already had transactional claim-flags — preserved.
- onRestoreRequested: the plan's "claim" is implemented as the existing 60s time-WINDOW
  (lastRestorePartnerAlertAt), not a permanent recipientUid marker — a permanent marker
  would wrongly block legitimate re-requests (restore docs are deleted+recreated by design).

Faithful port of onGameSessionUpdate/onGamePartFinished (broad wildcard + allowlist kept);
the trigger split and read reorder are deferred to B6 as separate commits.

Build clean; 70 tests green (+ new idempotency.test.ts, push token/prune tests). Emulator
discovery loads all triggers as v2 in us-central1. firebase-functions still v5.1.1 (v6 bump
deferred to B6). dist rebuilt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:46:17 -05:00
null fbf4e7957c build(functions): B0 v2 foundation — global options, shared push helper, structured logging
Groundwork for the v1→v2 Cloud Functions migration; no function is migrated yet
(all 35 still load as v1, verified via emulator discovery).

- options.ts: setGlobalOptions({ region: 'us-central1', maxInstances: 20 }), imported
  first in index.ts so it applies before any v2 function is defined. Region pin is
  load-bearing — the Android client uses the default region.
- notifications/push.ts: single canonical getUserTokens() + sendPushToUser() that
  batches via messaging.sendEachForMulticast() and prunes dead tokens, to replace the
  ~10 copied token readers and ~19 copied send/prune blocks in later batches.
- log.ts: firebase-functions/logger re-export + redactToken() (FCM tokens are secrets).
- push.test.ts: 9 unit tests (token merge/dedupe, BatchResponse→dead-token mapping,
  send/prune/no-op/whole-batch-failure paths). 67 tests green.

firebase-functions stays at v5.1.1 for the migration (supports both the root v1 API and
the /v2 subpaths); bump to v6 is deferred to the final batch once nothing references the
root namespace, so the build stays green at every step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:36:16 -05:00
null 6af9c15833 chore(repo): keep QA fixture docs local-only 2026-07-07 23:28:45 -05:00
null 694294c2e5 fix(ui): remove duplicate answer back button 2026-07-07 23:09:14 -05:00
null 5d03fd98be chore(qa): keep fixture credentials in a gitignored local file, not the repo
Test-account emails/uids/passwords now live in qa/fixtures.local.md (gitignored);
qa/README.md carries only a pointer. Credentials must never be committed or pushed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:32:37 -05:00
null 3acba138a3 chore(functions): rebuild dist for deployed daily-question + game-copy changes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:31:02 -05:00
null 15e4579410 docs(qa): R30 polish/UX/seed round — reports, coverage, review §5.1
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:06:36 -05:00
null ca67187754 fix(ui): strip internal 'mc' token from the reveal-screen category chip too
The daily reveal chip used displayCategoryName() (a different surface than the
Home pill fixed earlier), so it still showed 'Daily Fun Mc'. Fixed centrally in
the shared helper. Verified live: reveal now shows 'Daily Fun'.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:04:54 -05:00
null f2321d3536 fix(functions): server-authoritative, mode-aware, deterministic daily question
Replace pickRandomQuestionId (empty pool → unresolvable q_default_daily fallback)
with pickDailyQuestionId(date): computes today's weekday mode (mirrors the client
DailyModeResolver.DOW_DEFAULTS, FROZEN) and deterministically indexes the free
daily pool by epochDay % poolSize — the SAME question the free client would pick,
now assigned server-side.

Fixes two things at the root:
- The daily 'questions' pool was empty, so every couple got q_default_daily, which
  the client can't resolve → each device fell back to its own local selection
  (the DQ-MISMATCH-001 class; the client half was pinned to the free pool in C1).
- Partners in different time zones computed different device-local weekdays and got
  different questions; a single server-assigned id makes both devices identical.

Firestore 'questions' seeded with the 75 free daily_fun_mc weekday questions (ids
match the client asset DB so getQuestionById resolves them). Pool fetched once and
filtered in memory — no composite index needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:57:22 -05:00
null fda53175a9 feat(games): nudge + per-game copy + score a11y batch C4 from UX review
- Waiting-on-partner screen: 'Send a little nudge 💜' button (shown while the
  partner is still playing, not once it's your turn) reusing the generic
  thinking-of-you callable (10/day, quiet-hours-safe server-side); one-shot
  Toast on result incl. friendly rate-limit copy. Verified live: nudge →
  partner_activity push landed on the partner.
- Per-game banner copy: YOUR_TURN/RESULTS in-app banner now branches on gameType
  ('Your turn — guess their answers' for How Well, 'only mutual yeses ever show'
  for Desire Sync, etc.) instead of one generic line; mirrored in the Cloud
  Function's partner_completed_part push (yourTurnBody). Verified live.
- Accessibility: merged contentDescription on the This or That MatchScoreBadge
  ('You matched on N of M') and the How Well score ring Canvas ('You guessed N
  of M correctly') — both were split/Canvas visuals invisible to TalkBack.

Unit + functions typecheck green; assembleDebug clean. Server copy change needs
a functions deploy (bundled with the C1 finish-guard).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:50:39 -05:00
null 8e8223ca97 + 2026-07-07 21:47:20 -05:00
null cf8f82357e feat(games): game-loop retention batch C3 from UX review
- NextBeatCard: a self-contained 'what's next' card under every game's results
  CTAs so finishing a game doesn't dead-end — shows 'Tonight's question is ready
  →' when today's daily is unanswered, else a 'Day N together' streak note. Own
  @HiltViewModel (DailyQuestionResolver + LocalAnswerRepository + couple), so the
  four game screens only drop in the composable. Verified live on How Well results.
- How Well role swap: the guesser's results now lead with 'Your turn — let {name}
  guess you', starting a new round where they become the subject (Newlywed-style
  reversal; starter == subject). Verified live.
- Date Match: deterministic per-couple deck shuffle (kills 'every couple sees the
  same first card') + skip already-swiped ideas so the deck resumes past them
  instead of restarting at card 1. Verified live (swiped card no longer reappears).
- Desire Sync: remember served question ids per couple (SeenDesireQuestionStore on
  the settings DataStore) so back-to-back rounds don't repeat prompts; clears the
  record to cycle the pool when the unseen remainder can't fill a round.
- How Well pool purity: exclude daily_fun_mc novelty MCs from getQuestionsForPrediction
  (1869 real prediction questions remain); DAO method has no other caller.

Unit suite green; assembleDebug clean; live-verified on the emulator pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:42:39 -05:00
null 9d0a0c7fdf fix(onboarding,challenges): funnel + gating batch C2 from P3/UX review
- DOB picker (sign-up + profile): swap the View-based android.app.DatePickerDialog
  (platform teal) for a shared Compose M3 DobPickerDialog that renders in the app
  palette in both themes; future dates unselectable. Verified live dark = purple.
- SignupHandoff: back the pending DOB with the app's Preferences DataStore keyed
  per-uid, instead of an in-memory singleton that died on process death and made
  CreateProfile re-ask for the birth date after any restart mid-onboarding
  (verified: am kill mid-profile → relaunch → no DOB re-ask). Local-only write,
  so the auth-token/Firestore-rules race the old comment guarded against doesn't
  apply; per-uid key prevents cross-account leakage.
- Connection Challenges: expose statusDay (calendar-actionable day) on
  ChallengeState; the day card shows a 'Day N unlocks tomorrow 🌙' teaser instead
  of spoiling the next prompt the moment today's step is marked done. +2
  ChallengeStateMachine tests.

Unit + functions suites green; verified live on the emulator pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:24:27 -05:00
null 4ab79d34c4 fix(ui,games): defect-polish batch C1 from P3/UX review
- Challenges catalog: hide the 🔒 Premium badge once the couple has premium
  (A-003b; matches the Play hub's showPremiumBadge pattern). Verified live
  both directions under an authorized grant/revoke cycle.
- Game banner lifecycle (BANNER-LIFE-001): entering a session's screen now
  consumes any banner pointing at it (GamePromptController.consumeForSession
  wired into ActiveGameSessionMonitor.enter), and activity from a DIFFERENT
  session may replace a stale persistent banner. Verified live: no banner on
  reveal; stale RESULTS banner replaced by a new session's prompt.
- Waiting/join screen: says 'Your turn — {name} already played their part'
  for the non-starter once a first part landed (new partHasFinished mapped
  from partFinishNotifiedAt; completedByUsers only fills at reveal). Session
  observe mapping now also carries completedByUsers/joinedByUsers.
- How Well results: matched-row colors are now a theme-aware container+content
  pair (dark mode was near-invisible: fixed pale-green container under
  onSurfaceVariant text).
- Date Match: top card fully opaque — next card's text no longer bleeds
  through (was alpha 0.96).
- functions: don't send 'X finished — see your results!' for abandoned/quit
  sessions (status flips to completed with empty completedByUsers; a real
  completion always has both uids). Found live when a quit triggered a false
  banner. Needs deploy (bundled with C4).

Unit + functions suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:03:22 -05:00
null d554483ef2 docs(qa): R29 games/mechanics/retention/UI review round
- ClaudeGamesReview.md (new): per-game fidelity verdicts vs real-world analogs,
  live 2-device results, retention assessment (Hooked/Fogg/SDT lens) with
  prioritized recommendations, UI warmth scores, content-DB audit notes.
- ClaudeReport.md: R29 verdict entry (2 P1 + 2 P2 found & fixed, batches
  e5868bd + c36a101; new Ava/Ben fixture couple; premium lifecycle verified).
- ClaudeQACoverage.md: R29 coverage line (Pass B re-run pass, onboarding live
  pass, daily loop fail->fixed->re-verified, deferred items listed).
- Future.md: structural refactor specs (session-game engine dedupe, replay VM,
  shared composables, dispatcher injection, error surfacing) + retention and
  content follow-ups from the review.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:54:05 -05:00
null c36a101d38 refactor(cleanup): dead code + hygiene batch from games review
- Delete unused ui/theme/Color.kt (zero refs; live palette is Theme.kt).
- Remove orphaned EditProfileScreen wrapper composable (no route references it;
  AccountScreen embeds EditProfileContent, which stays).
- Debug-gate ArtPreviewScreen + PairedHomePreviewScreen out of the release nav
  graph (BuildConfig.DEBUG).
- Collapse duplicate wheel_history route into game_history (same screen; wheel
  surfaces now navigate to Past Games directly).
- CouplePremiumChecker: inject AuthRepository instead of raw
  FirebaseAuth.getInstance() (finishes the in-flight auth-DI refactor for
  non-data-layer call sites).
- Copy/UI polish: 'It's a match!' (matches the push copy), internal 'mc' token
  never renders in the Home daily pill ('Daily Fun Mc' → 'Daily Fun'),
  welcome/splash privacy tagline no longer clips mid-sentence (maxLines 4 +
  ellipsis, AUTH-TRUNC-001).

Unit + functions suites green; assembleDebug clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:48:56 -05:00
null e5868bd6b1 fix(games,daily): correctness batch from games review
- Game answer listeners (ToT/HowWell/DesireSync): close(err) instead of
  swallowing snapshot errors, + .catch in each VM's observeReveal surfacing a
  retryable ERROR (GameCopy.SYNC_ERROR + retrySync re-attach) — games no longer
  hang on WAITING forever on listener failure (GAME-HANG-001; matches the
  Wheel/Capsule sources' established pattern).
- Daily question: paired fallback pool is now premium-INDEPENDENT so both
  partners always resolve the same deterministic question; viewer-premium pools
  broke the couple contract when entitlement state differed or flipped mid-day
  (DQ-MISMATCH-001, reproduced live: partners answered different questions).
- Daily reveal: humanize raw option-id fallbacks so slugs never render
  (DQ-SLUG-001, e.g. 'fake_awards_should_be_mandatory').
- assignDailyQuestion.ts: replace hardcoded CST_OFFSET_HOURS=-6 with DST-safe
  Intl America/Chicago helpers (DST-001) + 5 regression tests (CDT/CST labeling,
  6PM reveal instant, spring-forward round-trip).

Verified live 2-device: identical daily question on both partners post-fix;
ToT full loop 5/5 reveal regression clean. Unit 244 + functions 58 green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 19:41:41 -05:00
null 5013c2cd09 docs: update standardization review to completed state
Records the applied standardizations (state collection, whole-data-layer
Tasks→await, version catalog, TS payload typing, 7/8 ViewModel-Firebase
extractions) and the four deliberately-scoped follow-ons (HomeViewModel metadata
listeners, color tokens, Functions v2, catch-unknown) with the reason each needs
its own verification loop rather than a broad sweep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:19:33 -05:00
null 0c59000065 refactor(ui): remove raw Firestore listeners from Daily/Partner VMs
DailyQuestionViewModel and PartnerHomeViewModel each held a raw
db.collection(...).addSnapshotListener on the partner's answer doc (just reading
snapshot.exists()). Replace with the existing, proven
FirestoreAnswerDataSource.observeAnswerForUser(...) Flow collected in a
viewModelScope Job (auto-cancelled; previous run cancelled on restart) — dropping
the FirebaseFirestore injection from both. Same behavior (exists → non-null),
now through the data layer.

Verified live: daily question shows the correct "waiting for your partner" state
via the new Flow; compiles + unit suite green.

HomeViewModel's two listeners use MetadataChanges.INCLUDE + inline analytics and
drive the core reveal — left as a scoped follow-on (needs a metadata-aware
data-source Flow + two-device real-time verification). See docs/standardization-review.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:18:30 -05:00
null cce2687312 refactor(ui): inject AuthRepository instead of FirebaseAuth.getInstance()
Five ViewModels reached into the FirebaseAuth.getInstance() static singleton for
the current user id; switch them to the injected AuthRepository.currentUserId
(BucketList, QuestionThread, DateBuilder, MessagesInbox, Conversation). Removes
the Firebase-in-presentation-layer coupling and makes them testable. No behavior
change; compiles + unit suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:12:47 -05:00
null 89cbd7cb55 refactor(functions): type callable payloads instead of `any`
Change the 5 https.onCall handlers from `data: any` to
`data: Record<string, unknown>`, so payload fields are `unknown` and must go
through the existing validators rather than being implicitly-typed. No behavior
change (every field was already validated); tsc + 53 function tests green.

Left as-is deliberately: `catch (err: unknown)` narrowing (churn, marginal) and
the untyped Tink handles in wrapReleaseKeyCallable (the crypto lib ships no types).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:06:45 -05:00
null c71817241c build: adopt Gradle version catalog (libs.versions.toml)
Centralize all plugin + dependency versions in gradle/libs.versions.toml (the
modern Gradle standard) and switch the root + app build scripts to the generated
libs.* / alias(libs.plugins.*) accessors. No version changes — a 1:1 migration of
the 57 hardcoded coordinates. Full assembleDebug + unit suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:05:10 -05:00
null fcc0d699fb refactor(data): finish Firebase Tasks→.await() across the data layer
Convert the remaining 10 data sources (Bucket, Conversation, QuestionThread,
DatePlan, DateMemory, DateReflection, Answer, User, Auth, DateMatch) from
verbose suspendCancellableCoroutine{cont-> ...addOnSuccess/addOnFailure} wrappers
(and the per-file getDoc/voidAwait/queryAwait/refAwait helper duplicates) to the
kotlinx-coroutines-play-services .await() extension. The data layer now has ZERO
Task-callback wrappers.

Per-block review preserved non-trivial semantics: callbackFlow snapshot listeners
and the DateMatch transaction are untouched; failure→null/false best-effort blocks
became runCatching{ ...await() }.getOrNull()/getOrDefault() (e.g. markRevealed,
hasProfile, PlayIntegrity.verifyWithServer); no-signed-in-user guards became
`?: throw`.

Verified live (real Firebase, paired account): auth + Home reads (couple/user/
answer/challenge), daily-question answer WRITE (secure payload + metadata sets),
and the streak update all succeed — no crash, no PERMISSION_DENIED. Unit suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 22:00:55 -05:00
null f160f141d6 chore(gitignore): ignore .code-workspace; cleanup Firebase data source dead code 2026-07-06 21:41:54 -05:00
null 39fbd943ff docs: standardization & modernization review
Grounded review of the Android app / functions / build with measured
inconsistencies, what was standardized this session (collectAsStateWithLifecycle;
Firebase Tasks→await subset), and risk-assessed recommendations for the larger
items (finish Tasks→await, version catalog, ViewModel Firebase extraction,
Functions v2, color tokens, TS types).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:36:59 -05:00
null 7507ca63e4 refactor(data): modernize Firebase Tasks to .await() (safe subset)
Replace verbose suspendCancellableCoroutine{ cont -> task.addOnSuccessListener
{cont.resume}.addOnFailureListener{cont.resumeWithException} } wrappers with the
kotlinx-coroutines-play-services .await() extension in the files whose blocks are
all one-shot Tasks (no snapshot listeners / transactions to endanger):
FirestoreOutcomeDataSource, FirestoreCoupleDataSource, PlayIntegrityChecker.

Notably fixes FirestoreOutcomeDataSource, which mixed both styles in one file.
PlayIntegrityChecker.verifyWithServer keeps its failure→null semantics via
runCatching{ ... .await() }.getOrNull() (a bare await would throw instead).

The remaining ~11 data sources also use the callback pattern but mix in
callbackFlow snapshot listeners / transactions, so they need per-block review —
tracked in Future.md rather than bulk-rewritten on untested I/O.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:36:09 -05:00
null 61a8609298 refactor(ui): standardize on collectAsStateWithLifecycle
Migrate all 56 Compose screens from collectAsState() to the lifecycle-aware
collectAsStateWithLifecycle() (added androidx.lifecycle:lifecycle-runtime-compose).
Upstream flow collection now pauses when the UI isn't visible instead of running
in the background — the modern, recommended idiom, and it unifies the two files
already using it with the rest of the app.

No behavior change for visible screens; compiles + unit suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:30:48 -05:00
null eeac3c9383 build(functions): compile dist for outcome aggregation
Tracked dist output rebuilt to match src (aggregateOutcomeStats export +
aggregateOutcomes module), keeping the deployed bundle in sync with source.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:10:02 -05:00
null a79db9474b docs(crypto): key-storage migration design (design-only, gated)
Design for moving off the deprecated androidx.security:security-crypto to
Tink Android-Keystore-backed storage, without ever losing the couple key.
Covers: the load-bearing hazard (SecurePreferencesFactory.reset() silently
WIPES the couple key on any read failure) which must be removed first;
lazy dual-read + re-wrap + verify-then-clean migration; fail-closed (recover,
never delete) failure matrix; consumer ordering (couple key last); staged RC
rollout + content-free telemetry; test plan; open questions for the owner.

No implementation ships until the owner approves this design (Batch 5.1 gate).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:08:52 -05:00
null 943507b72a feat(functions): privacy-safe outcome-stat aggregation
aggregateOutcomeStats (scheduled) rolls up per-couple check-in deltas
(couples/{id}/outcomes/day_30|60|90) into the "X% feel closer in N weeks" stat
WITHOUT reading any E2EE content — only the self-reported deltas.

Privacy: counts/percentages only (no couple id or individual scores);
minimum-cohort suppression (N<50 → window omitted, no percentages); EXPORT-ONLY
via the top-level aggregate_stats collection, now explicitly deny-all in
firestore.rules (owner reads via console).

Pure aggregate()/extractCoupleOutcome() unit-tested incl. below-threshold
suppression + a no-PII assertion (6 tests); rules deny client read/write of
aggregate_stats (2 tests). Full functions 53/53, rules 121/121.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:06:57 -05:00
null 95c3bc21fa feat(dates): Firestore-backed date catalog + sponsored ideas
- DateIdea gains sponsored/sponsorName/externalUrl (native, clearly-labeled ads
  — no tracking SDK).
- FirestoreDateIdeaDataSource reads the server-curated date_ideas collection
  (defensive: malformed docs skipped, https-only externalUrl); DateMatchRepo
  serves remote first, falling back to the shipped static seed when empty/failed
  so the feature always has content. Fallback verified live (Date Match renders
  the seed) + unit-tested (remote-present / empty / failure).
- Date card shows a "Sponsored · <name>" pill and a "Learn more" link (opened via
  the system browser, https-only, no click IDs).
- Local admin seeder (functions/scripts/seedDateIdeas.ts + date_ideas_seed.json,
  generated from the Kotlin seed + 1 sponsored example) — idempotent by id, has a
  --dry-run; NOT a deployed endpoint. Verified: 38 docs upserted to the emulator,
  1 sponsored, tsc builds.

Owner: run the seeder against prod (GOOGLE_APPLICATION_CREDENTIALS) to populate
date_ideas; add real sponsor content.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:02:22 -05:00
null 6ecc98eec2 feat(paywall): free-trial/intro pricing, gift framing, one-time SKUs
- PaywallPricing turns a RevenueCat Package into a headline price + an offer line
  (free trial, intro price, or one-time), so a dashboard-configured trial or a
  lifetime SKU renders without an app release; unknown shapes degrade to the
  plain price. Pure formatting is unit-tested (PaywallPricingTest); the SDK
  extraction is type-checked against purchases 8.20.
- Gift framing on the plan chooser ("One subscription covers you both — treat
  your partner"): couple-shared premium already unlocks both partners, so this
  is copy only, no billing change.

Live paywall (real offerings) still needs a real RC key + sandbox per
docs/SUBSCRIPTION_GO_LIVE.md (owner).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:53:15 -05:00
null 8464cb9219 feat(analytics): record specific notification type on push-opened
Foreground notification taps carried only app_route, so their
push_notification_opened event logged the generic "app_route" category. Also
put the notification type on the intent so the event records the specific kind
(partner_answered, reveal_ready, ...). Backgrounded taps already carry type via
the FCM data payload. Completes push-opened coverage across both tap paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:48:49 -05:00
null 368d8c455f fix(streak): gentle day-based streak with a grace day + earlier milestones
The live couple streak used a raw 48h rule that reset on any 48h gap AND
inflated the count by incrementing on every answer (several a day counted
several times). Replace with StreakCalculator.nextCount: once per calendar day,
one forgiving grace day (a single missed day still continues), reset only after
2+ missed days; skip the redundant write on a same-day answer.

Celebration milestones now include the habit-building early tiers
(1/3/7/14/30/100/365), single-sourced from StreakCalculator so the count and the
celebration can't drift; the milestone dialog handles day 1 grammatically.

Tested (StreakCountTest): same-day idempotency, consecutive, grace, reset, and
partner timezone-skew (never breaks a same-boundary streak). Existing 24
StreakCalculator tests still pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:46:14 -05:00
null f0a05f779b feat(widget): Glance "Today" home-screen widget
Content-free daily-question state + streak, mirrored from HomeViewModel via a
dedicated DataStore (TodayWidgetUpdater -> TodayWidgetState). App-lock drops it
to a generic card so partner-activity state doesn't sit on the protected home
screen; signed-out/unpaired show neutral prompts. Tapping opens the daily
question via MainActivity's app_route extra.

Verified: provider registers, state bridge writes, headline logic unit-tested
(TodayWidgetTest), no crash on launch. On-home-screen render + a Remote Config
kill switch are tracked in Future.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:40:28 -05:00
null 55699e17ed feat: security hardening, CI, analytics, invite deep links, solo mode, weekly recap
Roadmap phases 0-3.1 (see plan). Verified: unit tests, functions tests,
firestore rules 119/119, live emulator smokes.

Security + CI:
- Move App Check debug token out of committed build.gradle.kts to
  local.properties (secret() resolver: local.properties -> -P -> env);
  admin service-account key relocated outside the repo.
- Add GitHub Actions (android unit/scan/gitleaks, backend functions +
  rules emulator via emulators:exec) with a CI google-services.json stub
  and a reviewed .gitleaksignore baseline.
- Fix firestore rules tests to the current deployed contract (was 108/118,
  now 119/119).

Analytics + consent:
- FirebaseRetentionAnalytics behind a fail-safe composite sink, single-
  sourced hashing (AnalyticsHashing), Settings -> Privacy opt-out toggle
  gating Firebase + the sink, and the ~11 missing event call sites.
- NoPlaintextInAnalyticsTest guards the content-free rule at build time.

Funnel:
- Invite deep links (https App Link + closer:// fallback), JoinLink parser,
  PendingJoinCodeStore, prefilled/already-paired pairing, richer share text.
- Solo pre-pairing: UnpairedLockedCard on Play, SoloAnswerMigrator + consent
  dialog moving pre-pairing answers into Couple Lore.

Weekly recap:
- WeeklyRecapScreen driven by the existing generator from local answer data,
  Home card rewired, content-free shareable card.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 20:33:38 -05:00
null f68cab5cf2 feat: signup flow, age gate, user model updates, how well screen, game prompt banner 2026-07-02 02:42:55 -05:00
null 24823a39f0 feat: question discussion components, conversation VM, wheel session, settings, text input limits 2026-07-01 21:39:00 -05:00
null 09fea873e2 fix: DateReflectionScreen and FirestoreDateReflectionDataSource updates 2026-07-01 20:01:01 -05:00
null 896bf26b28 feat: date reflection reveal, UI upgrade plan, seed updates, ime-scan script 2026-07-01 04:12:58 -05:00
null 222bbd1c57 chore(db): update seed database 2026-07-01 02:55:38 -05:00
null b115cd2f66 chore(seed): update daily question content 2026-07-01 02:55:34 -05:00
null e1a8528489 docs: restructure README screenshot table to daily-question stages + core screens 2026-07-01 02:55:30 -05:00
null d49d67ecbd docs: add reveal-ready brand art + screenshot 2026-07-01 02:55:24 -05:00
null 31a2652e48 feat(art): add illustration_daily_reveal_ready (light + dark) 2026-07-01 02:55:20 -05:00
null d03e071098 feat(home): escalate daily-question art by state (partner prompt → reveal ready) 2026-07-01 02:55:15 -05:00
null 6d0a52641c feat(daily): rename modes to weekday theme names (Mood Check Monday, Tiny Win Tuesday, etc.) 2026-07-01 02:55:11 -05:00
null b19394b900 chore(seed): update daily question content to v61 weekly format 2026-07-01 02:20:09 -05:00
null a1b77f6786 docs: update README home screenshot alt text 2026-07-01 02:20:04 -05:00
null 18490f990e docs: update home screenshots (ready + your-turn states) 2026-07-01 02:19:57 -05:00