Commit Graph

92 Commits

Author SHA1 Message Date
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 7d015e3ab9 fix(functions): gcf_gen1 fractional CPU to fit the Cloud Run CPU quota
Deploys kept failing container healthchecks with "Quota exceeded for total allowable CPU
per project per region" even in small batches: v2 gives every instance a full vCPU (needed
for concurrency 80), and ~35 services at 1 vCPU exceeds this new project's default Cloud Run
CPU quota under any accounting. cpu:'gcf_gen1' restores the gen1 fractional tiers
(256MiB → 1/6 vCPU) — a 6x smaller footprint, identical to how these functions ran on gen1.
Concurrency must be 1 with cpu<1; costless at dev scale. At launch: raise the quota, drop
these two options to restore full-vCPU concurrency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 03:04:26 -05:00
null 9b62957052 fix(functions): lower global maxInstances 20→5 to fit the Cloud Run CPU quota
2nd-gen deploy failed with "Quota exceeded for total allowable CPU per project per region":
each function is a Cloud Run service and the regional CPU-allocation quota is charged as the
sum of (maxInstances × vCPU) across all functions. At maxInstances 20 × ~34 v2 functions =
~680 vCPU, over this new project's default (~560). Dropping to 5 → 170 vCPU, well under.

5 instances × ~80 concurrent requests still serves ~400 in flight — fine pre-launch. For
production, request a Cloud Run CPU quota increase and raise this back up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 02:44:06 -05:00
null 30eaa0cd61 build(functions): keep revenueCatWebhook out of the deploy until RevenueCat exists
RevenueCat isn't set up, so exporting revenueCatWebhook forced a Secret Manager entry:
defineSecret('REVENUECAT_SIGNING_KEY') runs at module load, and Firebase validates every
declared secret across the whole codebase at deploy time (even functions excluded via --only),
failing with "no latest version of the secret". Comment out the export so the file isn't loaded
during discovery — no secret, no validation. revenueCatWebhook.ts (already migrated to v2) is
untouched; re-enable by uncommenting the export, seeding the real key, and deploying it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 02:01:23 -05:00
null bf4cc41cd9 refactor(functions): B6d finish logger migration in shared helpers
Migrate the last console.* call sites (the shared helpers entitlementLogic.ts and
pruneTokens.ts) to firebase-functions/logger, completing the structured-logging sweep.
Zero console.* remain under functions/src.

Final verification of the whole v1→v2 migration:
- tsc clean under firebase-functions v7.2.5; 70 jest tests green.
- Emulator discovery loads all 36 functions in us-central1 with 0 errors and no
  outdated-SDK warning; onUserDelete remains a v1 auth trigger, the rest are v2.
- Grep gates clean: no functions.https.onCall/.firestore.document/.pubsub.schedule,
  no context.auth/app/params, no console.* in src, no messaging.send outside push.ts,
  no raw FCM tokens in any log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 00:10:10 -05:00
