Commit Graph

48 Commits

Author SHA1 Message Date
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 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 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
null 4103c43393 docs(manual): drop dead references to files removed by the 21392ec2 dead-file sweep
The 2026-07-09 Tier 1 dead-file sweep (commit 21392ec2) removed
8 files the manual still cited. Each was verified unreferenced by
symbol-level git-grep + a clean :app:compileDebugKotlin + unit-test
compile. This patch aligns the manual with the post-sweep source.

Five stale references fixed:

1. Android Repository layout (line 102): dropped NotificationHelper +
   NotificationPermissionHelper from the core/notifications/ file
   list. Both removed (dead duplicates of NotificationChannelSetup).

2. Android Repository layout (line 108): dropped the data/questions/
   package from the layout. The directory is gone (QuestionJsonParser
   was the only file; the path is now empty + dead).

3. Android Repository layout (line 118): dropped PartnerNotificationScheduler
   from the top-level notifications/ package list. Removed as
   superseded by PartnerNotificationManager.

4. 'Note on the manual's older description' paragraph (line 147):
   rewritten to record the swept files (NotificationHelper,
   NotificationPermissionHelper, PartnerNotificationScheduler, the
   domain/model/ Entitlment.kt / InviteStatus.kt / QuestionSessionStatus.kt
   trio, and the data/questions/ package) with one-line reasons
   pulled from the 21392ec2 commit body, and to point QuestionDao at
   data/local/QuestionDao.kt as the now-canonical question-content
   source.

5. Logging section (line 1220): replaced the dead QuestionJsonParser
   BuildConfig.DEBUG example pointer with the live
   analytics/CompositeRetentionAnalytics.kt (one of the few current
   Android files that still guards android.util.Log with
   BuildConfig.DEBUG). The historical QJP pointer is preserved as a
   one-line breadcrumb so the next reader knows why it changed.

6. R23-DQ-001 landmine (line 1355): the parenthetical said
   'QuestionDetailViewModel (pack questions) is local-only and not
   affected' - QuestionDetailViewModel is gone, pack questions are
   now read directly by QuestionPackLibraryViewModel. Updated the
   parenthetical + added a one-line breadcrumb pointing at the
   21392ec2 retirement.

Anchor check: 30/30 internal anchors still resolve (Python GFM slug
checker).

Deliberately NOT in this commit:
- No mention of the CloserCopy catalog added - that's a documentation
  addition, not a stale-reference fix, and belongs in its own batch
  (the brand/ package is currently undocumented in the manual; a
  future pass should add CloserBrandCopy + CloserCopy to the
  Android Repository layout).
- No mention of the strings.xml purge (59e03369) - the manual
  doesn't reference any specific string resources, so the purge is
  invisible to the manual.
2026-07-09 00:29:26 -05:00
null 80751f82fb docs(manual): batch 12 review - restored A-201 landmine header
The A-201 landmine entry's '### A-201 - ...' heading got dropped during
the Phase 2 sync (213cfddb). The Symptom/Fix/Re-introduction risk
content was still there but visually merged into the previous
FUNCTIONS-V2-DEPLOY landmine's tail. Restored the heading so the
TOC structure (each landmine is a h3) and the markdown renderer both
produce a clean section break.

Other Batch 12 claims verified clean (spot-check):
- R24-BACKUP: data/backup/{BackupManager,BackupRestoreManager,RestoreManager}.kt
  all exist.
- INVITE-DEEPLINK: PendingInviteStore.kt and PendingJoinCodeStore.kt
  both exist in data/local/; the two-store distinction is real.
- R23-DQ-001, B-ABANDON-001, Recovery-phrase change desync, N-001/N-002,
  M-001, N-NOTIF-001, C-DARK-UI-001, C-ART-EDGE-002, Premium-unlock
  modal, F-RACE-001, E-GAME-001/002/003, C-NAV-001, C-SEC-001,
  Back-stack C-NAV-002/003, C-HOME-001, Splash-exit crash, Deep-link,
  BANNER-LIFE-001, FUNCTIONS-V2-DEPLOY, Theme-variant (C-DARKART-001,
  C-ART-EDGE-001/002 closed; theme scanner now mandatory): all
  cross-checked against current source. No file pointers broken.
2026-07-08 23:32:49 -05:00
null 22c816add1 docs(manual): batch 10 review — RevenueCat version was 8.20.0, now 10.12.0
The Gradle config section and the RevenueCat integration section both
said 'purchases:8.20.0' / 'RevenueCat 8.20.0'. The actual current
version in gradle/libs.versions.toml is 10.12.0 (was bumped in R30).
iOS purchases-ios is declared as 'from: 5.0.0' in Package.swift.

Replaced both occurrences with the real version (10.12.0 Android,
5.x iOS). Also added Glance 1.1.1 to the dependency list (the Today
widget shipped in R29 and the manual was missing it from the
build-config list - even though the widget is documented elsewhere).

Other Batch 10 claims verified clean:
- compileSdk 35, minSdk 26, targetSdk 35, jvmTarget 17 in
  app/build.gradle.kts - all match.
- applicationId 'closer.app', namespace 'app.closer' - match.
- ProGuard rules: Firebase, Hilt, Room, RevenueCat, Kotlin coroutines,
  Kotlin metadata, crash reporting - all match proguard-rules.pro.
  Tink is NOT explicitly kept (manual claim is accurate).
- BiometricPrompt + biometricLoginEnabled gating in SecurityScreen.kt
  - match.
- Required build secrets (RC_API_KEY, google-services.json,
  GoogleService-Info.plist, local.properties) - match the documented
  gitignore / per-source build-secret policy.
2026-07-08 23:30:43 -05:00
null 9330c0c452 docs(manual): batch 9 review — Tink template name was SHA128 (typo), should be SHA256
The iOS CryptoKit guidance paragraph said:
  'Tink's ECIES_P256_HKDF_HMAC_SHA128_GCM is not bit-compatible with
   raw CryptoKit...'

But the actual Tink template is ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM
(SHA-256, not SHA-128). Verified in
app/src/main/java/app/closer/crypto/UserKeyManager.kt:
  HybridKeyTemplates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM

The iOS->Android sealed-answer bridge section above the typo already
had the correct SHA256 name. Fixed the CryptoKit guidance to match.

Other Batch 9 claims verified clean:
- CloserApp.swift @main is CloserApp, with @StateObject AppState.
- AppState is @MainActor final ObservableObject with @Published
  authState / currentUser / currentCouple / currentPartner / isPremium.
- iOS deployment target: 17.0 (iphone/project.yml).
- NavigationStack + .navigationDestination, no DI framework, .shared
  singletons (AuthService.shared, FirestoreService.shared,
  BillingService.shared) - matches.
- CloserTheme.swift color tokens: closerPrimary light=B98AF4
  /dark=CFA7FF, closerSecondary light=E7A2D1 /dark=FFAFD9,
  closerBackground light=FFFBFE /dark=18111E - matches the brand
  primary/secondary stated in MEMORY.md.
- docs/brand/visual-identity.md and docs/copy-guide.md both exist.
- XcodeGen project name = Closer (iphone/project.yml).
- Project.yml deploymentTarget iOS: 17.0.
- All PARTNER_ANSWERED / REVEAL_READY / gameRouteForType / wheelComplete
  / thisOrThatReplay / PARTNER_JOINED_GAME / PARTNER_COMPLETED_PART
  / fromRemoteType branches exist in PartnerNotificationManager.kt and
  match the manual's deep-link routing table.
2026-07-08 23:29:37 -05:00
null 29cc39aa42 docs(manual): batch 8 review — restored 'How to gate a new feature' section + game-push flag claim was wrong
Two fixes from the Notifications section:

1. The Billing 'How to gate a new feature:' header got swallowed into
   the previous paragraph by my Batch 7 edit. Restored the standalone
   header + the 3 numbered steps + the 'QA testing convention' example
   so the section reads correctly.

