feat(cloud-functions): onEntitlementChanged, acceptInviteCallable, onGameSessionUpdate, onAnswerRevealed, onMessageWritten — FirestoreUserDataSource E2EE, AppMessagingService, EditProfileScreen, iOS plan

This commit is contained in:
null 2026-06-30 02:38:31 -05:00
parent fb810a12aa
commit e74b6f59af
19 changed files with 117 additions and 71 deletions

View File

@ -48,7 +48,8 @@ per-domain services (User/Couple/Invite/Question/Answer/Game/Date/Conversation),
### 2.3 E2EE — full Tink-compatible crypto (HIGHEST RISK; cornerstone)
Implement byte-compatible Swift crypto for every Android wire format:
- `SealedAnswerEncryptor` (AES-256-GCM, 96-bit IV, AAD `"{coupleId}|{questionId}|{userId}"`, `sealed:v1:`),
`FieldEncryptor` (`enc:v1:`, AAD=coupleId) — used by messages/previews/dates,
`FieldEncryptor` (`enc:v1:`, AAD=coupleId) — used by messages/previews/dates **and now profile metadata
(`displayName` + `sex` in `users/{uid}`)**,
`AnswerCommitment` (SHA-256, `sha256:`), `UserKeyManager` (ECIES P-256 HKDF-HMAC-SHA256 AES128-CTR-HMAC, keypair in
Keychain, `pub:v1:`), `ReleaseKeyEncryptor` (`keybox:v1:`), `RecoveryKeyManager` (**Argon2id m=46MiB, t=3, p=1**,
BIP39-style wordlist), `CoupleEncryptionManager`, Keychain-backed key stores.
@ -60,6 +61,12 @@ Implement byte-compatible Swift crypto for every Android wire format:
`RecoveryKeyManager` Argon2id (fixed salt/params) must produce **identical bytes** on both platforms — assert in unit
tests on each side. AEAD/ECIES use random IVs so they can't be golden-matched; cover those with the **round-trip**
harness above. Generate the Android fixtures now (on Linux) so iOS has them ready.
- **⛔ Profile-metadata decrypt-on-read (REQUIRED before iOS launch — R22).** Android now encrypts `displayName` +
`sex` under the couple key. iOS reads these raw today ([HomeViews](iphone/Closer/Home/HomeViews.swift),
[SettingsViews](iphone/Closer/Settings/SettingsViews.swift)) and will show `enc:v1:…` until it decrypts them on
read (tolerant of legacy plaintext; show a 🔒 placeholder when the key is missing) — mirror Android's
`FirestoreUserDataSource` chokepoint + the pairing/legacy **migration** and **unpair-revert**. Until this ships,
iOS shows the locked placeholder for name/gender (acceptable in dev; **not** for release).
### 2.4 Screens & features to parity (~48 + new messaging)
All routes from the refreshed audit's screen map, **including the NEW Messages experience** (inbox + conversation

View File

@ -1,5 +1,6 @@
package app.closer.core.notifications
import app.closer.crypto.FieldEncryptor
import app.closer.domain.repository.AuthRepository
import app.closer.domain.repository.UserRepository
import app.closer.notifications.NotificationChannelSetup
@ -90,14 +91,20 @@ class AppMessagingService : FirebaseMessagingService() {
(gameKind == app.closer.notifications.GamePromptKind.STARTED ||
gameKind == app.closer.notifications.GamePromptKind.YOUR_TURN)
if (!suppress) {
gamePromptController.show(
coupleId = coupleId,
gameType = message.data["game_type"] ?: "",
kind = gameKind,
gameSessionId = sessionId,
partnerName = message.data["sender_name"],
avatarUrl = message.data["sender_avatar_url"]
)
// Resolve the partner's name from the locally-decrypted user, NOT the push payload —
// displayName is E2EE in users/{uid}, so the server can't send it in cleartext. The
// banner falls back to a generic label when this is null.
serviceScope.launch {
val partnerName = runCatching { resolvePartnerName() }.getOrNull()
gamePromptController.show(
coupleId = coupleId,
gameType = message.data["game_type"] ?: "",
kind = gameKind,
gameSessionId = sessionId,
partnerName = partnerName,
avatarUrl = message.data["sender_avatar_url"]
)
}
}
return
}
@ -120,4 +127,12 @@ class AppMessagingService : FirebaseMessagingService() {
}
}
}
/** The partner's display name from the locally-decrypted user (null if unavailable/locked here). */
private suspend fun resolvePartnerName(): String? {
val uid = authRepository.currentUserId ?: return null
val partnerId = userRepository.getUser(uid)?.partnerId ?: return null
return userRepository.getUser(partnerId)?.displayName
?.takeIf { it.isNotBlank() && it != FieldEncryptor.LOCKED_PLACEHOLDER }
}
}

