- 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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>