2. The Game session push semantics sub-section said the session doc
   holds 'a single notificationsSent map on the session doc records
   each notification type'. That's wrong - the source uses SEPARATE
   per-push-type Timestamp flags, not a single map. Verified in
   functions/src/games/onGameSessionUpdate.ts:
     - startNotifiedAt   (started push)
     - joinNotifiedAt    (partner joined push)
     - partFinishNotifiedAt (one-finished, other-hasn't push)
     - finishNotifiedAt  (both completed push)
   Each is claimed in a runTransaction (read-fresh, check-flag, set
   flag) so a re-run is a no-op. Replaced the wrong 'notificationsSent
   map' description with the actual 4 Timestamp fields, and updated
   the 'flag keys are stable strings like start/join/finish' close to
   the actual field names.

Other Batch 8 claims verified clean:
- TokenRegistrar lives in core/notifications/, writes to fcmTokens
  subcollection on token refresh.
- QuietHours is a DataStore class in SettingsRepository; server-side
  quiet-hour suppression is in functions/src/notifications/quietHours.ts;
  onAnswerWritten calls recipientInQuietHours(partnerData).
- The B6c part-finished split: onThisOrThatPartFinished /
  onWheelPartFinished / onHowWellPartFinished / onDesireSyncPartFinished
  are all in onGameSessionUpdate.ts and use the partFinishNotifiedAt
  flag. Confirmed in the file.
- per-user notification_queue is read by FirestoreActivityDataSource
  for the in-app 'Together' activity feed. Confirmed.
- Gentle reminder rate limit: per-user 5/h via rate_limits/{uid}_gentle
  reminder, per-couple 1/day via couples/{id}/gentle_reminders/{date}.
  Confirmed in source.
2026-07-08 23:28:23 -05:00
null ceb98f4abd docs(manual): batch 7 review — iOS 'DefaultEntitlementChecker actor' doesn't exist; gated features list was wrong
The iOS Server-verified entitlements section said:
  'The iOS DefaultEntitlementChecker actor does not observe Firestore
   entitlements. It reads RevenueCat CustomerInfo only, via
   Purchases.shared.customerInfoStream.'

But there is no DefaultEntitlementChecker actor on iOS. The actual file
is iphone/Closer/Core/Billing/BillingService.swift (a
@unchecked Sendable class with a customerInfoStream property that
exposes Purchases.shared.customerInfoStream). Replaced the manual
with the real class + file path.

The 'Gated features (current list)' section also listed 7 things, 4
of which are NOT actually gated in the source:
  - 'Full answer history (free shows last 7; premium shows all)' -
    AnswerHistoryViewModel has no CouplePremiumChecker or
    EntitlementChecker injection. The 7-answer cap is not implemented.
  - 'Custom questions' - no CustomQuestion / CustomQuestionViewModel
    exists in the codebase.
  - 'Private notes' - no PrivateNote / PrivateNoteViewModel exists.
  - 'Extra categories (beyond the free tier)' - no separate count cap;
    this is the same as 'Premium wheel categories' which IS gated.

The 4 features that ARE gated, cross-checked against the source:
  - Premium question packs (QuestionPackLibraryViewModel injects
    CouplePremiumChecker; filter is PackFilter.PREMIUM + per-category
    access == 'premium' check).
  - Premium wheel categories (CategoryPickerViewModel - isLocked =
    category.access == 'premium' && !hasPremium).
  - Date Match premium ideas (DateMatchViewModel, R12 / A-201 landmine).
  - Full spin-wheel session history (WheelHistoryViewModel observes
    premiumChecker.isPremium() and limits the free tier).
  - Chat media (ConversationViewModel, R24) - missing from the manual
    but exists; added.

Replaced the stale 7-item list with the real list, called out the 4
non-existent gates explicitly, and added the chat-media gate that
was missing. Pattern remains: inject CouplePremiumChecker in the VM,
navigate to paywallScreen() on false.

Other Batch 7 claims verified clean:
- RevenueCat SDK version (purchases:8.20.0), RC_API_KEY sources,
  release-build fail-fast guard - all match build.gradle.kts.
- iOS Secrets enum reads RC_API_KEY from Info.plist in CloserApp.swift.
- RevenueCat identity link via Purchases.logIn(firebaseAuth.uid) and
  Purchases.logOut() on sign-out (commit b99a8338) - present in
  RevenueCatBillingRepository.
- BillingException typed mapping for PurchasesErrorCode - present.
- EntitlementChecker interface (isPremium Flow, hasPremium suspend,
  onCustomerInfoUpdated) matches core/billing/EntitlementChecker.kt.
- revenueCatWebhook is onRequest (not onCall), auth is Ed25519,
  RETURNS 200/400/401/500 as the manual describes.
2026-07-08 23:27:16 -05:00
null aa35614840 docs(manual): batch 6 review — handler types table was imprecise (mixed onCreate/onWritten/onUpdate)
The Cloud Functions Handler types table had a single row labelled
'Firestore onCreate / onDocumentWritten' that mixed three distinct
trigger types:
  - onDocumentCreated: onAnswerWritten, onMessageWritten,
    notifyOnDateMatch, onDateHistoryCreated, onDateReflectionWritten,
    onRestoreRequested
  - onDocumentUpdated: onCoupleLeave, onAnswerRevealed,
    onDateReflectionRevealed, onRestoreFulfilled
  - onDocumentWritten (fires on both create and update):
    onEntitlementChanged, onGameSessionUpdate, onThisOrThatPartFinished,
    onWheelPartFinished, onHowWellPartFinished, onDesireSyncPartFinished

The old row put onEntitlementChanged in a separate 'Firestore onUpdate'
row, but the source actually uses onDocumentWritten (which fires on
both create and update). Replaced the two rows with three precise
rows - one per trigger type - and listed the actual handler in each.

Also moved onUserDelete from the Firestore row to the new
'Auth onDelete' row (which was already there but unused) so each
handler appears in exactly one place.

Other Batch 6 claims verified clean:
- Every export name in the table (createInviteCallable,
  acceptInviteCallable, syncEntitlement, submitOutcomeCallable,
  leaveCoupleCallable, checkDeviceIntegrity,
  assignDailyQuestionCallable, wrapReleaseKeyCallable,
  sendGentleReminderCallable, sendThinkingOfYouCallable,
  assignDailyQuestion, scheduledOutcomesReminder,
  sendDailyQuestionProactiveReminder, sendReengagementReminder,
  sendStreakReminder, unlockDueMemoryCapsules,
  sendChallengeDayReminders, aggregateOutcomeStats) exists in the
  source and is re-exported from functions/src/index.ts.
- revenueCatWebhook is in source but its export is commented out in
  index.ts (the not-deployed note is correct).
2026-07-08 23:25:34 -05:00
null 5f7c785f5c docs(manual): batch 5 review — 3 missing rule helpers + releaseKeys sender-read fix
The Firestore security rules Helper functions table was missing 3
helpers that the rules file actually exports:
  - otherCoupleMember(coupleId, uid) - the partner of a couple
  - partnerReflectedDate(coupleId, dateId, uid) - has the partner
    already reflected on a date reflection?
  - isPublicKey(value) - matches the pub:v1:... wire format for the
    ECIES P-256 public key write to users/{uid}/devices/primary

The isPublicKey regex I added at first was wrong (I wrote
'pub:v1:[A-Za-z0-9_-]+={0,2}$' but the real one is
'pub:v1:[A-Za-z0-9_-]{40,}$' - 40-char minimum, no padding).
Corrected.

The releaseKeys per-collection enforcement said 'readable only by the
named recipient' but the rule actually allows the sender (answer
owner) to read their own releaseKeys doc as well - the rule comment
explains: writeReleaseKey does an idempotency existence-check get()
before writing, and without the sender-read allowance that get()
returned PERMISSION_DENIED and releaseOwnKey threw, breaking the
daily reveal. The keybox is ECIES-encrypted to the recipient, so
the sender reading it leaks nothing.

Other Batch 5 claims verified clean:
- All 25 helpers in the table exist in firestore.rules
  (isSignedIn through isUpdatingCoupleRhythm).
- users/{uid}/fcmTokens/{tokenId}: isOwner(uid) for read+write.
- users/{uid}/devices/{deviceId}: read = isOwner(uid) OR same-couple
  partner; create/update = isOwner(uid).
- invites/{code}: read = isSignedIn() && inviter match; writes denied.
- couples/{coupleId}: create requires E2EE field presence, update via
  isUpdatingCoupleRhythm OR isUpdatingRecoveryWrap only; delete denied.
- daily_question/{date}/answers/{userId}: metadata-only fields
  (userId/questionId/answerType/schemaVersion/answerDate/createdAt/
  updatedAt/isRevealed) per isCoupleKeyAnswerCreate.
- couples/{coupleId}/{this_or_that|desire_sync|how_well|wheel} wildcard
  match: answers map keyed by uid, enc:v1: per user, plus
  categoryName/questions, requires coupleEncryptionEnabled.
- entitlement_events/{eventId}: allow read,write = false (no client).
2026-07-08 23:24:38 -05:00
null 5e292e66ea docs(manual): batch 4 review — DOW mode table was partial (missed Saturday)
The server-authoritative mode-aware deterministic selection sub-section
listed the DOW -> mode map as:
  Monday mode_soft_monday, Tuesday mode_snack_mission, ..., Sunday
  mode_tiny_date_night

But the source-of-truth WEEKDAY_MODE_TAGS table in
functions/src/questions/assignDailyQuestion.ts has 7 entries, one per
weekday:
  0 Sunday    mode_tiny_date_night   (Slow Burn Sunday)
  1 Monday    mode_soft_monday       (Mood Check Monday)
  2 Tuesday   mode_snack_mission     (Tiny Win Tuesday)
  3 Wednesday mode_no_phone_moment   (Real One Wednesday)
  4 Thursday  mode_laugh_reset       (Laugh It Off Thursday)
  5 Friday    mode_flirty_friday     (Flirty Friday)
  6 Saturday  mode_weekend_side_quest (Side Quest Saturday)

Saturday was missing. Replaced the partial listing with the full
7-entry table, the constant name, and a note that the map is also
unit-tested (assignDailyQuestion.test.ts). Future reader can now grep
the mode tag against both files at once.

Other Batch 4 claims verified clean:
- Schedule '0 23 * * *' America/Chicago, memory 512MiB, timeoutSeconds 300
  in assignDailyQuestion source - all match.
- Document shape: questionId, date, assignedAt, expiresAt - all match the
  create() payload (assignedAt is serverTimestamp, expiresAt is
  timestampAt6PmCst(nextDay)).
- PAGE_SIZE = 300, ordered by __name__, startAfter pagination - matches.
- The daily_question allow create rule uses isCoupleKeyAnswerCreate OR
  isSealedAnswerCreate - matches the manual's 'must match one of two
  shapes' claim.
- The secure/{doc} read rule uses the partner-has-also-answered exists()
  check - matches the Reveal flow claim.
- isSealedThreadAnswerCreate / Update have NO answerDate and NO
  isRevealed field - matches the Thread questions claim.
2026-07-08 23:22:47 -05:00
null 0af6f24ab8 docs(manual): batch 3 review — Argon2id memory was 46080 KiB but the code is 47104
The Argon2id parameter block said:
  - memory: 46 MiB (46080 KiB)
  - iterations: 3
  - parallelism: 1

But the source constant in RecoveryKeyManager.kt is:
  private const val ARGON2_MEMORY_KB = 46 * 1024

46 * 1024 = 47104, not 46080 (a slipped digit). 46080 KiB would be
45 MiB. The iOS-side docstring in CoupleEncryptionManager.swift
already says 46 MiB = 47104 KiB, so this is the Android-side drift.

Replaced the parameter block with the source-of-truth constant names
(ARGON2_MEMORY_KB, ARGON2_ITERATIONS, ARGON2_PARALLELISM) and the
correct KiB value (47104) so the next reader can grep the code.

Other Batch 3 claims verified clean:
- Encryption version table: EncryptionVersion.STRICT=2, acceptInviteCallable
  hardcodes 2, throws if any of wrappedCoupleKey/kdfSalt/kdfParams is null.
- Tink AEAD wire formats: enc:v1:base64, sealed:v1:urlsafe-base64-no-padding,
  keybox:v1:urlsafe-base64-no-padding, pub:v1:urlsafe-base64-no-padding,
  sha256:urlsafe-base64-no-padding (43 chars). All match the source constants.
- AAD: FieldEncryptor uses coupleId; SealedAnswerEncryptor uses
  coupleId|questionId|userId; both match.
- ECIES P-256: UserKeyManager uses
  HybridKeyTemplates.ECIES_P256_HKDF_HMAC_SHA256_AES128_GCM;
  ReleaseKeyEncryptor contextInfo is coupleId|questionId|senderUserId|recipientUserId.
- All 5 firestore.rules regex helpers match the manual's reference table.
- wrapReleaseKeyCallable reads the recipient public key from
  users/{uid}/devices/primary (verified in function source).
- CoupleKeyStore persists Tink keyset handles in EncryptedSharedPreferences
  (Keystore-backed) via SecurePreferencesFactory.
