Commit Graph

774 Commits

Author SHA1 Message Date
null 757d4efdda test(home): robust assertion in render smoke's unanswered-state case
The unanswered case asserted a specific CTA ('Answer privately') that the preview's
demo state doesn't surface in the test viewport. Assert the stable header instead —
still exercises the UNANSWERED render path (setContent), just without a brittle text
match. Verified 3/3 green on a throwaway emulator (fixtures untouched).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:54:17 -05:00
null ed0aa4d3b3 fix(test): HomeContentRenderSmokeTest nested-comment (components/* opened a block comment)
'ui/home/components/*' in the KDoc contains a /* sequence, which Kotlin treats as
a NESTED block-comment opener (Kotlin nests block comments) -> unclosed comment.
Reworded. androidTest now compiles. (App was never affected.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:49:06 -05:00
null 1eacf36d81 fix(test): HomeContentRenderSmokeTest unclosed-comment (emoji in KDoc broke Kotlin lexer)
The prior commit's androidTest didn't compile — a stop-sign emoji in the KDoc
tripped the Kotlin lexer ('Unclosed comment'). Replaced with plain text. App was
never affected (androidTest isn't in the APK); this restores connectedAndroidTest.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:47:59 -05:00
null f7eb03417b test(home): HomeActionMapperTest (JVM) + HomeContentRenderSmokeTest (instrumented)
Part 3 of the Home refactor. Adds the durable coverage the plan called for:
- HomeActionMapperTest: 12 JVM cases over the lifted pure mapper (refresh states,
  withHomeActions pairing/daily/loading/error paths, toHomeLabel mc-drop, secondary
  cap of 3, C-HOME-001 primary/pending dedup) — locks the extraction as faithful.
- HomeContentRenderSmokeTest: instrumented render net for Home (via the VM-free
  PairedHomePreviewScreen), light + dark — catches 'composes fine, crashes on
  first paint'. Runs on a THROWAWAY only (uninstalls app-under-test).

JVM test green; androidTest compiles. Fills the 'Home has no UI test' gap.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:46:16 -05:00
null 9e81c15350 perf(home): @Immutable on HomeAction/PendingActionCard/HomeCategorySummary
Part 2b (Compose stack advantage). Marks the three verified-deeply-immutable
card models @Immutable so their single-instance params get structural-equality
skipping (upgrade over the K2 reference-equality strong-skipping default).
HomeAnswerStats/HomeUiState left unannotated (LocalAnswer/Question/Set transitive
types not fully audited — a false @Immutable promise would cause stale UI).
Rendered output identical; only recomposition frequency drops. compile green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:42:04 -05:00
null 7be9a1bf15 refactor(home): lift pure action mapper -> HomeActionMapper.kt (VM 867->562)
Part 2b. Moves the ~305 lines of pure HomeUiState action-derivation extensions
(refreshDailyQuestionState/withHomeActions internal; toHomeAction/
buildDailyQuestionAction/buildPendingActions/has*/toHomeLabel private) out of
HomeViewModel verbatim (de-indented, byte-identical bodies). Verified pure — no
this@HomeViewModel/repo refs. VM call-sites unchanged (same-package extensions).
compile + full unit suite green. HomeViewModel now 562 lines (from 1030).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:40:31 -05:00
null 227d141221 refactor(home): extract data models -> HomeModels.kt (VM 1030->867)
Part 2a. Moves the Home data classes/enums + computeDailyQuestionState (kept
@VisibleForTesting internal) + gameRouteFor (widened private->internal, its only
caller loadHome is same-package) out of HomeViewModel into HomeModels.kt.
Same package -> no consumer import churn (AppNavigation, DailyQuestionStateTest
unchanged). Behavior-identical. compile + ui.home unit tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:38:29 -05:00
null 415f8d6866 refactor(home): extract StreakMilestoneDialog + drop dead MomentCueCard
Step 9/9 of the UI split. StreakMilestoneDialog -> components/ (kept internal;
ArtPreviewScreen call updated to the new package). MomentCueCard was dead (no
callers) — removed. Orphaned imports cleaned. compileDebugKotlin green.
HomeScreen.kt now ~620 lines (from 1921).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 20:34:37 -05:00
null 5e266a95fa merge(home): HomeScreen UI decomposition (8 cluster files) into dev
Behavior-preserving split of HomeScreen.kt (1921 -> 712 lines) into
ui/home/components/: HomeActionStyle, HomeStateCards, HomeCategoryPreview,
HomePendingCards, HomeActionFeed, HomeStatusStrip, HomePrimaryCards,
HomeHeaderSection. Each step compile-gated. ViewModel extraction + tests still
pending (tracked separately).
2026-07-11 19:59:45 -05:00
null 94360a5438 refactor(home): extract header + partner sheet -> components/HomeHeaderSection.kt
Step 8/9. Verbatim move (HomeHeader + PartnerQuickActionsSheet public, rest private;
@OptIn(ExperimentalMaterial3Api) + .semantics a11y + full state param preserved).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:43:22 -05:00
null c13dbc5bfd refactor(home): extract primary/activation cards -> components/HomePrimaryCards.kt
Step 7/9. Verbatim move (activation + primary hero public, benefit-pill private).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:40:08 -05:00
null d324efe182 refactor(home): extract status strip -> components/HomeStatusStrip.kt
Step 6/9. Verbatim move (strip public, chip private). compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:32:54 -05:00
null 48197ffd1e refactor(home): extract action feed -> components/HomeActionFeed.kt
Step 5/9. Verbatim move (section public, card private). compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:31:42 -05:00
null 88460922c3 refactor(home): extract pending cards -> components/HomePendingCards.kt
Step 4/9. Verbatim move (section public, card-view private). compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:30:25 -05:00
null 07c810c9a1 refactor(home): extract category preview -> components/HomeCategoryPreview.kt
Step 3/9. Verbatim move (grid public, mini-card private). compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:29:06 -05:00
null 18ad31506e refactor(home): extract loading/error/empty -> components/HomeStateCards.kt
Step 2/9. Verbatim move, public entries. compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:27:52 -05:00
null 5f39194c00 refactor(home): extract shared card kit -> components/HomeActionStyle.kt
Step 1/9 of the HomeScreen decomposition. Moves the 6 shared style decls
(homeActionGlyph, HomeGlyphIcon, homePrimaryArt, HomeActionColors,
HomeActionTone.actionColors, HomePill) verbatim into ui/home/components/, made
public per the components/ visibility idiom. No behavior change. compileDebugKotlin green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 19:26:08 -05:00
null 047185134e review 2026-07-11 19:17:51 -05:00
null eb70177178 revoew 2026-07-11 19:15:33 -05:00
null a7f252e656 review 2026-07-11 18:49:40 -05:00
null a63bf350b1 fix(crash): LocalDate.ofInstant crashes on API<34 + add Lint/R8 CI gates
Adding Android Lint to CI immediately caught two real crash bugs invisible to
all emulator QA (the fixture emulators run API 34+):

- SettingsViewModel + YourProgressViewModel called LocalDate.ofInstant, which
  was only added in API 34 — but minSdk is 26. On every device running Android
  8-13 (the bulk of the install base) these throw NoSuchMethodError and crash
  the Settings and Your Progress screens. Fixed with the API-26-safe equivalent
  Instant.atZone(zone).toLocalDate() (same result).

The other two Lint errors were false positives (ProduceStateDoesNotAssignValue
on two EncryptedChatImage composables that DO assign value inside the producer —
the check misfires on a suspend/?.let RHS) — explicitly @Suppress'd with a note,
so Lint reaches 0 errors legitimately rather than via a blanket baseline.

CI (android-ci.yml) gains two jobs:
- android-lint: ./gradlew :app:lintDebug (fails on error-severity; 113 existing
  warnings are non-fatal and left for a separate burndown).
- release-build: first-ever R8 gate — builds :app:bundleRelease with a throwaway
  keystore + dummy RC_API_KEY (satisfies the release guards; AAB not distributed),
  so the minify/shrink/sign toolchain can never silently rot. Verified locally:
  bundleRelease SUCCEEDS today (95MB AAB). A green build proves the toolchain,
  not runtime survival of reflectively-loaded classes (Tink) — that stays a
  release-APK QA item.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:41:04 -05:00
null 53d18beb03 feat(seed): sex_and_desire light-depth batch 6 of 6 (6 written, 338->344) - hits 40.1% light 2026-07-11 18:32:47 -05:00
null 5af109e119 feat(seed): sex_and_desire light-depth batch 5 of 6 (8 single + 4 multi + 4 tot + 4 scale, 318->338) 2026-07-11 18:32:15 -05:00
null a362742494 feat(seed): sex_and_desire light-depth batch 4 of 6 (12 written + 4 single + 4 tot, 298->318) 2026-07-11 18:31:30 -05:00
null 57b0f02763 feat(seed): sex_and_desire light-depth batch 3 of 6 (12 written + 4 single_choice, 282->298) 2026-07-11 18:30:51 -05:00
null b3165645e7 chore(repo): track the repaired pre-push hook + installer; backlog entries (Batch 2)
- scripts/git-hooks/pre-push: the fixed hook (dirty-tree guards kept; the
  self-defeating 'unpushed commits' blocker replaced with a behind-remote
  fail-fast + an informational listing of what a push will publish). Hooks
  don't sync on clone, so scripts/install-git-hooks.sh installs them; verified
  idempotent against the live .git/hooks copy.
- Future.md: scheduledOutcomesReminder .limit(200) no-pagination scaling cap
  recorded with the fix pattern (assignDailyQuestion's page loop); npm-audit
  entry re-verified 2026-07-11 (still 9 moderate, transitive; no lockfile churn).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:30:27 -05:00
null 25c463ee2a feat(seed): sex_and_desire light-depth batch 2 of 6 (16 written, 266->282) 2026-07-11 18:30:21 -05:00
null 2dc07de350 feat(seed): sex_and_desire light-depth batch 1 of 6 (16 written, 250->266) 2026-07-11 18:29:50 -05:00
null 08e9be626e docs: reconcile stale docs with project reality (Batch 1 doc hygiene)
Every point-in-time doc now carries an explicit status header so no one works
from a dead plan or stale claim:

- CopyMigration.md: CLOSED — migration stopped by owner decision after Slice 1;
  Slices 2-4 cancelled (several target strings since rewritten by the Closer
  Couples prose rename anyway).
- store-assets.md + NameChange.md: the support-URL 'couplesconnect.app' claim
  was stale — ExternalLinks.kt already uses closer.app/support; remaining action
  is Play-console listing fields only.
- internal-testing-checklist.md: SUPERSEDED by store-assets.md + release_guide;
  also corrected its applicationId error (app.closer -> closer.app, the same
  namespace swap behind the 4e807cb0 deep-link bug).
- private-mvp-checklist.md: SUPERSEDED by the ClaudeQAPlan system.
- UI_UPGRADE / standardization-review / ui-review / date-planning-roadmap /
  IOS_E2EE_STATUS / iphone ARCHITECTURE_AUDIT: stamped historical/paused with
  pointers to the live sources (Future.md, ClaudeBrandingReview, ERM).

NOT touched: DEVELOPMENT_LOG.md (verified alive — newest entry 2026-07-08,
prepend-ordered; earlier 'log is dead' read was wrong) and the Engineering
Reference Manual (another agent's Phase-4 review owns its drift).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:29:08 -05:00
null 654be1feff fix(seed): rewrite sex_and_desire_074 to drop 'Discuss' (banned phrase per QUESTION_QUALITY_CHECKLIST.md) 2026-07-11 18:26:46 -05:00
null 4e807cb088 fix(settings): Manage-subscription deep link used namespace, not applicationId
ExternalLinks.MANAGE_SUBSCRIPTION pointed Play's subscription manager at
package=app.closer (the code namespace); the Play package is the applicationId
closer.app. In production the Manage button would miss the app's subscription
entry. Invisible to all QA so far because no real subscription has ever existed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:22:03 -05:00
null 35b0eac2df review 2026-07-11 18:11:31 -05:00
null 0c588d0a7d chore: gitignore release_guide.md (local ship-readiness operator doc)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 18:10:42 -05:00
null 9401762bd4 review 2026-07-11 17:41:53 -05:00
null 523c7120b8 review 2026-07-11 17:09:05 -05:00
null 6668a3c4b7 refactor(brand): in-app prose -> 'Closer Couples' (launcher + wordmark stay 'Closer')
Extends the store/marketing rename to in-app user-facing PROSE, per owner decision.
Every string where 'Closer' names the app in a sentence now reads 'Closer Couples'
(Android + iOS): settings, pairing, invite share, widget headlines, paywall
subhead/thank-you, age gate, privacy body.

Deliberately KEPT as 'Closer' (judgment calls, documented in docs/NameChange.md):
- launcher label / iOS CFBundleDisplayName (14 chars truncates under the icon)
- wordmark logos (onboarding, auth alt-text, widget header, share-card drawText,
  iOS onboarding) + their asserting tests
- 'Grow Closer' onboarding pun (intimacy, not the app name)
- entity/length-capped lines: privacy rotator 'so Closer cannot read them'
  (hard 64-char cap), iOS 'not even Closer's servers'
- iOS 'Settings > Closer' help path (must match the kept display name)
- 'Closer Premium' tier label (matches the RevenueCat entitlement display name)
- all technical identifiers (app.closer / closer.app / closer_premium / etc.)

TodayWidgetTest exact-match assertions updated. compileDebugKotlin + unit tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 17:07:31 -05:00
null b17271c89f review 2026-07-11 17:05:54 -05:00
null d3763fd37b docs(daily): reconcile wildcard counts across guide + schema; pool 12->11
Self-review of the wildcard authoring spec caught two real issues:

1. Count contradiction: the new Wildcard section said '+12 free' while the pack's
   authoritative counts (QUESTION_SCHEMA.md daily table + metadata, and the guide's
   Required Counts + Final Production Gate 'count validation') still said 500/75. A
   content agent running count validation would fail, or worse delete weekday
   questions to hit 500. Reconciled all four locations to the weekday pack (frozen
   500) + wildcard add-on -> 511 total / 86 free / 425 premium, and folded the
   wildcard set into the Final Production Gate review sampling.

2. Rotation math: the picker indexes epochDay % poolSize and wildcard days fall ~10
   apart, so a pool size sharing a factor with 10 (12 is even) only surfaces a
   fraction of the questions per year. Pool size must be coprime with 10 -> 11, not
   12. Documented as a hard constraint so it isn't 'rounded to 10' later.

Also hardened the section: net-new (not a patch), answer_config option mirror,
unique snake_case option ids, and a QUESTION_SCHEMA.md cross-reference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:48:18 -05:00
null f74f0e96f5 feat(daily): wildcard-day picker branch + authoring spec (Option A)
The client's DailyModeResolver promotes ~10% of days (dayOfYear % 10 == 3) to a
'Wildcard' theme, but there were zero wildcard questions and the server never
assigned one — so those days showed the Wildcard banner over a normal weekday
question. This adds the server half:

- pickDailyQuestionId now detects wildcard days (new pure, tested dayOfYearUtc /
  isWildcardDay helpers mirroring the client cadence) and prefers the mode_wildcard
  pool. Until that content is seeded it falls back to the day's WEEKDAY mode (not
  the whole pool), so it's a safe no-op pre-content. +3 unit tests (fn 80 -> 83).
- Authoring spec for the missing content added to
  seed/questions/DAILY_SINGLE_CHOICE_WEEKDAY_SYSTEM.md (## Wildcard Mode): 12 free
  single_choice, day-agnostic voice, REQUIRED mode_wildcard tag, id scheme, schema,
  and the post-authoring rollout (asset-db data-only insert + Firestore pointer seed
  + deploy).

Content authoring handed to another agent per the guide. Takes effect after the
wildcard rows land in app.db + Firestore and assignDailyQuestion is deployed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-11 16:39:59 -05:00
null f336c91731 docs(manual): batch 6 - fix broken iOS-Android sealed-answer bridge anchor (→ slug mismatch) 2026-07-11 16:25:36 -05:00
null 690b567f82 docs(plan): phase 4 batch 5 no-op - qa_push.js fix is internal to the QA helper 2026-07-11 16:24:20 -05:00
null fcee89912f docs(plan): phase 4 batch 4 no-op - copy catalog + string cleanup don't touch the manual 2026-07-11 16:24:04 -05:00
null dc624ebb21 docs(manual): batch 3 - flip per-module bullet for billing webhook to exported 2026-07-11 16:23:38 -05:00
null 0b92632973 docs(manual): batch 2 - RevenueCat 10.13.0 + HMAC-SHA256 webhook in tree + gitignore entry 2026-07-11 16:23:16 -05:00
null c60fa958a5 fix(qa): qa_push.js picks newest FCM token, not docs[0]
After an APK reinstall a device registers a fresh FCM token alongside stale
ones (~33h). The smoke harness grabbed docs[0] (unordered) and could hit a
stale token -> FCM 'Requested entity was not found' -> false smoke FAIL.
Now sort fcmTokens by updatedAt and try newest-first across all tokens,
skipping dead ones, mirroring the app's sendToUser + pruneTokens behavior.

Also file two R31 QA-env observations to Future.md (add the RevenueCat test_
key to local.properties so QA can drive the live paywall; FCM token churn note).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-10 10:06:07 -05:00
null 89d06eeb7d fix(billing): correct RevenueCat webhook auth to HMAC-SHA256 + enable export
The webhook verified an Ed25519 signature in an X-Signature header, but RevenueCat
offers no public-key signing — it sends HMAC-SHA256 in X-RevenueCat-Webhook-Signature
(t=<ts>,v1=<hex>) computed over "<ts>.<rawBody>". As written, every real event would
have 401'd and premium would never sync for the partner.

- Rewrite verification to HMAC-SHA256 with a +/-5-min timestamp replay guard and a
  constant-time compare; extract a pure verifyWebhookSignature() for unit testing.
- Rename secret REVENUECAT_SIGNING_KEY -> REVENUECAT_WEBHOOK_SECRET (it is an HMAC
  secret, not an Ed25519 key). Never deployed, so no migration.
- Uncomment the export in index.ts (deploy still gated on seeding the secret).
- Add revenueCatWebhook.test.ts: valid / tampered / wrong-secret / missing / stale.
- Reconcile Future.md + Engineering_Reference_Manual.md to the real scheme.

Verified against the live account: the entitlement identifier is now closer_premium,
so events match entitlementLogic. Build + 80 tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 03:43:59 -05:00
null 48217dd716 chore(security): gitignore revenucat_secret_api.md (holds a RevenueCat secret key)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 03:43:43 -05:00
null 8cb23ea244 build(billing): bump RevenueCat SDK 10.12.0 → 10.13.0
Routine SDK patch bump. Resolves + compileDebugKotlin clean; no API changes needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 01:45:36 -05:00
null 2c3c2df8c4 fix(copy): reconcile divergent premium benefit lists + drop inaccurate claim
The paywall and subscription screens showed two different premium benefit lists (centralizing
into CloserCopy surfaced the drift). Unified to a single CloserCopy.premiumBenefits used by both.

Also an accuracy fix: dropped "Exportable memories" — it directly contradicts the app's own
privacy copy ("Closer does not currently offer a data export", strings.xml privacy_no_export_body),
i.e. a benefit the app explicitly does not provide. Remaining items are all verified real features
(QuestionComposer, Connection Challenges, Desire Sync, Memory Lane, date planning, answer history).

compileDebugKotlin clean; single source of truth for both surfaces going forward.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 00:50:29 -05:00
null 42fba641f1 chore(cleanup): remove the unused server/ Express backend (Tier 3)
server/ was a standalone Express webhook/health service (couples-connect-server, 17 files)
that duplicated the Firebase Cloud Functions — entitlement logic, FCM sends, an answer
listener, a webhook route — and was documented as "not client-facing… most teams will not
need it." It hadn't been touched since 2026-06-20 (pre-dating the whole v2 Functions
migration), had no deploy/CI wiring, and was a standing source of "which backend is real?"
confusion. Cloud Functions are the sole backend.

Removed the directory + all doc references so nothing dangles: README + Engineering Reference
Manual directory trees and the "Optional Express server" section; the iOS ARCHITECTURE_AUDIT
intro, its Section 8 (now a "removed" note), and the file-count table.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 00:48:02 -05:00