feat(cloud-functions): onEntitlementChanged, acceptInviteCallable, onGameSessionUpdate, onAnswerRevealed, onMessageWritten — FirestoreUserDataSource E2EE, AppMessagingService, EditProfileScreen, iOS plan
This commit is contained in:
parent
fb810a12aa
commit
e74b6f59af
|
|
@ -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)
|
### 2.3 E2EE — full Tink-compatible crypto (HIGHEST RISK; cornerstone)
|
||||||
Implement byte-compatible Swift crypto for every Android wire format:
|
Implement byte-compatible Swift crypto for every Android wire format:
|
||||||
- `SealedAnswerEncryptor` (AES-256-GCM, 96-bit IV, AAD `"{coupleId}|{questionId}|{userId}"`, `sealed:v1:`),
|
- `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
|
`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**,
|
Keychain, `pub:v1:`), `ReleaseKeyEncryptor` (`keybox:v1:`), `RecoveryKeyManager` (**Argon2id m=46MiB, t=3, p=1**,
|
||||||
BIP39-style wordlist), `CoupleEncryptionManager`, Keychain-backed key stores.
|
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
|
`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**
|
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.
|
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)
|
### 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
|
All routes from the refreshed audit's screen map, **including the NEW Messages experience** (inbox + conversation
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package app.closer.core.notifications
|
package app.closer.core.notifications
|
||||||
|
|
||||||
|
import app.closer.crypto.FieldEncryptor
|
||||||
import app.closer.domain.repository.AuthRepository
|
import app.closer.domain.repository.AuthRepository
|
||||||
import app.closer.domain.repository.UserRepository
|
import app.closer.domain.repository.UserRepository
|
||||||
import app.closer.notifications.NotificationChannelSetup
|
import app.closer.notifications.NotificationChannelSetup
|
||||||
|
|
@ -90,14 +91,20 @@ class AppMessagingService : FirebaseMessagingService() {
|
||||||
(gameKind == app.closer.notifications.GamePromptKind.STARTED ||
|
(gameKind == app.closer.notifications.GamePromptKind.STARTED ||
|
||||||
gameKind == app.closer.notifications.GamePromptKind.YOUR_TURN)
|
gameKind == app.closer.notifications.GamePromptKind.YOUR_TURN)
|
||||||
if (!suppress) {
|
if (!suppress) {
|
||||||
gamePromptController.show(
|
// Resolve the partner's name from the locally-decrypted user, NOT the push payload —
|
||||||
coupleId = coupleId,
|
// displayName is E2EE in users/{uid}, so the server can't send it in cleartext. The
|
||||||
gameType = message.data["game_type"] ?: "",
|
// banner falls back to a generic label when this is null.
|
||||||
kind = gameKind,
|
serviceScope.launch {
|
||||||
gameSessionId = sessionId,
|
val partnerName = runCatching { resolvePartnerName() }.getOrNull()
|
||||||
partnerName = message.data["sender_name"],
|
gamePromptController.show(
|
||||||
avatarUrl = message.data["sender_avatar_url"]
|
coupleId = coupleId,
|
||||||
)
|
gameType = message.data["game_type"] ?: "",
|
||||||
|
kind = gameKind,
|
||||||
|
gameSessionId = sessionId,
|
||||||
|
partnerName = partnerName,
|
||||||
|
avatarUrl = message.data["sender_avatar_url"]
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
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 }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ class FirestoreUserDataSource @Inject constructor(
|
||||||
return User(
|
return User(
|
||||||
id = id,
|
id = id,
|
||||||
email = data.getString("email") ?: "",
|
email = data.getString("email") ?: "",
|
||||||
displayName = data.getString("displayName") ?: "",
|
displayName = decryptProfileField(data.getString("displayName") ?: "", coupleId),
|
||||||
photoUrl = data.getString("photoUrl") ?: "",
|
photoUrl = data.getString("photoUrl") ?: "",
|
||||||
sex = decryptProfileField(data.getString("sex") ?: "", coupleId),
|
sex = decryptProfileField(data.getString("sex") ?: "", coupleId),
|
||||||
partnerId = data.getString("partnerId"),
|
partnerId = data.getString("partnerId"),
|
||||||
|
|
@ -92,7 +92,7 @@ class FirestoreUserDataSource @Inject constructor(
|
||||||
userRef(user.id).set(
|
userRef(user.id).set(
|
||||||
mapOf(
|
mapOf(
|
||||||
"email" to user.email,
|
"email" to user.email,
|
||||||
"displayName" to user.displayName,
|
"displayName" to encryptProfileField(user.displayName, user.coupleId),
|
||||||
"photoUrl" to user.photoUrl,
|
"photoUrl" to user.photoUrl,
|
||||||
"sex" to encryptProfileField(user.sex, user.coupleId),
|
"sex" to encryptProfileField(user.sex, user.coupleId),
|
||||||
"partnerId" to user.partnerId,
|
"partnerId" to user.partnerId,
|
||||||
|
|
@ -106,15 +106,19 @@ class FirestoreUserDataSource @Inject constructor(
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
.addOnFailureListener { cont.resumeWithException(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateDisplayName(uid: String, displayName: String): Unit =
|
suspend fun updateDisplayName(uid: String, displayName: String) {
|
||||||
suspendCancellableCoroutine { cont ->
|
require(displayName != FieldEncryptor.LOCKED_PLACEHOLDER) {
|
||||||
userRef(uid).set(
|
"Refusing to persist the locked placeholder as displayName"
|
||||||
mapOf("displayName" to displayName, "lastActiveAt" to System.currentTimeMillis()),
|
|
||||||
SetOptions.merge()
|
|
||||||
)
|
|
||||||
.addOnSuccessListener { cont.resume(Unit) }
|
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
|
||||||
}
|
}
|
||||||
|
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 =
|
suspend fun updatePhotoUrl(uid: String, photoUrl: String): Unit =
|
||||||
suspendCancellableCoroutine { cont ->
|
suspendCancellableCoroutine { cont ->
|
||||||
|
|
@ -156,9 +160,11 @@ class FirestoreUserDataSource @Inject constructor(
|
||||||
val coupleId = snap.getString("coupleId") ?: return
|
val coupleId = snap.getString("coupleId") ?: return
|
||||||
if (encryptionManager.aeadFor(coupleId) == null) return
|
if (encryptionManager.aeadFor(coupleId) == null) return
|
||||||
val updates = mutableMapOf<String, Any?>()
|
val updates = mutableMapOf<String, Any?>()
|
||||||
val rawSex = snap.getString("sex") ?: ""
|
for (field in ENCRYPTED_PROFILE_FIELDS) {
|
||||||
if (rawSex.isNotBlank() && !fieldEncryptor.isEncrypted(rawSex)) {
|
val raw = snap.getString(field) ?: ""
|
||||||
updates["sex"] = encryptProfileField(rawSex, coupleId)
|
if (raw.isNotBlank() && !fieldEncryptor.isEncrypted(raw)) {
|
||||||
|
updates[field] = encryptProfileField(raw, coupleId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (updates.isNotEmpty()) setMerge(uid, updates)
|
if (updates.isNotEmpty()) setMerge(uid, updates)
|
||||||
}
|
}
|
||||||
|
|
@ -172,13 +178,20 @@ class FirestoreUserDataSource @Inject constructor(
|
||||||
val aead = encryptionManager.aeadFor(coupleId) ?: return
|
val aead = encryptionManager.aeadFor(coupleId) ?: return
|
||||||
val snap = getSnapshot(uid) ?: return
|
val snap = getSnapshot(uid) ?: return
|
||||||
val updates = mutableMapOf<String, Any?>()
|
val updates = mutableMapOf<String, Any?>()
|
||||||
val rawSex = snap.getString("sex") ?: ""
|
for (field in ENCRYPTED_PROFILE_FIELDS) {
|
||||||
if (fieldEncryptor.isEncrypted(rawSex)) {
|
val raw = snap.getString(field) ?: ""
|
||||||
fieldEncryptor.decrypt(rawSex, aead, coupleId)?.let { updates["sex"] = it }
|
if (fieldEncryptor.isEncrypted(raw)) {
|
||||||
|
fieldEncryptor.decrypt(raw, aead, coupleId)?.let { updates[field] = it }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (updates.isNotEmpty()) setMerge(uid, updates)
|
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 =
|
suspend fun hasProfile(uid: String): Boolean =
|
||||||
suspendCancellableCoroutine { cont ->
|
suspendCancellableCoroutine { cont ->
|
||||||
userRef(uid).get()
|
userRef(uid).get()
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,8 @@ fun EditProfileScreen(
|
||||||
EditProfileContent(
|
EditProfileContent(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(padding),
|
.padding(padding)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
snackbar = snackbar,
|
snackbar = snackbar,
|
||||||
viewModel = viewModel
|
viewModel = viewModel
|
||||||
)
|
)
|
||||||
|
|
@ -161,9 +162,11 @@ fun EditProfileContent(
|
||||||
if (granted) cameraLauncher.launch(cameraUri)
|
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(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.imePadding()
|
.imePadding()
|
||||||
.padding(horizontal = 24.dp, vertical = 16.dp),
|
.padding(horizontal = 24.dp, vertical = 16.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
|
|
||||||
|
|
@ -73,7 +73,7 @@ async function collectTokens(db, userId) {
|
||||||
exports.onEntitlementChanged = functions.firestore
|
exports.onEntitlementChanged = functions.firestore
|
||||||
.document('users/{userId}/entitlements/premium')
|
.document('users/{userId}/entitlements/premium')
|
||||||
.onWrite(async (change, context) => {
|
.onWrite(async (change, context) => {
|
||||||
var _a, _b, _c, _d;
|
var _a, _b;
|
||||||
const { userId } = context.params;
|
const { userId } = context.params;
|
||||||
const before = isActive(change.before.data());
|
const before = isActive(change.before.data());
|
||||||
const after = isActive(change.after.data());
|
const after = isActive(change.after.data());
|
||||||
|
|
@ -105,11 +105,12 @@ exports.onEntitlementChanged = functions.firestore
|
||||||
console.log(`[onEntitlementChanged] partner ${partnerId} already premium; skip`);
|
console.log(`[onEntitlementChanged] partner ${partnerId} already premium; skip`);
|
||||||
return;
|
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 = {
|
const payload = {
|
||||||
type: 'subscription_entitlement_changed',
|
type: 'subscription_entitlement_changed',
|
||||||
title: 'Premium unlocked ✨',
|
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.
|
// In-app record for the partner.
|
||||||
await db
|
await db
|
||||||
|
|
|
||||||
|
|
@ -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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"}
|
||||||
|
|
@ -162,6 +162,9 @@ exports.acceptInviteCallable = functions.https.onCall(async (data, context) => {
|
||||||
acceptedAt: admin.firestore.FieldValue.serverTimestamp(),
|
acceptedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
coupleId,
|
coupleId,
|
||||||
encryptedRecoveryPhrase: admin.firestore.FieldValue.delete(),
|
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();
|
await batch.commit();
|
||||||
console.log(`[acceptInviteCallable] ${callerId} accepted an invite; created couple ${coupleId}`);
|
console.log(`[acceptInviteCallable] ${callerId} accepted an invite; created couple ${coupleId}`);
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -46,7 +46,7 @@ const quietHours_1 = require("../notifications/quietHours");
|
||||||
exports.onGameSessionUpdate = functions.firestore
|
exports.onGameSessionUpdate = functions.firestore
|
||||||
.document('couples/{coupleId}/sessions/{sessionId}')
|
.document('couples/{coupleId}/sessions/{sessionId}')
|
||||||
.onWrite(async (change, context) => {
|
.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;
|
const { coupleId, sessionId } = context.params;
|
||||||
// The per-couple active-session lock lives at sessions/_active — it is a pointer, not a
|
// 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.
|
// game session, so it must never produce a partner notification.
|
||||||
|
|
@ -75,16 +75,17 @@ exports.onGameSessionUpdate = functions.firestore
|
||||||
}
|
}
|
||||||
const partnerA = userIds[0];
|
const partnerA = userIds[0];
|
||||||
const partnerB = userIds[1];
|
const partnerB = userIds[1];
|
||||||
// Get user display names for notifications
|
|
||||||
const userA = await db.collection('users').doc(partnerA).get();
|
const userA = await db.collection('users').doc(partnerA).get();
|
||||||
const userB = await db.collection('users').doc(partnerB).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';
|
// displayName is E2EE in users/{uid}, so the OS-rendered push uses a generic label; the app shows
|
||||||
const partnerBName = (_f = (_e = userB.data()) === null || _e === void 0 ? void 0 : _e.displayName) !== null && _f !== void 0 ? _f : 'Partner B';
|
// the real name in-app (resolved locally). Avatar (photoUrl) stays plaintext and is still sent.
|
||||||
const avatarA = (_g = userA.data()) === null || _g === void 0 ? void 0 : _g.photoUrl;
|
const partnerAName = 'Your partner';
|
||||||
const avatarB = (_h = userB.data()) === null || _h === void 0 ? void 0 : _h.photoUrl;
|
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.
|
// M-001: per-recipient quiet-hours lookup ("no notifications" promise). Fail-open.
|
||||||
const dataFor = (uid) => (uid === partnerA ? userA.data() : userB.data());
|
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)
|
if (!change.after.exists)
|
||||||
return; // deletion — nothing to notify
|
return; // deletion — nothing to notify
|
||||||
const status = currentData.status;
|
const status = currentData.status;
|
||||||
|
|
@ -108,7 +109,7 @@ exports.onGameSessionUpdate = functions.firestore
|
||||||
});
|
});
|
||||||
if (claimed) {
|
if (claimed) {
|
||||||
const startedBy = currentData.startedByUserId;
|
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 recipientId = startedBy === partnerA ? partnerB : partnerA;
|
||||||
const starterName = startedBy === partnerA ? partnerAName : partnerBName;
|
const starterName = startedBy === partnerA ? partnerAName : partnerBName;
|
||||||
const starterAvatar = startedBy === partnerA ? avatarA : avatarB;
|
const starterAvatar = startedBy === partnerA ? avatarA : avatarB;
|
||||||
|
|
@ -141,7 +142,7 @@ exports.onGameSessionUpdate = functions.firestore
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
if (claimed) {
|
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 joinerName = joiner === partnerA ? partnerAName : partnerBName;
|
||||||
const joinerAvatar = joiner === partnerA ? avatarA : avatarB;
|
const joinerAvatar = joiner === partnerA ? avatarA : avatarB;
|
||||||
if ((0, quietHours_1.recipientInQuietHours)(dataFor(startedBy))) {
|
if ((0, quietHours_1.recipientInQuietHours)(dataFor(startedBy))) {
|
||||||
|
|
@ -165,7 +166,7 @@ exports.onGameSessionUpdate = functions.firestore
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
if (claimed) {
|
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.
|
// Notify BOTH partners, each naming the OTHER. M-001: skip a recipient in quiet hours.
|
||||||
if ((0, quietHours_1.recipientInQuietHours)(dataFor(partnerA))) {
|
if ((0, quietHours_1.recipientInQuietHours)(dataFor(partnerA))) {
|
||||||
console.log(`[onGameSessionUpdate] ${partnerA} in quiet hours — suppressing finish push`);
|
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
|
exports.onGamePartFinished = functions.firestore
|
||||||
.document('couples/{coupleId}/{gameType}/{sessionId}')
|
.document('couples/{coupleId}/{gameType}/{sessionId}')
|
||||||
.onWrite(async (change, context) => {
|
.onWrite(async (change, context) => {
|
||||||
var _a, _b, _c, _d, _e, _f, _g;
|
var _a, _b, _c, _d, _e;
|
||||||
const { coupleId, gameType, sessionId } = context.params;
|
const { coupleId, gameType, sessionId } = context.params;
|
||||||
if (!ASYNC_GAME_COLLECTIONS.includes(gameType))
|
if (!ASYNC_GAME_COLLECTIONS.includes(gameType))
|
||||||
return; // ignore messages/reactions/etc.
|
return; // ignore messages/reactions/etc.
|
||||||
|
|
@ -236,8 +237,8 @@ exports.onGamePartFinished = functions.firestore
|
||||||
if (!recipient)
|
if (!recipient)
|
||||||
return;
|
return;
|
||||||
const finisher = await db.collection('users').doc(finisherUid).get();
|
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 finisherName = 'Your partner'; // displayName is E2EE; the app shows the real name in-app
|
||||||
const finisherAvatar = (_g = finisher.data()) === null || _g === void 0 ? void 0 : _g.photoUrl;
|
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);
|
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
|
|
@ -47,7 +47,7 @@ const quietHours_1 = require("../notifications/quietHours");
|
||||||
exports.onAnswerRevealed = functions.firestore
|
exports.onAnswerRevealed = functions.firestore
|
||||||
.document('couples/{coupleId}/daily_question/{date}/answers/{userId}')
|
.document('couples/{coupleId}/daily_question/{date}/answers/{userId}')
|
||||||
.onUpdate(async (change, context) => {
|
.onUpdate(async (change, context) => {
|
||||||
var _a, _b, _c, _d, _e, _f;
|
var _a, _b, _c, _d, _e;
|
||||||
const { coupleId, date, userId } = context.params;
|
const { coupleId, date, userId } = context.params;
|
||||||
const before = change.before.data();
|
const before = change.before.data();
|
||||||
const after = change.after.data();
|
const after = change.after.data();
|
||||||
|
|
@ -100,12 +100,13 @@ exports.onAnswerRevealed = functions.firestore
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const questionId = typeof after.questionId === 'string' ? after.questionId : '';
|
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 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 = (_e = revealerDoc.data()) === null || _e === void 0 ? void 0 : _e.photoUrl;
|
||||||
const revealerAvatar = (_f = revealerDoc.data()) === null || _f === void 0 ? void 0 : _f.photoUrl;
|
|
||||||
const payload = {
|
const payload = {
|
||||||
notification: {
|
notification: {
|
||||||
title: `${revealerName} opened your answers`,
|
title: 'Your partner opened your answers',
|
||||||
body: 'Open to see what you each said.',
|
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
|
data: Object.assign({ type: 'partner_opened_answer', couple_id: coupleId, question_id: questionId, date }, (typeof revealerAvatar === 'string' && revealerAvatar.length > 0
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
{"version":3,"file":"onAnswerRevealed.js","sourceRoot":"","sources":["../../src/questions/onAnswerRevealed.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AACvC,4DAAmE;AAEnE;;;;;;GAMG;AACU,QAAA,gBAAgB,GAAG,SAAS,CAAC,SAAS;KAChD,QAAQ,CAAC,2DAA2D,CAAC;KACrE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE;;IAClC,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAI1C,CAAA;IAED,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAsC,CAAA;IACvE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,EAAsC,CAAA;IAErE,mDAAmD;IACnD,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI;QAAE,OAAM;IAEnE,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,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;IACD,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;IAC7D,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,IAAI,CAAC,+BAA+B,MAAM,oBAAoB,QAAQ,EAAE,CAAC,CAAA;QACjF,OAAM;IACR,CAAC;IACD,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,KAAK,MAAM,CAAC,CAAA;IACvD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,OAAO,CAAC,IAAI,CAAC,4CAA4C,QAAQ,EAAE,CAAC,CAAA;QACpE,OAAM;IACR,CAAC;IAED,kEAAkE;IAClE,MAAM,MAAM,GAAa,EAAE,CAAA;IAC3B,MAAM,cAAc,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,CAAA;IACxE,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;YAAE,MAAM,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;IACzF,CAAC;IACD,MAAM,aAAa,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC,GAAG,EAAE,CAAA;IAC/F,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;YAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAClF,CAAC,CAAC,CAAA;IACF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACxB,OAAO,CAAC,GAAG,CAAC,gDAAgD,SAAS,EAAE,CAAC,CAAA;QACxE,OAAM;IACR,CAAC;IAED,kEAAkE;IAClE,IAAI,CAAA,MAAA,cAAc,CAAC,IAAI,EAAE,0CAAE,oBAAoB,MAAK,KAAK,EAAE,CAAC;QAC1D,OAAO,CAAC,GAAG,CAAC,8BAA8B,SAAS,yCAAyC,CAAC,CAAA;QAC7F,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,UAAU,GAAG,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC,CAAC,EAAE,CAAA;IAC/E,MAAM,WAAW,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,CAAA;IAClE,MAAM,YAAY,GAAG,CAAC,MAAA,WAAW,CAAC,IAAI,EAAE,0CAAE,WAAkC,KAAI,cAAc,CAAA;IAC9F,MAAM,cAAc,GAAG,MAAA,WAAW,CAAC,IAAI,EAAE,0CAAE,QAAQ,CAAA;IAEnD,MAAM,OAAO,GAAqC;QAChD,YAAY,EAAE;YACZ,KAAK,EAAE,GAAG,YAAY,sBAAsB;YAC5C,IAAI,EAAE,iCAAiC;SACxC;QACD,IAAI,kBACF,IAAI,EAAE,uBAAuB,EAC7B,SAAS,EAAE,QAAQ,EACnB,WAAW,EAAE,UAAU,EACvB,IAAI,IACD,CAAC,OAAO,cAAc,KAAK,QAAQ,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC;YACjE,CAAC,CAAC,EAAE,iBAAiB,EAAE,cAAc,EAAE;YACvC,CAAC,CAAC,EAAE,CAAC,CACR;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,EACL,OAAO,EAAE,EAAE,YAAY,EAAE,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE,GAClC,CAAC,CAC9B,CACF,CAAA;IACD,MAAM,QAAQ,GAAa,EAAE,CAAA;IAC7B,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QAC3B,IAAI,CAAC,CAAC,MAAM,KAAK,UAAU;YAAE,QAAQ,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAA;IACjF,CAAC,CAAC,CAAA;IACF,IAAI,QAAQ,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,CAAC,KAAK,CAAC,+CAA+C,EAAE,QAAQ,CAAC,CAAA;IAEjG,OAAO,CAAC,GAAG,CAAC,+BAA+B,SAAS,SAAS,MAAM,kBAAkB,QAAQ,EAAE,CAAC,CAAA;AAClG,CAAC,CAAC,CAAA"}
|
{"version":3,"file":"onAnswerRevealed.js","sourceRoot":"","sources":["../../src/questions/onAnswerRevealed.ts"],"names":[],"mappings":"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"}
|
||||||
|
|
@ -48,7 +48,7 @@ const quietHours_1 = require("../notifications/quietHours");
|
||||||
exports.onMessageWritten = functions.firestore
|
exports.onMessageWritten = functions.firestore
|
||||||
.document('couples/{coupleId}/conversations/{conversationId}/messages/{messageId}')
|
.document('couples/{coupleId}/conversations/{conversationId}/messages/{messageId}')
|
||||||
.onCreate(async (snap, context) => {
|
.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 { coupleId, conversationId, messageId } = context.params;
|
||||||
const db = admin.firestore();
|
const db = admin.firestore();
|
||||||
const messageData = snap.data();
|
const messageData = snap.data();
|
||||||
|
|
@ -103,14 +103,13 @@ exports.onMessageWritten = functions.firestore
|
||||||
console.log(`[onMessageWritten] no FCM tokens for partner ${partnerId}`);
|
console.log(`[onMessageWritten] no FCM tokens for partner ${partnerId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// The recipient sees the message from the author (their partner), so surface the author's
|
// displayName is E2EE in users/{uid}, so the server can't read it for the OS-rendered push (the
|
||||||
// photo/name — the in-app chat bubble uses sender_avatar_url to show the partner's face.
|
// 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 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 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 = {
|
const payload = {
|
||||||
notification: {
|
notification: {
|
||||||
title: authorName ? `${authorName} sent a message` : 'Your partner sent a message',
|
title: 'Your partner sent a message',
|
||||||
body: 'Tap to read and reply.',
|
body: 'Tap to read and reply.',
|
||||||
},
|
},
|
||||||
data: Object.assign({ type: 'chat_message', couple_id: coupleId, conversation_id: conversationId }, (authorPhotoUrl ? { sender_avatar_url: authorPhotoUrl } : {})),
|
data: Object.assign({ type: 'chat_message', couple_id: coupleId, conversation_id: conversationId }, (authorPhotoUrl ? { sender_avatar_url: authorPhotoUrl } : {})),
|
||||||
|
|
|
||||||
|
|
@ -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":"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"}
|
||||||
|
|
@ -71,13 +71,12 @@ export const onEntitlementChanged = functions.firestore
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscriberName =
|
// displayName is E2EE in users/{uid}, so use a generic label (this copy is stored server-side in
|
||||||
(await db.doc(`users/${userId}`).get()).data()?.displayName ?? 'Your partner'
|
// notification_queue + the push, neither of which can decrypt).
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
type: 'subscription_entitlement_changed',
|
type: 'subscription_entitlement_changed',
|
||||||
title: 'Premium unlocked ✨',
|
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.
|
// In-app record for the partner.
|
||||||
|
|
|
||||||
|
|
@ -151,6 +151,9 @@ export const acceptInviteCallable = functions.https.onCall(async (data: any, con
|
||||||
acceptedAt: admin.firestore.FieldValue.serverTimestamp(),
|
acceptedAt: admin.firestore.FieldValue.serverTimestamp(),
|
||||||
coupleId,
|
coupleId,
|
||||||
encryptedRecoveryPhrase: admin.firestore.FieldValue.delete(),
|
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()
|
await batch.commit()
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,12 @@ export const onGameSessionUpdate = functions.firestore
|
||||||
const partnerA = userIds[0]
|
const partnerA = userIds[0]
|
||||||
const partnerB = userIds[1]
|
const partnerB = userIds[1]
|
||||||
|
|
||||||
// Get user display names for notifications
|
|
||||||
const userA = await db.collection('users').doc(partnerA).get()
|
const userA = await db.collection('users').doc(partnerA).get()
|
||||||
const userB = await db.collection('users').doc(partnerB).get()
|
const userB = await db.collection('users').doc(partnerB).get()
|
||||||
const partnerAName = userA.data()?.displayName ?? 'Partner A'
|
// displayName is E2EE in users/{uid}, so the OS-rendered push uses a generic label; the app shows
|
||||||
const partnerBName = userB.data()?.displayName ?? 'Partner B'
|
// 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 avatarA = userA.data()?.photoUrl as string | undefined
|
||||||
const avatarB = userB.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.
|
// 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)
|
const recipient = userIds.find((u) => u !== finisherUid)
|
||||||
if (!recipient) return
|
if (!recipient) return
|
||||||
const finisher = await db.collection('users').doc(finisherUid).get()
|
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
|
const finisherAvatar = finisher.data()?.photoUrl as string | undefined
|
||||||
|
|
||||||
await notifyPartner(
|
await notifyPartner(
|
||||||
|
|
|
||||||
|
|
@ -72,13 +72,14 @@ export const onAnswerRevealed = functions.firestore
|
||||||
}
|
}
|
||||||
|
|
||||||
const questionId = typeof after.questionId === 'string' ? after.questionId : ''
|
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 revealerDoc = await db.collection('users').doc(userId).get()
|
||||||
const revealerName = (revealerDoc.data()?.displayName as string | undefined) || 'Your partner'
|
|
||||||
const revealerAvatar = revealerDoc.data()?.photoUrl
|
const revealerAvatar = revealerDoc.data()?.photoUrl
|
||||||
|
|
||||||
const payload: admin.messaging.MessagingPayload = {
|
const payload: admin.messaging.MessagingPayload = {
|
||||||
notification: {
|
notification: {
|
||||||
title: `${revealerName} opened your answers`,
|
title: 'Your partner opened your answers',
|
||||||
body: 'Open to see what you each said.',
|
body: 'Open to see what you each said.',
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
|
|
||||||
|
|
@ -80,15 +80,14 @@ export const onMessageWritten = functions.firestore
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// The recipient sees the message from the author (their partner), so surface the author's
|
// displayName is E2EE in users/{uid}, so the server can't read it for the OS-rendered push (the
|
||||||
// photo/name — the in-app chat bubble uses sender_avatar_url to show the partner's face.
|
// 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 authorDoc = await db.collection('users').doc(authorId).get()
|
||||||
const authorPhotoUrl = (authorDoc.data()?.photoUrl as string | undefined) ?? ''
|
const authorPhotoUrl = (authorDoc.data()?.photoUrl as string | undefined) ?? ''
|
||||||
const authorName = (authorDoc.data()?.displayName as string | undefined) ?? ''
|
|
||||||
|
|
||||||
const payload: admin.messaging.MessagingPayload = {
|
const payload: admin.messaging.MessagingPayload = {
|
||||||
notification: {
|
notification: {
|
||||||
title: authorName ? `${authorName} sent a message` : 'Your partner sent a message',
|
title: 'Your partner sent a message',
|
||||||
body: 'Tap to read and reply.',
|
body: 'Tap to read and reply.',
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue