Commit Graph

707 Commits

Author SHA1 Message Date
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
null 611481786f fix(home): update PairedHomePreview to BOTH_ANSWERED state 2026-07-01 02:19:35 -05:00
null cbffaeca9f test(settings): ChangePasswordViewModelTest — validation + typed exception mapping 2026-07-01 02:19:31 -05:00
null e585856228 feat(nav): wire ChangePasswordScreen composable into AppNavigation 2026-07-01 02:19:26 -05:00
null 06c94f55f4 feat(nav): add CHANGE_PASSWORD route constant 2026-07-01 02:19:22 -05:00
null a096ca15d1 feat(settings): add Change password row to SecurityScreen (email/password accounts only) 2026-07-01 02:19:18 -05:00
null 30000e7150 feat(settings): ChangePasswordScreen + ViewModel with validation and typed error mapping 2026-07-01 02:19:14 -05:00
null 7fe095ef3b feat(auth): delegate changePassword in FirebaseAuthRepositoryImpl 2026-07-01 02:19:11 -05:00
null 54608796e1 feat(auth): add changePassword to AuthRepository interface 2026-07-01 02:19:04 -05:00
null d1e23f24ee feat(auth): wire changePassword with reauth + typed exceptions in FirebaseAuthDataSource 2026-07-01 02:19:01 -05:00
null 07eaf3c989 feat(auth): ChangePasswordException sealed class with typed failure reasons 2026-07-01 02:18:50 -05:00
null 2ecff95c12 docs: update home-dark screenshot 2026-07-01 01:50:54 -05:00
null 7ff405cad6 feat(notifications): remove partner avatar from notifications (privacy) 2026-07-01 01:50:50 -05:00
null f47fa3cdbd test(auth): ForgotPasswordViewModelTest — typed exception + blank-email coverage 2026-07-01 01:50:46 -05:00
null 1a6cdaa078 feat(auth): enumeration-safe forgot-password confirmation copy 2026-07-01 01:50:40 -05:00
null 79bfea7cfb feat(auth): use typed PasswordResetException in ForgotPasswordViewModel 2026-07-01 01:50:36 -05:00
null 885af0b9e4 feat(auth): wire typed PasswordResetException into FirebaseAuthDataSource 2026-07-01 01:50:32 -05:00
null a1d2ea2db8 feat(auth): typed PasswordResetException sealed class 2026-07-01 01:50:27 -05:00
null 7aa72532b7 docs: update home-dark screenshot 2026-07-01 00:21:54 -05:00
null f117da16c8 test(home): 10 unit tests for computeDailyQuestionState 2026-07-01 00:21:42 -05:00
null 0feb72eaf0 feat(home): reveal-waiting art swap, copy polish, extracted computeDailyQuestionState 2026-07-01 00:21:33 -05:00