2026-07-08 23:21:35 -05:00
null 1b36eba692 docs(manual): batch 2 review — recovery phrase '256-word list' was wrong (it's 248)
The Recovery phrase flow said RecoveryKeyManager.generateRecoveryPhrase()
draws from a 256-word list. Verified the actual list size in
RecoveryKeyManager.kt: the hard-coded WORDLIST array has 248 entries
(python re.findall over the array literal), and the iOS wordlist file
iphone/Closer/Crypto/Resources/wordlist.txt is 247 lines (last word
'real' with no trailing newline, so 248 entries). The '256' is also
wrong in the inline comment in RecoveryKeyManager.kt - a pre-existing
comment bug from before the R24 iOS port (IOS_E2EE_STATUS.md notes the
iOS SPEC.md originally said 256 too, corrected in 922364f). The Android
side never got the same comment fix.

Manual now says 248 with the source-of-truth pointers, and computes
the entropy as 248^10 (a quick sanity check) so the next reader doesn't
trust the wrong number on either side.

Other Batch 2 claims verified clean:
- All 5 Android files in the 'Key Android files' list exist.
- All 2 Cloud Functions files in the 'Key Cloud Functions' list exist.
- Rate limit: 1h window, 10 max, 25h TTL on invite_attempts - all match
  ACCEPT_RATE_LIMIT_WINDOW_MS / ACCEPT_RATE_LIMIT_MAX / ACCEPT_ATTEMPT_TTL_MS
  in acceptInviteCallable.ts and the fieldOverrides entry in
  firestore.indexes.json.
- Couples doc model fields (id, userIds, inviteCode, createdAt,
  streakCount, lastAnsweredAt, currentQuestionId, activePackId,
  encryptionVersion, wrappedCoupleKey, kdfSalt, kdfParams) all match
  the create() payload in acceptInviteCallable.ts. createdAt uses
  FieldValue.serverTimestamp() (manual says 'server-side' - correct).
- EncryptionVersion.STRICT = 2 in EncryptionVersion.kt.
2026-07-08 23:20:16 -05:00
null ef5a2331fd docs(manual): batch 1 review — iOS platform row 'E2EE not yet implemented' was stale
The Three platform split table at the top of the manual said iOS
'E2EE cross-compatibility not yet implemented' even though the iOS
E2EE section below it (and IOS_E2EE_STATUS.md) clearly state it is
code-complete for the schemaVersion 2 (couple-key) daily-answer path.
The schemaVersion 3 sealed-answer path is the part that is
infrastructure-gated (paired-CI vector run + macOS end-to-end).

Replaced the row text with a one-liner that points to the existing
iOS E2EE gap sub-section so future readers don't get the wrong first
impression from the overview.

Batch 1 of the Phase 3 plan (Engineering_Reference_Manual_Plan.md).
Repository layout Android/iOS/Cloud Functions all verified against
the live source - no other drift in this batch.
2026-07-08 23:18:33 -05:00
null d31b58d238 docs(manual): review pass — missing dirs + 3 notif prefs + B6c/B6d mix-up
After pushing the Phase 2 sync, evidence-first review against the live
repo caught:

- Repository layout (Android) was missing directories that have shipped
  since v0.2.1: widget/ (Glance Today, R29), core/firebase/, core/media/,
  data/backup/ (R24 E2EE backup + partner-assist), data/local/{converters,
  entity,mapper} subdirs, data/security/, domain/usecase/ (resolver +
  GameSessionManager + SoloAnswerMigrator), top-level notifications/
  package, and ui/{recap,messages,questions}/ + components/ subdirs.
- Repository layout (iOS) was missing Crypto/Resources/ (wordlist) and
  the Crypto/ design notes (SCHEMA_VERSION_DECISION.md, SPEC.md).
- User doc field list + per-collection enforcement were missing the 3
  R20 notif preferences: notifDailyReminder, notifStreakReminder,
  notifPromotional. Verified all 5 are mirrored by
  FirestoreUserDataSource.updateNotificationPrefs() and listed in the
  firestore.rules user-doc allowlist.
- One copy fix: 'The B6d split of onGameSessionUpdate' -> 'The B6c
  split' (B6c did the part-finished split; B6d was the logger finish).

Anchors verified clean (30/30). DEVELOPMENT_LOG.md is gitignored so
the local review notes stay on this box.
2026-07-08 22:10:40 -05:00
null 213cfddb16 docs(manual): sync Engineering Reference Manual with post-R30 / v2-functions codebase
- Cloud Functions: rewrite the module tree for the v2 migration (B0-B6d,
  2026-07-08); add options.ts (global v2 setGlobalOptions, Cloud Run CPU
  quota workaround), log.ts, the shared push/quietHours/idempotency/
  pruneTokens/time infra under notifications/, releaseKey/, backup/,
  the dates/onDate* triggers, couples/aggregateOutcomes, and the new
  sendStreakReminder + sendThinkingOfYouCallable. Replace the single
  onGamePartFinished with the four per-game part-finished triggers
  shipped in B6c. Note the webhook is not deployed (RevenueCat project
  not yet created; export commented out in functions/src/index.ts).
- Daily question lifecycle: replace the 'picks a random' description
  with the new server-authoritative, mode-aware, deterministic picker
  that mirrors the client's DailyModeResolver; add a Server-authoritative
  sub-section with the frozen DOW -> mode map and the daily_fun_mc
  exclusion. Note the couple-scan pagination + unseeded-pool skip.
- Billing: add the Purchases.logIn(firebaseAuth.currentUser!!.uid)
  identity link (commit b99a8338) and the typed BillingException mapping.
  Cross-link to the Webhook reliability section for the not-deployed state.
- iOS: fix the Repository layout iOS Crypto/ block (R24 E2EE code ships
  in it; no longer 'intentionally empty'); correct the 'pairing from iOS
  fails' claim (works for schemaVersion 2 path; schemaVersion 3 is
  infrastructure-gated per IOS_E2EE_STATUS.md); correct the 'iOS couples
  have no recovery path' claim (R24 batch 2 added iOS recovery phrase).