View File

@ -55,7 +55,7 @@ class FirestoreUserDataSource @Inject constructor(
return User(
id = id,
email = data.getString("email") ?: "",
displayName = data.getString("displayName") ?: "",
displayName = decryptProfileField(data.getString("displayName") ?: "", coupleId),
photoUrl = data.getString("photoUrl") ?: "",
sex = decryptProfileField(data.getString("sex") ?: "", coupleId),
partnerId = data.getString("partnerId"),
@ -92,7 +92,7 @@ class FirestoreUserDataSource @Inject constructor(
userRef(user.id).set(
mapOf(
"email" to user.email,
"displayName" to user.displayName,
"displayName" to encryptProfileField(user.displayName, user.coupleId),
"photoUrl" to user.photoUrl,
"sex" to encryptProfileField(user.sex, user.coupleId),
"partnerId" to user.partnerId,
@ -106,15 +106,19 @@ class FirestoreUserDataSource @Inject constructor(
.addOnFailureListener { cont.resumeWithException(it) }
}
suspend fun updateDisplayName(uid: String, displayName: String): Unit =
suspendCancellableCoroutine { cont ->
userRef(uid).set(
mapOf("displayName" to displayName, "lastActiveAt" to System.currentTimeMillis()),
SetOptions.merge()
)
.addOnSuccessListener { cont.resume(Unit) }
.addOnFailureListener { cont.resumeWithException(it) }
suspend fun updateDisplayName(uid: String, displayName: String) {
require(displayName != FieldEncryptor.LOCKED_PLACEHOLDER) {
"Refusing to persist the locked placeholder as displayName"
}
val coupleId = getSnapshot(uid)?.getString("coupleId")
setMerge(
uid,
mapOf(
"displayName" to encryptProfileField(displayName, coupleId),
"lastActiveAt" to System.currentTimeMillis()
)
)
}
suspend fun updatePhotoUrl(uid: String, photoUrl: String): Unit =
suspendCancellableCoroutine { cont ->
@ -156,9 +160,11 @@ class FirestoreUserDataSource @Inject constructor(
val coupleId = snap.getString("coupleId") ?: return
if (encryptionManager.aeadFor(coupleId) == null) return
val updates = mutableMapOf<String, Any?>()
val rawSex = snap.getString("sex") ?: ""
if (rawSex.isNotBlank() && !fieldEncryptor.isEncrypted(rawSex)) {
updates["sex"] = encryptProfileField(rawSex, coupleId)
for (field in ENCRYPTED_PROFILE_FIELDS) {
val raw = snap.getString(field) ?: ""
if (raw.isNotBlank() && !fieldEncryptor.isEncrypted(raw)) {
updates[field] = encryptProfileField(raw, coupleId)
}
}
if (updates.isNotEmpty()) setMerge(uid, updates)
}
@ -172,13 +178,20 @@ class FirestoreUserDataSource @Inject constructor(
val aead = encryptionManager.aeadFor(coupleId) ?: return
val snap = getSnapshot(uid) ?: return
val updates = mutableMapOf<String, Any?>()
val rawSex = snap.getString("sex") ?: ""
if (fieldEncryptor.isEncrypted(rawSex)) {
fieldEncryptor.decrypt(rawSex, aead, coupleId)?.let { updates["sex"] = it }
for (field in ENCRYPTED_PROFILE_FIELDS) {
val raw = snap.getString(field) ?: ""
if (fieldEncryptor.isEncrypted(raw)) {
fieldEncryptor.decrypt(raw, aead, coupleId)?.let { updates[field] = it }
}
}
if (updates.isNotEmpty()) setMerge(uid, updates)
}
private companion object {
/** Profile fields encrypted under the couple key (AAD=coupleId). Keep migrate/revert in sync. */
val ENCRYPTED_PROFILE_FIELDS = listOf("displayName", "sex")
}
suspend fun hasProfile(uid: String): Boolean =
suspendCancellableCoroutine { cont ->
userRef(uid).get()

View File

@ -106,7 +106,8 @@ fun EditProfileScreen(
EditProfileContent(
modifier = Modifier
.fillMaxSize()
.padding(padding),
.padding(padding)
.verticalScroll(rememberScrollState()),
snackbar = snackbar,
viewModel = viewModel
)
@ -161,9 +162,11 @@ fun EditProfileContent(
if (granted) cameraLauncher.launch(cameraUri)
}
// NOTE: this content does NOT scroll itself — the host screen owns the scroll container. AccountScreen
// already wraps it in a verticalScroll Column (with extra cards below), and nesting two vertical
// scrollers crashes with "infinity maximum height". (C-ACCOUNT-001)
Column(
modifier = modifier
.verticalScroll(rememberScrollState())
.imePadding()
.padding(horizontal = 24.dp, vertical = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally

View File

@ -73,7 +73,7 @@ async function collectTokens(db, userId) {
exports.onEntitlementChanged = functions.firestore
.document('users/{userId}/entitlements/premium')
.onWrite(async (change, context) => {
var _a, _b, _c, _d;
var _a, _b;
const { userId } = context.params;
const before = isActive(change.before.data());
const after = isActive(change.after.data());
@ -105,11 +105,12 @@ exports.onEntitlementChanged = functions.firestore
console.log(`[onEntitlementChanged] partner ${partnerId} already premium; skip`);
return;
}
const subscriberName = (_d = (_c = (await db.doc(`users/${userId}`).get()).data()) === null || _c === void 0 ? void 0 : _c.displayName) !== null && _d !== void 0 ? _d : 'Your partner';
// displayName is E2EE in users/{uid}, so use a generic label (this copy is stored server-side in
// notification_queue + the push, neither of which can decrypt).
const payload = {
type: 'subscription_entitlement_changed',
title: 'Premium unlocked ✨',
body: `${subscriberName} upgraded — you both have Premium now.`,
body: 'Your partner upgraded — you both have Premium now.',
};
// In-app record for the partner.
await db

View File

@ -1 +1 @@
{"version":3,"file":"onEntitlementChanged.js","sourceRoot":"","sources":["../../src/billing/onEntitlementChanged.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"onEntitlementChanged.js","sourceRoot":"","sources":["../../src/billing/onEntitlementChanged.ts"],"names":[],"mappings":"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"}

View File

@ -162,6 +162,9 @@ exports.acceptInviteCallable = functions.https.onCall(async (data, context) => {
acceptedAt: admin.firestore.FieldValue.serverTimestamp(),
coupleId,
encryptedRecoveryPhrase: admin.firestore.FieldValue.delete(),
// Don't let the plaintext inviter name linger once the couple exists (the profile name is E2EE
// from here on). It was only needed for the pre-accept "X invited you" preview.
inviterDisplayName: admin.firestore.FieldValue.delete(),
});
await batch.commit();
console.log(`[acceptInviteCallable] ${callerId} accepted an invite; created couple ${coupleId}`);

File diff suppressed because one or more lines are too long

View File

@ -46,7 +46,7 @@ const quietHours_1 = require("../notifications/quietHours");
exports.onGameSessionUpdate = functions.firestore
.document('couples/{coupleId}/sessions/{sessionId}')
.onWrite(async (change, context) => {
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
var _a, _b, _c, _d, _e, _f, _g, _h;
const { coupleId, sessionId } = context.params;
// The per-couple active-session lock lives at sessions/_active — it is a pointer, not a
// game session, so it must never produce a partner notification.
@ -75,16 +75,17 @@ exports.onGameSessionUpdate = functions.firestore
}
const partnerA = userIds[0];
const partnerB = userIds[1];
// Get user display names for notifications
const userA = await db.collection('users').doc(partnerA).get();
const userB = await db.collection('users').doc(partnerB).get();
const partnerAName = (_d = (_c = userA.data()) === null || _c === void 0 ? void 0 : _c.displayName) !== null && _d !== void 0 ? _d : 'Partner A';
const partnerBName = (_f = (_e = userB.data()) === null || _e === void 0 ? void 0 : _e.displayName) !== null && _f !== void 0 ? _f : 'Partner B';
const avatarA = (_g = userA.data()) === null || _g === void 0 ? void 0 : _g.photoUrl;
const avatarB = (_h = userB.data()) === null || _h === void 0 ? void 0 : _h.photoUrl;
// displayName is E2EE in users/{uid}, so the OS-rendered push uses a generic label; the app shows
// the real name in-app (resolved locally). Avatar (photoUrl) stays plaintext and is still sent.
const partnerAName = 'Your partner';
const partnerBName = 'Your partner';
const avatarA = (_c = userA.data()) === null || _c === void 0 ? void 0 : _c.photoUrl;
const avatarB = (_d = userB.data()) === null || _d === void 0 ? void 0 : _d.photoUrl;
// M-001: per-recipient quiet-hours lookup ("no notifications" promise). Fail-open.
const dataFor = (uid) => (uid === partnerA ? userA.data() : userB.data());
const currentData = (_j = change.after.data()) !== null && _j !== void 0 ? _j : {};
const currentData = (_e = change.after.data()) !== null && _e !== void 0 ? _e : {};
if (!change.after.exists)
return; // deletion — nothing to notify
const status = currentData.status;
@ -108,7 +109,7 @@ exports.onGameSessionUpdate = functions.firestore
});
if (claimed) {
const startedBy = currentData.startedByUserId;
const gameType = (_k = currentData.gameType) !== null && _k !== void 0 ? _k : 'wheel';
const gameType = (_f = currentData.gameType) !== null && _f !== void 0 ? _f : 'wheel';
const recipientId = startedBy === partnerA ? partnerB : partnerA;
const starterName = startedBy === partnerA ? partnerAName : partnerBName;
const starterAvatar = startedBy === partnerA ? avatarA : avatarB;
@ -141,7 +142,7 @@ exports.onGameSessionUpdate = functions.firestore
return true;
});
if (claimed) {
const gameType = (_l = currentData.gameType) !== null && _l !== void 0 ? _l : 'wheel';
const gameType = (_g = currentData.gameType) !== null && _g !== void 0 ? _g : 'wheel';
const joinerName = joiner === partnerA ? partnerAName : partnerBName;
const joinerAvatar = joiner === partnerA ? avatarA : avatarB;
if ((0, quietHours_1.recipientInQuietHours)(dataFor(startedBy))) {
@ -165,7 +166,7 @@ exports.onGameSessionUpdate = functions.firestore
return true;
});
if (claimed) {
const gt = (_m = currentData.gameType) !== null && _m !== void 0 ? _m : 'wheel';
const gt = (_h = currentData.gameType) !== null && _h !== void 0 ? _h : 'wheel';
// Notify BOTH partners, each naming the OTHER. M-001: skip a recipient in quiet hours.
if ((0, quietHours_1.recipientInQuietHours)(dataFor(partnerA))) {
console.log(`[onGameSessionUpdate] ${partnerA} in quiet hours — suppressing finish push`);
@ -199,7 +200,7 @@ const ASYNC_GAME_COLLECTIONS = ['this_or_that', 'wheel', 'how_well', 'desire_syn
exports.onGamePartFinished = functions.firestore
.document('couples/{coupleId}/{gameType}/{sessionId}')
.onWrite(async (change, context) => {
var _a, _b, _c, _d, _e, _f, _g;
var _a, _b, _c, _d, _e;
const { coupleId, gameType, sessionId } = context.params;
if (!ASYNC_GAME_COLLECTIONS.includes(gameType))
return; // ignore messages/reactions/etc.
@ -236,8 +237,8 @@ exports.onGamePartFinished = functions.firestore
if (!recipient)
return;
const finisher = await db.collection('users').doc(finisherUid).get();
const finisherName = (_f = (_e = finisher.data()) === null || _e === void 0 ? void 0 : _e.displayName) !== null && _f !== void 0 ? _f : 'Your partner';
const finisherAvatar = (_g = finisher.data()) === null || _g === void 0 ? void 0 : _g.photoUrl;
const finisherName = 'Your partner'; // displayName is E2EE; the app shows the real name in-app
const finisherAvatar = (_e = finisher.data()) === null || _e === void 0 ? void 0 : _e.photoUrl;
await notifyPartner(db, messaging, recipient, finisherName, gameType, 'partner_completed_part', `${finisherName} finished their part — your turn to play!`, coupleId, finisherAvatar, sessionId);
});
/**

File diff suppressed because one or more lines are too long

View File

@ -47,7 +47,7 @@ const quietHours_1 = require("../notifications/quietHours");
exports.onAnswerRevealed = functions.firestore
.document('couples/{coupleId}/daily_question/{date}/answers/{userId}')
.onUpdate(async (change, context) => {
var _a, _b, _c, _d, _e, _f;
var _a, _b, _c, _d, _e;
const { coupleId, date, userId } = context.params;
const before = change.before.data();
const after = change.after.data();
@ -100,12 +100,13 @@ exports.onAnswerRevealed = functions.firestore
return;
}
const questionId = typeof after.questionId === 'string' ? after.questionId : '';
// displayName is E2EE in users/{uid} → generic title; the app shows the real name in-app. Avatar
// (photoUrl) stays plaintext, so it's still sent.
const revealerDoc = await db.collection('users').doc(userId).get();
const revealerName = ((_e = revealerDoc.data()) === null || _e === void 0 ? void 0 : _e.displayName) || 'Your partner';
const revealerAvatar = (_f = revealerDoc.data()) === null || _f === void 0 ? void 0 : _f.photoUrl;
const revealerAvatar = (_e = revealerDoc.data()) === null || _e === void 0 ? void 0 : _e.photoUrl;
const payload = {
notification: {
title: `${revealerName} opened your answers`,
title: 'Your partner opened your answers',
body: 'Open to see what you each said.',
},
data: Object.assign({ type: 'partner_opened_answer', couple_id: coupleId, question_id: questionId, date }, (typeof revealerAvatar === 'string' && revealerAvatar.length > 0

View File

@ -1 +1 @@
{"version":3,"file":"onAnswerRevealed.js","sourceRoot":"","sources":["../../src/questions/onAnswerRevealed.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"onAnswerRevealed.js","sourceRoot":"","sources":["../../src/questions/onAnswerRevealed.ts"],"names":[],"mappings":"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"}

View File

@ -48,7 +48,7 @@ const quietHours_1 = require("../notifications/quietHours");
exports.onMessageWritten = functions.firestore
.document('couples/{coupleId}/conversations/{conversationId}/messages/{messageId}')
.onCreate(async (snap, context) => {
var _a, _b, _c, _d, _e, _f, _g, _h;
var _a, _b, _c, _d, _e, _f;
const { coupleId, conversationId, messageId } = context.params;
const db = admin.firestore();
const messageData = snap.data();
@ -103,14 +103,13 @@ exports.onMessageWritten = functions.firestore
console.log(`[onMessageWritten] no FCM tokens for partner ${partnerId}`);
return;
}
// The recipient sees the message from the author (their partner), so surface the author's
// photo/name — the in-app chat bubble uses sender_avatar_url to show the partner's face.
// displayName is E2EE in users/{uid}, so the server can't read it for the OS-rendered push (the
// app shows the real name in-app). photoUrl stays plaintext, so the avatar is still sent.
const authorDoc = await db.collection('users').doc(authorId).get();
const authorPhotoUrl = (_f = (_e = authorDoc.data()) === null || _e === void 0 ? void 0 : _e.photoUrl) !== null && _f !== void 0 ? _f : '';
const authorName = (_h = (_g = authorDoc.data()) === null || _g === void 0 ? void 0 : _g.displayName) !== null && _h !== void 0 ? _h : '';
const payload = {
notification: {
title: authorName ? `${authorName} sent a message` : 'Your partner sent a message',
title: 'Your partner sent a message',
body: 'Tap to read and reply.',
},
data: Object.assign({ type: 'chat_message', couple_id: coupleId, conversation_id: conversationId }, (authorPhotoUrl ? { sender_avatar_url: authorPhotoUrl } : {})),

View File

@ -1 +1 @@
{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AACvC,4DAAmE;AAEnE;;;;;;;GAOG;AACU,QAAA,gBAAgB,GAAG,SAAS,CAAC,SAAS;KAChD,QAAQ,CAAC,wEAAwE,CAAC;KAClF,QAAQ,CAAC,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;;IAChC,MAAM,EAAE,QAAQ,EAAE,cAAc,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,MAIvD,CAAA;IAED,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAC5B,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAsC,CAAA;IACnE,MAAM,QAAQ,GAAG,OAAO,WAAW,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAA;IAC/F,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,OAAO,CAAC,IAAI,CAAC,iDAAiD,SAAS,EAAE,CAAC,CAAA;QAC1E,OAAM;IACR,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IACpE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QACtB,OAAO,CAAC,IAAI,CAAC,6BAA6B,QAAQ,YAAY,CAAC,CAAA;QAC/D,OAAM;IACR,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;IAC7D,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAA;IACzD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,kDAAkD,QAAQ,EAAE,CAAC,CAAA;QAC1E,OAAM;IACR,CAAC;IAED,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IAExE,+EAA+E;IAC/E,MAAM,YAAY,GAAG,MAAA,cAAc,CAAC,IAAI,EAAE,0CAAE,gBAAgB,CAAA;IAC5D,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;QAC3B,OAAO,CAAC,GAAG,CAAC,8BAA8B,SAAS,6BAA6B,CAAC,CAAA;QACjF,OAAM;IACR,CAAC;IAED,2FAA2F;IAC3F,IAAI,IAAA,kCAAqB,EAAC,cAAc,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,8BAA8B,SAAS,kCAAkC,CAAC,CAAA;QACtF,OAAM;IACR,CAAC;IAED,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,IAAI,cAAc,CAAC,MAAM,EAAE,CAAC;QAC1B,MAAM,WAAW,GAAG,MAAA,cAAc,CAAC,IAAI,EAAE,0CAAE,QAAQ,CAAA;QACnD,IAAI,OAAO,WAAW,KAAK,QAAQ,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9D,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;QAC1B,CAAC;IACH,CAAC;IAED,MAAM,aAAa,GAAG,MAAM,EAAE;SAC3B,UAAU,CAAC,OAAO,CAAC;SACnB,GAAG,CAAC,SAAS,CAAC;SACd,UAAU,CAAC,WAAW,CAAC;SACvB,GAAG,EAAE,CAAA;IACR,aAAa,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;;QACjC,MAAM,CAAC,GAAG,MAAA,GAAG,CAAC,IAAI,EAAE,0CAAE,KAAK,CAAA;QAC3B,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;YACjE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChB,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,gDAAgD,SAAS,EAAE,CAAC,CAAA;QACxE,OAAM;IACR,CAAC;IAED,gGAAgG;IAChG,0FAA0F;IAC1F,MAAM,SAAS,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IAClE,MAAM,cAAc,GAAG,MAAC,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,QAA+B,mCAAI,EAAE,CAAA;IAE/E,MAAM,OAAO,GAAqC;QAChD,YAAY,EAAE;YACZ,KAAK,EAAE,6BAA6B;YACpC,IAAI,EAAE,wBAAwB;SAC/B;QACD,IAAI,kBACF,IAAI,EAAE,cAAc,EACpB,SAAS,EAAE,QAAQ,EACnB,eAAe,EAAE,cAAc,IAC5B,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,iBAAiB,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CACjE;KACF,CAAA;IAED,MAAM,WAAW,GAAG,MAAM,OAAO,CAAC,UAAU,CAC1C,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CACnB,KAAK,CAAC,SAAS,EAAE,CAAC,IAAI,CAAC,gCAClB,OAAO,KACV,KAAK;QACL,0FAA0F;QAC1F,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE,GAClC,CAAC,CAC9B,CACF,CAAA;IAED,MAAM,QAAQ,GAAa,EAAE,CAAA;IAC7B,WAAW,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;QACpC,IAAI,MAAM,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACjC,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,KAAK,CAAC,KAAK,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;QAC7D,CAAC;IACH,CAAC,CAAC,CAAA;IAEF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,KAAK,CAAC,+CAA+C,EAAE,QAAQ,CAAC,CAAA;IAC1E,CAAC;IAED,OAAO,CAAC,GAAG,CACT,uCAAuC,SAAS,qBAAqB,cAAc,cAAc,QAAQ,EAAE,CAC5G,CAAA;AACH,CAAC,CAAC,CAAA"}

View File

@ -71,13 +71,12 @@ export const onEntitlementChanged = functions.firestore
return
}
const subscriberName =
(await db.doc(`users/${userId}`).get()).data()?.displayName ?? 'Your partner'
// displayName is E2EE in users/{uid}, so use a generic label (this copy is stored server-side in
// notification_queue + the push, neither of which can decrypt).
const payload = {
type: 'subscription_entitlement_changed',
title: 'Premium unlocked ✨',
body: `${subscriberName} upgraded — you both have Premium now.`,
body: 'Your partner upgraded — you both have Premium now.',
}
// In-app record for the partner.

View File

@ -151,6 +151,9 @@ export const acceptInviteCallable = functions.https.onCall(async (data: any, con
acceptedAt: admin.firestore.FieldValue.serverTimestamp(),
coupleId,
encryptedRecoveryPhrase: admin.firestore.FieldValue.delete(),
// Don't let the plaintext inviter name linger once the couple exists (the profile name is E2EE
// from here on). It was only needed for the pre-accept "X invited you" preview.
inviterDisplayName: admin.firestore.FieldValue.delete(),
})
await batch.commit()

View File

@ -45,11 +45,12 @@ export const onGameSessionUpdate = functions.firestore
const partnerA = userIds[0]
const partnerB = userIds[1]
// Get user display names for notifications
const userA = await db.collection('users').doc(partnerA).get()
const userB = await db.collection('users').doc(partnerB).get()
const partnerAName = userA.data()?.displayName ?? 'Partner A'
const partnerBName = userB.data()?.displayName ?? 'Partner B'
// displayName is E2EE in users/{uid}, so the OS-rendered push uses a generic label; the app shows
// the real name in-app (resolved locally). Avatar (photoUrl) stays plaintext and is still sent.
const partnerAName = 'Your partner'
const partnerBName = 'Your partner'
const avatarA = userA.data()?.photoUrl as string | undefined
const avatarB = userB.data()?.photoUrl as string | undefined
// M-001: per-recipient quiet-hours lookup ("no notifications" promise). Fail-open.
@ -216,7 +217,7 @@ export const onGamePartFinished = functions.firestore
const recipient = userIds.find((u) => u !== finisherUid)
if (!recipient) return
const finisher = await db.collection('users').doc(finisherUid).get()
const finisherName = (finisher.data()?.displayName as string) ?? 'Your partner'
const finisherName = 'Your partner' // displayName is E2EE; the app shows the real name in-app
const finisherAvatar = finisher.data()?.photoUrl as string | undefined
await notifyPartner(

View File

@ -72,13 +72,14 @@ export const onAnswerRevealed = functions.firestore
}
const questionId = typeof after.questionId === 'string' ? after.questionId : ''
// displayName is E2EE in users/{uid} → generic title; the app shows the real name in-app. Avatar
// (photoUrl) stays plaintext, so it's still sent.
const revealerDoc = await db.collection('users').doc(userId).get()
const revealerName = (revealerDoc.data()?.displayName as string | undefined) || 'Your partner'
const revealerAvatar = revealerDoc.data()?.photoUrl
const payload: admin.messaging.MessagingPayload = {
notification: {
title: `${revealerName} opened your answers`,
title: 'Your partner opened your answers',
body: 'Open to see what you each said.',
},
data: {

View File

@ -80,15 +80,14 @@ export const onMessageWritten = functions.firestore
return
}
// The recipient sees the message from the author (their partner), so surface the author's
// photo/name — the in-app chat bubble uses sender_avatar_url to show the partner's face.
// displayName is E2EE in users/{uid}, so the server can't read it for the OS-rendered push (the
// app shows the real name in-app). photoUrl stays plaintext, so the avatar is still sent.
const authorDoc = await db.collection('users').doc(authorId).get()
const authorPhotoUrl = (authorDoc.data()?.photoUrl as string | undefined) ?? ''
const authorName = (authorDoc.data()?.displayName as string | undefined) ?? ''
const payload: admin.messaging.MessagingPayload = {
notification: {
title: authorName ? `${authorName} sent a message` : 'Your partner sent a message',
title: 'Your partner sent a message',
body: 'Tap to read and reply.',
},
data: {