null 4c077c4d7c refactor(functions): B6c split game part-finished trigger + reorder reads (#11)
Split the single broad onGamePartFinished (couples/{coupleId}/{gameType}/{sessionId}
wildcard, which fired a no-op invocation on every write to ANY couple subcollection) into
four narrow, explicitly-pathed triggers sharing one handler:
onThisOrThatPartFinished, onWheelPartFinished, onHowWellPartFinished, onDesireSyncPartFinished.
Behavior is identical for the four game collections; the spurious invocations for
messages/reactions/etc. are eliminated. (Background triggers have no client name dependency;
the old export is dropped and the four deploy fresh — the deploy runbook already accounts for
this.)

onGameSessionUpdate: move the `!change.after.exists` deletion guard ABOVE its four reads
(session/couple/userA/userB) so a delete/no-op event returns before doing any reads. The
delicate exactly-once claim-flag logic is otherwise untouched.

Build clean; 70 tests green. Discovery loads all four split triggers as v2 in us-central1
(36 functions total); old onGamePartFinished gone. dist rebuilt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 00:08:18 -05:00
null eb4bab0b90 perf(functions): B6b bound the unbounded couple scanners (#10)
assignDailyQuestion and aggregateOutcomeStats previously did an unbounded
db.collection('couples').get() (loading every couple into memory), and aggregate did a
serial outcomes.get() per couple (O(couples) round-trips). Both now paginate the couple
scan (orderBy __name__ + startAfter, 300/200 per page — no custom index needed):

- assignDailyQuestion: each page's create() writes fan out with the burst bounded to a page
  instead of all couples at once; ALREADY_EXISTS stays the idempotent no-op.
- aggregateOutcomeStats: reads each page's outcomes in parallel instead of serially.
  couples.length still counts every couple, so the aggregate windows + totalCouples are
  unchanged (pure aggregate() helper and its tests untouched).

This is the one behavior-touching improvement flagged in the plan; the 512MiB/300s resource
options from B2 remain the safety net. Build clean; 70 tests green. dist rebuilt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 00:05:32 -05:00
null a5af32d3d2 refactor(functions): B4+B5 migrate webhook to v2 + pin onUserDelete on v1
B4 — revenueCatWebhook: functions.https.onRequest → firebase-functions/v2/https onRequest,
with REVENUECAT_SIGNING_KEY bound as a Secret Manager secret via defineSecret (injected into
process.env, so the Ed25519 verify + process-before-ack/500-retry logic is unchanged). Request
type retyped to the v2 Request; console → logger. The key must be seeded in Secret Manager at
deploy (runbook) — it isn't in the repo.

B5 — onUserDelete: kept on the v1 API (2nd gen has no auth.user().onDelete), imported explicitly
from firebase-functions/v1 and wrapped in runWith({ timeoutSeconds: 300, memory: '512MB' }) for
its dual recursiveDelete + Storage sweep. Adopts shared getUserTokens/sendPushToUser + logger;
preserves the original "only notify if the partner has a live token" behavior.
(wrapReleaseKey's HttpsError→v2 swap already landed in B3.)

Build clean; 70 tests green. dist rebuilt. Still on firebase-functions v5.1.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 00:00:29 -05:00
null e0c2d67373 refactor(functions): B3 migrate 12 callables to v2 + harden
Migrate all callables off functions.https.onCall to firebase-functions/v2/https onCall:
createInvite, acceptInvite, leaveCouple, submitOutcome, sendGentleReminder,
sendThinkingOfYou, checkDeviceIntegrity, syncEntitlement, assignDailyQuestionCallable,
wrapReleaseKey. context.auth/app → request.auth/app, data arg → request.data. The 8
client-hardcoded callable names are preserved verbatim (verified via emulator discovery).
The manual `if (!request.app)` App Check check is a 1:1 port (no enforceAppCheck switch).

Hardening folded in:
- acceptInviteCallable: await the previously fire-and-forget partner_joined push (gen 2
  freezes the instance after the response) — still swallows push errors so a failed push
  never fails the accept.
- checkDeviceIntegrity: 10s timeout on the Play Integrity client.request so a hung upstream
  can't pin the instance (fail-closed catch already handles the throw); memory 512MiB.
- wrapReleaseKey: memory 512MiB (tink); HttpsError swapped to v2; lazy tink require + graceful
  failure preserved.
- Error mapping with `if (e instanceof HttpsError) throw e` re-throw guard around the risky
  DB sections in acceptInvite, leaveCouple, submitOutcome, sendGentleReminder,
  sendThinkingOfYou — raw errors map to a clean 'internal' without masking intentional codes
  (resource-exhausted rate limits, permission-denied, etc.). leaveCouple's best-effort
  recursiveDelete sweep now swallows errors (the transactional leave already succeeded).
- Adopt shared sendPushToUser()/logger; remove copied token readers + plaintext token logging.

Delete dead placeholder callables notifications/reminders.ts (sendDailyQuestionReminder,
sendPartnerAnsweredNotification) — no client caller; wrote sent:false rows nothing consumed.

Build clean; 70 tests green; discovery loads all callables as v2 in us-central1. dist rebuilt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:58:32 -05:00
null 6681bf1518 refactor(functions): B2 migrate 8 scheduled functions to v2 + harden
Migrate all scheduled jobs off functions.pubsub.schedule().onRun() to
firebase-functions/v2/scheduler onSchedule({ schedule, timeZone, ...opts }, handler):
sendChallengeDayReminders, unlockDueMemoryCapsules, sendDailyQuestionProactiveReminder,
sendStreakReminder, sendReengagementReminder, assignDailyQuestion (scheduled export),
aggregateOutcomeStats, scheduledOutcomesReminder.

Hardening folded in:
- Fan-out isolation: Promise.all → Promise.allSettled in dailyQuestionReminder (outer+inner),
  reengagement, gameRetention (both jobs), scheduledOutcomesReminder — one bad couple can no
  longer abort a whole run. streakReminder / assignDailyQuestion already isolated.
- Resource options: assignDailyQuestion + aggregateOutcomeStats memory 512MiB + timeout 300s
  (they iterate all couples); the four fan-out reminders get timeout 180s.
- Adopt shared sendPushToUser()/logger everywhere; remove five copied getUserTokens() and the
  copied send/prune blocks (no plaintext token logging remains here).
- Consolidate duplicated date/time helpers into notifications/time.ts (chicagoDateKey, toMillis),
  replacing streakReminder's + scheduledOutcomesReminder's per-file copies.

assignDailyQuestion.ts callable export stays v1 for now (migrates in B3); its tested CST helpers
are untouched. Scanner pagination for assignDailyQuestion/aggregateOutcomes is deferred to B6.

Build clean; 70 tests green (tested pure helpers preserved). dist rebuilt. Still on v5.1.1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-07 23:50:59 -05:00
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 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 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 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 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 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 896bf26b28 feat: date reflection reveal, UI upgrade plan, seed updates, ime-scan script 2026-07-01 04:12:58 -05:00
null afd1eab299 feat(users): wire pruneDeadTokens into onUserDelete 2026-06-30 23:45:36 -05:00
null 2488e18790 feat(notifications): wire pruneDeadTokens into streakReminder 2026-06-30 23:45:31 -05:00
null 40e4f48131 feat(notifications): wire pruneDeadTokens into dailyQuestionReminder 2026-06-30 23:45:26 -05:00
null 355edd0887 feat(couples): wire pruneDeadTokens into acceptInviteCallable 2026-06-30 23:45:21 -05:00
null f55c49010d feat(notifications): wire pruneDeadTokens into sendThinkingOfYouCallable 2026-06-30 23:36:09 -05:00
null 5306d40c72 feat(notifications): wire pruneDeadTokens into sendGentleReminderCallable 2026-06-30 23:35:59 -05:00
null e35b8151c6 feat(notifications): wire pruneDeadTokens into reengagement 2026-06-30 23:35:49 -05:00
null 35eca7d08d feat(notifications): wire pruneDeadTokens into gameRetention 2026-06-30 23:35:42 -05:00
null 7b5790da43 feat(billing): wire pruneDeadTokens into onEntitlementChanged 2026-06-30 23:35:38 -05:00
null c7181e276f feat(questions): wire pruneDeadTokens into onMessageWritten 2026-06-30 23:35:29 -05:00
null 8b3356e60d feat(questions): wire pruneDeadTokens into onAnswerWritten 2026-06-30 23:35:24 -05:00
null 9ca9f19c51 feat(questions): wire pruneDeadTokens into onAnswerRevealed 2026-06-30 23:35:20 -05:00
null 13c0769efb feat(games): wire pruneDeadTokens into onGameSessionUpdate 2026-06-30 23:35:15 -05:00
null ffa038ca56 feat(dates): wire pruneDeadTokens into onDateReflectionWritten 2026-06-30 23:35:07 -05:00
null 2aa08efd39 feat(dates): wire pruneDeadTokens into onDateHistoryCreated 2026-06-30 23:35:00 -05:00
null f8240e01a9 feat(dates): wire pruneDeadTokens into createDateMatch 2026-06-30 23:34:56 -05:00
null 342ca37e91 feat(couples): wire pruneDeadTokens into scheduledOutcomesReminder 2026-06-30 23:34:39 -05:00
null c32a5ad8cc feat(couples): wire pruneDeadTokens into onCoupleLeave 2026-06-30 23:34:33 -05:00
null 6fda1574ea feat(backup): wire pruneDeadTokens into onRestoreRequested 2026-06-30 23:34:26 -05:00
null b564d1da42 feat(notifications): add pruneDeadTokens utility for FCM dead-token cleanup 2026-06-30 23:34:22 -05:00
null 4b4f79361f feat(backup): add onRestoreRequested and onRestoreFulfilled Cloud Functions (partner push + owner self-alert) 2026-06-30 20:43:22 -05:00
null b7ad62054b feat(date-memories): export onDateHistoryCreated and onDateReflectionWritten from index 2026-06-30 18:14:48 -05:00
null 7a5f4e9bbd feat(date-memories): add onDateReflectionWritten Cloud Function 2026-06-30 18:14:46 -05:00
null 2eb21f42f9 feat(date-memories): add onDateHistoryCreated Cloud Function 2026-06-30 18:14:44 -05:00
null 6dddac4615 Revert "feat(date-memories): add onDateHistoryCreated + onDateReflectionWritten Cloud Functions (batch 5/8)"
This reverts commit 8829cffa7e.
2026-06-30 18:13:00 -05:00
null 8829cffa7e feat(date-memories): add onDateHistoryCreated + onDateReflectionWritten Cloud Functions (batch 5/8) 2026-06-30 16:51:59 -05:00
null d87603211a feat(activity): ActivityScreen timeline, HomeScreen activity feed, HomeViewModel wiring, PartnerNotificationManager, sendThinkingOfYouCallable Cloud Function 2026-06-30 03:54:01 -05:00
null e74b6f59af feat(cloud-functions): onEntitlementChanged, acceptInviteCallable, onGameSessionUpdate, onAnswerRevealed, onMessageWritten — FirestoreUserDataSource E2EE, AppMessagingService, EditProfileScreen, iOS plan 2026-06-30 02:38:31 -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 f51a55743c feat(games): partner game-session push orchestration — in-app notification banner, Firestore rules, Cloud Function, QA docs 2026-06-28 22:24:46 -05:00
null fa8005f25f feat(functions): wrapReleaseKeyCallable — server-side Tink wrap for iOS→Android release keys (closes keybox Path A interop gap) 2026-06-28 17:19:06 -05:00