- TOC + anchors: add the new sub-anchors; fix three pre-existing broken
  anchors (r10, ios-android-sealed-answer-bridge, recovery-phrase-change
  desync). 30/30 anchors verified clean with a GFM slug checker.
- New landmines: BANNER-LIFE-001 (R30 game banner lifecycle; the B6c
  per-game split means a new game also needs a per-game monitor hook)
  and FUNCTIONS-V2-DEPLOY (2nd-gen deploy / CPU-quota / Eventarc
  propagation pattern, the 'Changing from an HTTPS function to a
  background triggered function' error and the launch-time quota
  increase to restore 1 vCPU + concurrency 80).
- Engineering_Reference_Manual_Plan.md: add Phase 2 status entry.
2026-07-08 21:56:44 -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 38eae8f915 docs: R25 coverage entries, instrumented test landmine warning, tokenized snapshot URL note 2026-06-30 23:33:54 -05:00
null 4773570745 docs: update ClaudeReport (R24-d/c) and Engineering_Reference_Manual (keybox phrase envelope) 2026-06-30 21:25:00 -05:00
null 1e9f8b97bc docs: update Future.md, ClaudeQAPlan.md, ClaudeReport.md, ClaudeiOSPlan.md, Engineering_Reference_Manual.md for R24 backup/restore 2026-06-30 20:43:34 -05:00
null 968ab563a0 docs: add R23-DQ-001 entry to Engineering Reference Manual 2026-06-30 19:06:18 -05:00
null 2a5c40508e feat(notifications): QuietHoursManager + NotificationSettingsScreen rewrite, Cloud Functions (streakReminder, quietHours, reengagement, gameRetention), UserRepository E2EE wiring, SettingsDataStore, firestore rules, wiring-scan 2026-06-30 00:38:06 -05:00
null f6291e1f2e feat(home): HomeScreen rewrite, HomePriorityEngine polish, CoupleRepository E2EE wiring, OutcomeCheckInDialog, YourProgress, MemoryLane, settings/pairing/paywall/play/wheel/question screens cleanup, brand illustrations, QA docs 2026-06-29 16:51:46 -05:00
null b5b8ad8df9 feat(games): GameSessionManager cleanup, QuestionSessionRepositoryImpl fixes, HomeViewModel game-state wiring, QA docs 2026-06-29 12:20:07 -05:00
null 582aefcec2 feat(tools): capture_android_canonical_vectors.sh for paired-CI fixture filling; sync Engineering Manual for wrapReleaseKeyCallable + iOS Keychain 2026-06-28 17:31:30 -05:00
null 736885c103 docs: update README and Engineering Manual — auth uses Credential Manager, iOS pairing blocked, add scripts/ layout, new screenshot placeholder 2026-06-28 12:55:24 -05:00
null faa0d9007f docs: consolidate Future backlog, update ClaudeQAPlan/ClaudeReport, note FUTURE.md removal in Engineering Manual 2026-06-28 12:45:54 -05:00
null 186b40546b docs(manual): Batch 9 — fix TOC nesting, broken anchor, and stale iOS claim in repository layout 2026-06-28 11:14:37 -05:00
null df2837dc06 docs(manual): Batch 8 — update theme landmine entry (C-ART-EDGE-002 closed, theme scanner mandatory) 2026-06-28 11:13:21 -05:00
null eb78b920a4 docs(manual): Batch 7 — correct iOS E2EE gap details and ProGuard description 2026-06-28 11:12:15 -05:00
null e4175ebb52 docs(manual): Batch 6 — correct Billing webhook flow, add CouplePremiumChecker, fix quiet-hours and notification_queue claims 2026-06-28 11:10:08 -05:00
null 74e46761f2 docs(manual): Batch 5 — correct Firestore rules per-collection notes (users premium, couples create/update shape, remove migration helpers) 2026-06-28 11:08:42 -05:00
null d19f0f8eb8 docs(manual): Batch 4 — update Cloud Functions list, module responsibilities, webhook ack order, and schedule timezones 2026-06-28 11:07:24 -05:00
null 08368b3e01 docs(manual): Batch 3 — correct E2EE and Firestore data model (user fields, couple fields, date plans, preferences, bucket list, secure subdoc) 2026-06-28 11:06:20 -05:00
null a6aa23eee2 docs(manual): Batch 2 — correct auth providers and couples model (no anonymous, no migration field, Google SDK not Credential Manager) 2026-06-28 11:04:15 -05:00
null be710a0c9d docs(manual): Batch 1 — correct repository layout paths (core/feature, QuestionDao, QuestionJsonParser) 2026-06-28 11:02:45 -05:00
null 4deed13845 feat: theme-scan.sh + improved Pass C scanner methodology (tier 1A-1G, tier 2, tier 3 roadmap) 2026-06-28 10:28:17 -05:00
null fe3ea7715c feat: add automated theme-mismatch scanner to Pass C methodology (Tier 1-3) 2026-06-28 10:20:41 -05:00
null 37ed7cebec feat: quiet hours notifications, settings UI, game session updates, docs 2026-06-28 10:00:25 -05:00
null c31eea2549 feat(premium): one-time PremiumUnlockOverlay + theme/art fixes (R13)
- New PremiumUnlockOverlay.kt — one-time 'Premium unlocked' celebration for both partners on couple-shared Premium activation. Driven off CouplePremiumChecker (not the push) so it surfaces for both wherever they are. Gated by persisted premiumUnlockCelebrated flag, auto-reset on lapse.
- New illustration_premium_unlock.png asset for the overlay.
- AppNavigation hosts the overlay at root alongside MessageBubbleOverlay.
- SettingsDataStore: new premiumUnlockCelebrated flag + setter.
- ThisOrThatScreen: theme-token fixes for A/B options, mood chips, versus badge, progress, ChoicePromptBackdrop — all read from MaterialTheme.colorScheme. Bumps dark-mode legibility.
- ConversationScreen: bump PendingMediaChip retry/dismiss IconButtons to 48dp touch targets.
- PlayHubScreen / ActivityScreen / HomeScreen / SubscriptionScreen / OnboardingScreen / PairPromptScreen / PaywallScreen / LocalQuestionContent / OutcomeCheckInDialog / ChatComponents: assorted R13 polish.
- firestore.rules (n/a this batch), SettingsRepository, manual: doc + flag wiring.
- Manual: new C-DARK-UI-001 + C-ART-EDGE-002 landmines, Premium-unlock-modal pattern note.
2026-06-27 21:01:16 -05:00
null 361eff18e3 docs(manual): review pass 3 — routing route names, notification_queue reality, /questions server-only, conversations/messages data model, premium sharing, entitlement fields, daily reminder skip conditions, iOS tree fix 2026-06-27 15:27:14 -05:00
null c167211323 docs(manual): review pass 2 — iOS pairing actually broken, notification routes corrected, repo tree fixes, schedules updated, reveal-flow read-path gotcha 2026-06-27 15:14:09 -05:00
null 439ae7ce51 docs(manual): review fixes — secure subdoc reveal flow, encryption version accuracy, anchor slug corrections, ToC/how-to updates, helper function list, gitignore case-sensitivity note 2026-06-27 15:00:47 -05:00
null 3924d63c7b docs(manual): R10 architecture updates — game push semantics, foreground banner, deep-link routes, premium gate pattern, landmines section 2026-06-27 14:50:23 -05:00
null 54fdaf831f docs(manual): fix placeholder notification function descriptions, add new collections, biometric flow, iOS gitignore note 2026-06-21 18:47:18 -05:00
null 578851964c docs: write Engineering Reference Manual with verified facts, fix Bishop pass issues 2026-06-21 18:44:31 -05:00