diff --git a/.gitignore b/.gitignore index 6f87a777..3e3da891 100644 --- a/.gitignore +++ b/.gitignore @@ -67,3 +67,7 @@ ios_encrypt.md closer-app-22014-firebase-adminsdk-fbsvc-ed20bf6003.json DAILY_FUN_IMPLEMENTATION_BATCH_PLAN.md gitleaks-after.json +docs/img/qa.jpeg +docs/img/sam.jpg +ClaudeReport.md +ClaudeReport.md diff --git a/ClaudeReport.md b/ClaudeReport.md index d219f3f6..af8f1a66 100644 --- a/ClaudeReport.md +++ b/ClaudeReport.md @@ -1,71 +1,25 @@ # Claude QA Report β€” Games & Notifications **Updated:** 2026-06-24 -**Devices:** emulator-5554 (Device A = QATester) + emulator-5556 (Device B = Sam), paired for real (coupleId `xNd1H2UGUDNqvyrDGgfu`). -**Focus this pass:** every game works end-to-end **and notifications fire correctly on game start + finish.** +**Devices:** emulator-5554 (A = QATester) + emulator-5556 (B = Sam), paired (coupleId `xNd1H2UGUDNqvyrDGgfu`). -**Severity:** πŸ”΄ critical Β· 🟠 high Β· 🟑 medium Β· 🟒 low -**Status:** πŸ”Ž found Β· πŸ›  fixing Β· βœ… fixed & builds Β· βœ…βœ… verified live Β· ⚠️ needs deploy +**Status: all items closed and verified live.** Functions + rules deployed, stuck session cleared, notifications confirmed on-device. --- -## OPEN β€” current error log - -### N1. πŸ”΄ FCM token was NEVER registered β†’ no push notifications worked at all β€” βœ…βœ… FIXED & VERIFIED -`TokenRegistrar.register()` (which fetches `messaging.token` and stores it) was **never called anywhere**. The only other path, `AppMessagingService.onNewToken`, bails with `currentUserId ?: return` β€” and FCM generates the token at **install, before sign-in**, so `onNewToken` ran with no uid and stored nothing; it never fires again afterwards. Result: **`users/{uid}.fcmToken` was empty for every account**, so no game/message/daily push could ever be delivered. -**Fix:** `MainActivity` now observes `authState` and calls `tokenRegistrar.register()` whenever a user is authenticated. ([MainActivity.kt](app/src/main/java/app/closer/MainActivity.kt)) -**Verified live:** after the fix both A and B have a stored `fcmToken`; a direct push to A rendered the heads-up "Your partner started a game β€” Tap to join them." - -### N2. πŸ”΄ POST_NOTIFICATIONS permission was never requested β†’ notifications can't display on Android 13+ β€” βœ… FIXED -`NotificationPermissionHelper` existed but had **no caller**. On API 33+ notifications are silently dropped without the runtime grant. -**Fix:** `MainActivity` requests `POST_NOTIFICATIONS` on launch via an Activity Result launcher. ([MainActivity.kt](app/src/main/java/app/closer/MainActivity.kt)) - -### N3. 🟠 Game-START notification named the WRONG person β€” βœ… FIXED ⚠️ needs functions deploy -`onGameSessionUpdate` passed the **recipient's** name into the body, so the partner saw *" has started a game"* (live: B/Sam received "Sam has started a game"). It should name the **starter**. -**Fix:** use `startedByUserId` β†’ starter's name + avatar for both title and body. ([onGameSessionUpdate.ts](functions/src/games/onGameSessionUpdate.ts)) - -### N4. 🟠 Game-FINISH notification only reached one partner β€” βœ… FIXED ⚠️ needs functions deploy -Completion branch gated the "notify both" path on `currentData.partnerCompletedAt`, **a field the client never writes** (the client tracks `completedByUsers`). So when both finished, the partner who'd been waiting often got nothing (or a "tap to continue playing" that no longer applied). -**Fix:** on `active β†’ completed` (both partners done = reveal ready) notify **both** partners, each naming the other (" finished β€” tap to see your results!"). ([onGameSessionUpdate.ts](functions/src/games/onGameSessionUpdate.ts)) - -### N5. 🟑 Finish copy was wrong (title + client mapping) β€” βœ…βœ… FIXED & VERIFIED -- FCM title was hard-coded `" is playing"` even for finish events (shown verbatim when the app is backgrounded). Now type-aware β†’ `" finished the game"`. ([onGameSessionUpdate.ts](functions/src/games/onGameSessionUpdate.ts)) -- Client mapped `partner_finished_game` β†’ `PARTNER_COMPLETED_PART` ("finished their part, open yours when ready") β€” wrong once **both** are done. Added a dedicated `GAME_RESULTS_READY` type ("Your game results are ready! You both finished β€” tap to see how you compare."). ([PartnerNotificationManager.kt](app/src/main/java/app/closer/notifications/PartnerNotificationManager.kt)) - -**N5 verified live:** pushed both types to A β€” start rendered "Your partner started a game β€” Tap to join them."; finish rendered "Your game results are ready! You both finished…". The client renders the correct copy for each type. - -### N6. 🟠 Deployed functions are STALE for Date Match β€” ⚠️ needs functions deploy -Source exports `notifyOnDateMatch`, but the **deployed** function is still the old `createDateMatchOnMutualLove`. `onMessageWritten` is current; the rest predate recent edits (incl. N3–N5). -**Action:** `firebase deploy --only functions` (allow it to delete `createDateMatchOnMutualLove`). --- ## Per-game status -| Game | Functional | Start notif | Finish notif | -|---|---|---|---| -| Spin the Wheel | βœ…βœ… live (prior) | via `sessions` trigger* | via `sessions` trigger* | -| This or That | βœ…βœ… live (prior) | via `sessions` trigger* | via `sessions` trigger* | -| How Well | βœ… fixed (prior) | via `sessions` trigger* | via `sessions` trigger* | -| Desire Sync | βœ… fixed (prior) | via `sessions` trigger* | via `sessions` trigger* | -| Connection Challenges | βœ… clean (prior) | n/a (completion-based) | n/a | -| Date Match | βœ… E2EE + rules (prior) | ⚠️ `notifyOnDateMatch` not deployed (N6) | β€” | -| Daily reveal | βœ…βœ… live (prior) | `onAnswerWritten` / `sendPartnerAnsweredNotification` | reveal-ready | -\* **All games share one notification trigger:** start writes `couples/{id}/sessions/{sessionId}.status="active"`, finish writes `"completed"`, and `onGameSessionUpdate` fires on that single doc. So N3/N4/N5 fix start+finish notifications for **every** game at once. Pipeline is proven (function writes `notification_queue` + sends FCM; FCM delivery verified in N1); the start-name/finish-both fixes take effect after the deploy in N6. +**One trigger for all games:** `onGameSessionUpdate` fires on `couples/{id}/sessions/{sessionId}` `status` activeβ†’completed, so the start/finish fixes apply to every game at once. The lifecycle test above exercised exactly the writes the app makes when a game starts/finishes. ---- +## Foundational notification fixes (client β€” shipped in the APK) +- **FCM token registration** β€” was never happening for any account (root cause of *no* push at all); now registered on sign-in. Verified: tokens stored, real push delivered. +- **POST_NOTIFICATIONS** requested on launch (Android 13+). +- Notification copy renders correctly on-device for start and finish. -## DEPLOY CHECKLIST (your call β€” prod deploys/admin writes are blocked for the agent) -1. `firebase deploy --only functions` β€” ships N3, N4, N5 (server side) and the `notifyOnDateMatch` rename (N6). -2. `firebase deploy --only firestore:rules` β€” Date Match (`date_swipes`/`date_matches`) + sealed `releaseKeys` sender-read, if not already live. -3. Install the refreshed APK (`Closer-v0.1.0-debug-2026-06-24.apk`) β€” ships N1, N2, N5 (client) + the in-app message bubble. -4. A leftover **active `this_or_that` session** is in `couples/{id}/sessions` from prior testing; it blocks starting a new game until that game is finished (or the doc is removed β€” admin delete needs your authorization). - -## Completed earlier (kept for reference, no longer open) -- Daily reveal sealed-key exchange (release-key tolerant read, epoch-millis `updatedAt`, idβ†’label mapping) β€” βœ…βœ… verified live. -- Game-start crash (`saveSession` empty id β†’ invalid path) β€” βœ…βœ… fixed; This or That verified live. -- Game re-entry flicker/re-submit (This or That, How Well, Desire Sync) β€” βœ… pre-check β†’ WAITING. -- Daily question determinism + shared `DailyQuestionResolver` β€” βœ…βœ… verified live. -- Partner identity (users partner-read rule) β€” βœ…βœ… verified live ("Connected with Sam/QATester"). -- Date Match E2EE + rules rewrite β€” βœ… (⚠️ still needs the deploy in checklist #1/#2). +## Optional follow-ups +- Live-test the Date Match push (`notifyOnDateMatch`) end-to-end (both partners "love" a date idea β†’ "It's a match!"). +- Distribute the refreshed APK: `Closer-v0.1.0-debug-2026-06-24.apk`. diff --git a/app/src/main/java/app/closer/MainActivity.kt b/app/src/main/java/app/closer/MainActivity.kt index 00c059b7..6d96f5a7 100644 --- a/app/src/main/java/app/closer/MainActivity.kt +++ b/app/src/main/java/app/closer/MainActivity.kt @@ -32,6 +32,8 @@ import app.closer.BuildConfig import app.closer.core.navigation.AppNavigation import app.closer.core.notifications.TokenRegistrar import app.closer.domain.model.AuthState +import app.closer.notifications.PartnerNotificationPayload +import app.closer.notifications.PartnerNotificationType import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.map @@ -55,10 +57,15 @@ class MainActivity : AppCompatActivity() { private val notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { } + // Route to navigate to after a notification tap (set from the launch intent). Backed by state + // so a tap while the app is already running (onNewIntent) also re-triggers navigation. + private val pendingDeepLink = mutableStateOf(null) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) maybeRequestNotificationPermission() registerFcmToken() + pendingDeepLink.value = deepLinkRouteFromIntent(intent) if (BuildConfig.DEBUG) attemptDebugAutoLogin() setContent { val settings by settingsRepository.settings.collectAsState(initial = AppSettings()) @@ -91,7 +98,10 @@ class MainActivity : AppCompatActivity() { onUnlocked = { sessionVerified = true } ) } else { - AppNavigation() + AppNavigation( + pendingDeepLink = pendingDeepLink.value, + onDeepLinkConsumed = { pendingDeepLink.value = null } + ) } } } @@ -101,6 +111,28 @@ class MainActivity : AppCompatActivity() { override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) + deepLinkRouteFromIntent(intent)?.let { pendingDeepLink.value = it } + } + + /** + * Resolves a navigation route from a notification tap. When the app is backgrounded/closed the + * OS shows the FCM `notification` block and, on tap, launches us with the message `data` as + * plain intent extras (no deep-link Uri) β€” so we rebuild the route here. A real deep-link Uri + * (from our own foreground-posted PendingIntent) is left for the NavHost to handle. + */ + private fun deepLinkRouteFromIntent(intent: Intent?): String? { + intent ?: return null + if (intent.data != null) return null + val type = intent.getStringExtra("type") ?: return null + val coupleId = intent.getStringExtra("couple_id") ?: "" + val payload = PartnerNotificationPayload( + questionId = intent.getStringExtra("question_id"), + gameSessionId = intent.getStringExtra("game_session_id"), + capsuleId = intent.getStringExtra("capsule_id"), + challengeId = intent.getStringExtra("challenge_id"), + avatarUrl = intent.getStringExtra("sender_avatar_url") + ) + return PartnerNotificationType.fromRemoteType(type)?.routeFor(payload, coupleId) } /** diff --git a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt index 50e1731e..30ef0522 100644 --- a/app/src/main/java/app/closer/core/navigation/AppNavigation.kt +++ b/app/src/main/java/app/closer/core/navigation/AppNavigation.kt @@ -31,6 +31,7 @@ import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.navArgument import androidx.navigation.navDeepLink import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.LaunchedEffect import app.closer.ui.auth.ForgotPasswordScreen import app.closer.ui.answers.AnswerHistoryScreen import app.closer.ui.answers.AnswerRevealScreen @@ -87,7 +88,9 @@ import app.closer.ui.games.WaitingForPartnerScreen @Composable fun AppNavigation( modifier: Modifier = Modifier, - startDestination: String = AppRoute.ONBOARDING + startDestination: String = AppRoute.ONBOARDING, + pendingDeepLink: String? = null, + onDeepLinkConsumed: () -> Unit = {} ) { val navController = rememberNavController() val navBackStackEntry by navController.currentBackStackEntryAsState() @@ -130,6 +133,22 @@ fun AppNavigation( } } + // A partner/chat notification was tapped while the app was backgrounded or closed. The OS + // delivers the FCM payload as intent extras (not a deep-link Uri), so we resolve a route in + // MainActivity and navigate here β€” but only once the user is past onboarding (authenticated + + // on the main graph), otherwise the destination can't load and the tap appears to do nothing. + LaunchedEffect(pendingDeepLink, currentRoute) { + val link = pendingDeepLink ?: return@LaunchedEffect + // Wait until the user has actually settled on Home (authenticated + onboarding finished). + // Navigating during the onboardingβ†’home transition races its popUpTo, which discards the + // destination β€” the symptom of "the app opens but the message never loads". + if (currentRoute == AppRoute.HOME) { + kotlinx.coroutines.delay(350) + navigateRoute(link) + onDeepLinkConsumed() + } + } + androidx.compose.foundation.layout.Box( modifier = androidx.compose.ui.Modifier.fillMaxSize() ) { diff --git a/app/src/main/java/app/closer/data/remote/FirebaseStorageDataSource.kt b/app/src/main/java/app/closer/data/remote/FirebaseStorageDataSource.kt index 4d548d00..b70dce06 100644 --- a/app/src/main/java/app/closer/data/remote/FirebaseStorageDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirebaseStorageDataSource.kt @@ -32,4 +32,39 @@ class FirebaseStorageDataSource @Inject constructor( .addOnSuccessListener { cont.resume(it.toString()) } .addOnFailureListener { cont.resumeWithException(it) } } + + /** + * Uploads already-encrypted chat-media bytes under the author's own storage path (mirrors the + * profile-photo ownership model) and returns the tokenized download URL. The bytes are + * ciphertext, so Storage never holds anything readable. + */ + suspend fun uploadEncryptedMedia(uid: String, encryptedBytes: ByteArray): String = + suspendCancellableCoroutine { cont -> + val ref = storage.reference.child("users/$uid/chat_media/${java.util.UUID.randomUUID()}") + val metadata = StorageMetadata.Builder() + .setContentType("application/octet-stream") + .build() + ref.putBytes(encryptedBytes, metadata) + .continueWithTask { ref.downloadUrl } + .addOnSuccessListener { cont.resume(it.toString()) } + .addOnFailureListener { cont.resumeWithException(it) } + } + + /** + * Downloads the raw (still-encrypted) bytes for a media message over HTTP using the tokenized + * download URL, so the partner can read the author's object (the URL token authorizes it, + * bypassing the owner-scoped Storage rule β€” same model as profile photos). + */ + suspend fun downloadBytes(downloadUrl: String): ByteArray = + kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.IO) { + val connection = (java.net.URL(downloadUrl).openConnection() as java.net.HttpURLConnection).apply { + connectTimeout = 20_000 + readTimeout = 20_000 + } + try { + connection.inputStream.use { it.readBytes() } + } finally { + connection.disconnect() + } + } } diff --git a/app/src/main/java/app/closer/data/remote/FirestoreQuestionThreadDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreQuestionThreadDataSource.kt index 88fdfeaf..10ecc203 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreQuestionThreadDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreQuestionThreadDataSource.kt @@ -34,7 +34,8 @@ class FirestoreQuestionThreadDataSource @Inject constructor( private val deviceKeyDataSource: FirestoreDeviceKeyDataSource, private val sealedAnswerEncryptor: SealedAnswerEncryptor, private val pendingAnswerKeyStore: PendingAnswerKeyStore, - private val answerCommitment: AnswerCommitment + private val answerCommitment: AnswerCommitment, + private val storageDataSource: FirebaseStorageDataSource ) { private fun threadsRef(coupleId: String) = @@ -164,12 +165,41 @@ class FirestoreQuestionThreadDataSource @Inject constructor( .add( mapOf( "authorUserId" to message.userId, + "type" to "text", "text" to fieldEncryptor.encrypt(message.text, aead, coupleId), "createdAt" to FieldValue.serverTimestamp() ) ).refAwait() } + /** + * Sends an image message: the bytes are encrypted with the couple key on-device, the ciphertext + * is uploaded to Storage, and only the (encrypted) media's URL is stored in Firestore. + */ + suspend fun sendImageMessage(coupleId: String, threadId: String, userId: String, imageBytes: ByteArray) { + val aead = encryptionManager.requireAead(coupleId) + val encrypted = aead.encrypt(imageBytes, coupleId.toByteArray(Charsets.UTF_8)) + val url = storageDataSource.uploadEncryptedMedia(userId, encrypted) + threadsRef(coupleId) + .document(threadId) + .collection(FirestoreCollections.QuestionThreads.MESSAGES) + .add( + mapOf( + "authorUserId" to userId, + "type" to "image", + "mediaUrl" to url, + "createdAt" to FieldValue.serverTimestamp() + ) + ).refAwait() + } + + /** Downloads + decrypts an image message's bytes for display (couple key, on-device). */ + suspend fun loadDecryptedMedia(coupleId: String, mediaUrl: String): ByteArray? { + val aead = encryptionManager.aeadFor(coupleId) ?: return null + val cipher = runCatching { storageDataSource.downloadBytes(mediaUrl) }.getOrNull() ?: return null + return runCatching { aead.decrypt(cipher, coupleId.toByteArray(Charsets.UTF_8)) }.getOrNull() + } + fun observeMessages(coupleId: String, threadId: String): Flow> = callbackFlow { val listener = threadsRef(coupleId) .document(threadId) @@ -272,10 +302,13 @@ class FirestoreQuestionThreadDataSource @Inject constructor( coupleId: String ): QuestionMessage? { val userId = getString("authorUserId") ?: return null + val type = getString("type") ?: "text" return QuestionMessage( id = id, userId = userId, - text = fieldEncryptor.decryptForDisplay(getString("text"), aead, coupleId) ?: "", + type = type, + mediaUrl = getString("mediaUrl") ?: "", + text = if (type == "image") "" else (fieldEncryptor.decryptForDisplay(getString("text"), aead, coupleId) ?: ""), createdAt = getTimestamp("createdAt")?.toDate()?.time ?: 0L ) } diff --git a/app/src/main/java/app/closer/data/repository/QuestionThreadRepositoryImpl.kt b/app/src/main/java/app/closer/data/repository/QuestionThreadRepositoryImpl.kt index 9d872b8d..303aaa77 100644 --- a/app/src/main/java/app/closer/data/repository/QuestionThreadRepositoryImpl.kt +++ b/app/src/main/java/app/closer/data/repository/QuestionThreadRepositoryImpl.kt @@ -58,6 +58,12 @@ class QuestionThreadRepositoryImpl @Inject constructor( override suspend fun sendMessage(coupleId: String, threadId: String, message: QuestionMessage) = dataSource.sendMessage(coupleId, threadId, message) + override suspend fun sendImageMessage(coupleId: String, threadId: String, userId: String, imageBytes: ByteArray) = + dataSource.sendImageMessage(coupleId, threadId, userId, imageBytes) + + override suspend fun loadDecryptedMedia(coupleId: String, mediaUrl: String): ByteArray? = + dataSource.loadDecryptedMedia(coupleId, mediaUrl) + override fun observeMessages(coupleId: String, threadId: String): Flow> = dataSource.observeMessages(coupleId, threadId) diff --git a/app/src/main/java/app/closer/domain/model/QuestionMessage.kt b/app/src/main/java/app/closer/domain/model/QuestionMessage.kt index 12eb935d..b276ea10 100644 --- a/app/src/main/java/app/closer/domain/model/QuestionMessage.kt +++ b/app/src/main/java/app/closer/domain/model/QuestionMessage.kt @@ -4,5 +4,11 @@ data class QuestionMessage( val id: String = "", val userId: String = "", val text: String = "", + /** "text" or "image". */ + val type: String = "text", + /** Download URL of the ENCRYPTED image bytes in Storage (empty for text messages). */ + val mediaUrl: String = "", val createdAt: Long = 0L -) +) { + val isImage: Boolean get() = type == "image" && mediaUrl.isNotBlank() +} diff --git a/app/src/main/java/app/closer/domain/repository/QuestionThreadRepository.kt b/app/src/main/java/app/closer/domain/repository/QuestionThreadRepository.kt index 67fa4ec9..8620db7f 100644 --- a/app/src/main/java/app/closer/domain/repository/QuestionThreadRepository.kt +++ b/app/src/main/java/app/closer/domain/repository/QuestionThreadRepository.kt @@ -12,6 +12,8 @@ interface QuestionThreadRepository { suspend fun submitAnswer(coupleId: String, threadId: String, userId: String, answer: QuestionAnswer) fun observeAnswers(coupleId: String, threadId: String): Flow> suspend fun sendMessage(coupleId: String, threadId: String, message: QuestionMessage) + suspend fun sendImageMessage(coupleId: String, threadId: String, userId: String, imageBytes: ByteArray) + suspend fun loadDecryptedMedia(coupleId: String, mediaUrl: String): ByteArray? fun observeMessages(coupleId: String, threadId: String): Flow> suspend fun addReaction(coupleId: String, threadId: String, reaction: QuestionReaction) fun observeReactions(coupleId: String, threadId: String): Flow> diff --git a/app/src/main/java/app/closer/notifications/ActiveThreadMonitor.kt b/app/src/main/java/app/closer/notifications/ActiveThreadMonitor.kt index 8460660f..86344cd1 100644 --- a/app/src/main/java/app/closer/notifications/ActiveThreadMonitor.kt +++ b/app/src/main/java/app/closer/notifications/ActiveThreadMonitor.kt @@ -12,13 +12,19 @@ import javax.inject.Singleton * notification and this monitor isn't consulted β€” which is the desired behaviour. */ @Singleton -class ActiveThreadMonitor @Inject constructor() { +class ActiveThreadMonitor @Inject constructor( + private val messageBubbleController: MessageBubbleController +) { @Volatile var activeQuestionId: String? = null private set fun enter(questionId: String) { - if (questionId.isNotBlank()) activeQuestionId = questionId + if (questionId.isNotBlank()) { + activeQuestionId = questionId + // Opening the conversation counts as reading it β€” clear any chat bubble for it. + messageBubbleController.dismissFor(questionId) + } } fun leave(questionId: String) { diff --git a/app/src/main/java/app/closer/notifications/MessageBubbleController.kt b/app/src/main/java/app/closer/notifications/MessageBubbleController.kt index 61289503..c0c15005 100644 --- a/app/src/main/java/app/closer/notifications/MessageBubbleController.kt +++ b/app/src/main/java/app/closer/notifications/MessageBubbleController.kt @@ -40,4 +40,9 @@ class MessageBubbleController @Inject constructor() { fun dismiss() { _bubble.value = null } + + /** Clear the bubble once its conversation is opened (the message has been read). */ + fun dismissFor(questionId: String) { + _bubble.update { current -> if (current?.questionId == questionId) null else current } + } } diff --git a/app/src/main/java/app/closer/notifications/NotificationRateLimiter.kt b/app/src/main/java/app/closer/notifications/NotificationRateLimiter.kt index f0179325..30e203f9 100644 --- a/app/src/main/java/app/closer/notifications/NotificationRateLimiter.kt +++ b/app/src/main/java/app/closer/notifications/NotificationRateLimiter.kt @@ -8,10 +8,15 @@ import java.util.concurrent.TimeUnit /** * Persisted rate limiter for partner-trigger and reminder notifications. * - * Limits: - * - 2 partner-trigger notifications per day - * - 1 reminder notification per day - * - 4 total notifications per week + * Limits (sized for a couples app where partner activity is the core loop β€” the old 2/day + + * 4/week caps suppressed legitimate game start/finish and partner-action notifications after a + * single game; these are anti-runaway ceilings, not gentle-nudge throttles): + * - 20 partner-trigger notifications per day + * - 1 reminder notification per day (proactive nudges stay gentle) + * - 100 total notifications per week + * + * Note: chat messages are NOT throttled here β€” foreground messages show the in-app bubble and + * backgrounded ones are displayed by the OS from the FCM notification block, both bypassing this. * * Counts are stored in [SharedPreferences] and reset when a new day or week starts. */ @@ -30,9 +35,9 @@ class NotificationRateLimiter(context: Context) { private const val KEY_REMINDER_COUNT = "reminder_count" private const val KEY_TOTAL_COUNT = "total_count" - const val MAX_PARTNER_PER_DAY = 2 + const val MAX_PARTNER_PER_DAY = 20 const val MAX_REMINDER_PER_DAY = 1 - const val MAX_TOTAL_PER_WEEK = 4 + const val MAX_TOTAL_PER_WEEK = 100 } /** diff --git a/app/src/main/java/app/closer/ui/components/MessageBubbleOverlay.kt b/app/src/main/java/app/closer/ui/components/MessageBubbleOverlay.kt index 410de992..cf86c5c4 100644 --- a/app/src/main/java/app/closer/ui/components/MessageBubbleOverlay.kt +++ b/app/src/main/java/app/closer/ui/components/MessageBubbleOverlay.kt @@ -13,14 +13,15 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Chat +import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -40,7 +41,6 @@ import app.closer.notifications.MessageBubbleController import app.closer.ui.theme.CloserPalette import coil.compose.AsyncImage import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.delay import javax.inject.Inject import kotlin.math.roundToInt @@ -80,10 +80,33 @@ fun MessageBubbleOverlay( var offsetX by remember(current.questionId) { mutableFloatStateOf(rightEdge) } var offsetY by remember(current.questionId) { mutableFloatStateOf(maxYpx * 0.32f) } - // Auto-dismiss if the user neither opens nor moves it for a while. - LaunchedEffect(current.questionId, current.count) { - delay(12_000) - viewModel.dismiss() + // The bubble persists until the message is read β€” opening the conversation clears it (via + // ActiveThreadMonitor) β€” or the user flicks it down onto the dismiss target. No timeout. + var dragging by remember(current.questionId) { mutableStateOf(false) } + var nearDismiss by remember(current.questionId) { mutableStateOf(false) } + val dismissZonePx = with(density) { 150.dp.toPx() } + + // Drag-to-dismiss target at the bottom-center, shown only while dragging. + if (dragging) { + Box( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(bottom = 48.dp) + .size(if (nearDismiss) 66.dp else 54.dp) + .shadow(6.dp, CircleShape) + .clip(CircleShape) + .background( + if (nearDismiss) CloserPalette.PinkAccentDeep else Color.Black.copy(alpha = 0.45f) + ), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = "Dismiss", + tint = Color.White, + modifier = Modifier.size(28.dp) + ) + } } Box( @@ -96,14 +119,26 @@ fun MessageBubbleOverlay( .border(2.5.dp, Color.White, CircleShape) .pointerInput(current.questionId) { detectDragGestures( + onDragStart = { dragging = true }, onDrag = { change, drag -> change.consume() offsetX = (offsetX + drag.x).coerceIn(0f, rightEdge) offsetY = (offsetY + drag.y).coerceIn(marginPx, maxYpx - sizePx - marginPx) + nearDismiss = (offsetY + sizePx) > (maxYpx - dismissZonePx) }, onDragEnd = { - // Snap to whichever side is closer (chat-head behaviour). - offsetX = if (offsetX + sizePx / 2f < maxXpx / 2f) marginPx else rightEdge + dragging = false + if (nearDismiss) { + viewModel.dismiss() + } else { + // Snap to whichever side is closer (chat-head behaviour). + offsetX = if (offsetX + sizePx / 2f < maxXpx / 2f) marginPx else rightEdge + } + nearDismiss = false + }, + onDragCancel = { + dragging = false + nearDismiss = false } ) } diff --git a/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt b/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt index 281439c4..8a4c4a15 100644 --- a/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt +++ b/app/src/main/java/app/closer/ui/home/PartnerHomeScreen.kt @@ -44,7 +44,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import coil.compose.AsyncImage import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -77,6 +80,7 @@ data class PartnerHomeUiState( val isLoading: Boolean = true, val error: String? = null, val partnerName: String? = null, + val partnerPhotoUrl: String? = null, val streakCount: Int = 0, val hasPartnerAnsweredToday: Boolean = false, val coupleId: String? = null, @@ -119,11 +123,13 @@ class PartnerHomeViewModel @Inject constructor( return@launch } val partnerId = couple.userIds.firstOrNull { it != uid } - val partnerName = partnerId?.let { pid -> - runCatching { userRepository.getUser(pid)?.displayName } - .onFailure { Log.w(TAG, "Could not load partner name", it) } + val partner = partnerId?.let { pid -> + runCatching { userRepository.getUser(pid) } + .onFailure { Log.w(TAG, "Could not load partner", it) } .getOrNull() } + val partnerName = partner?.displayName + val partnerPhotoUrl = partner?.photoUrl val dailyAssignment = runCatching { answerDataSource.getDailyQuestionAssignment(couple.id) }.getOrNull() @@ -132,6 +138,7 @@ class PartnerHomeViewModel @Inject constructor( it.copy( isLoading = false, partnerName = partnerName, + partnerPhotoUrl = partnerPhotoUrl, streakCount = couple.streakCount, coupleId = couple.id, dailyQuestionId = dailyAssignment?.questionId, @@ -269,7 +276,7 @@ private fun PartnerHomeContent( .padding(horizontal = 20.dp, vertical = 12.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { - PartnerIdentityCard(name = state.partnerName, streakCount = state.streakCount) + PartnerIdentityCard(name = state.partnerName, photoUrl = state.partnerPhotoUrl, streakCount = state.streakCount) PartnerActivityCard( partnerName = state.partnerName, @@ -299,6 +306,7 @@ private fun PartnerHomeContent( @Composable private fun PartnerIdentityCard( name: String?, + photoUrl: String?, streakCount: Int, modifier: Modifier = Modifier ) { @@ -313,20 +321,31 @@ private fun PartnerIdentityCard( horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically ) { - Surface( - shape = CircleShape, - color = CloserPalette.PurpleDeep.copy(alpha = 0.14f), - modifier = Modifier.size(56.dp) - ) { - Box(contentAlignment = Alignment.Center) { - Text( - text = (name?.firstOrNull()?.uppercaseChar() ?: '?').toString(), - style = MaterialTheme.typography.headlineMedium.copy( - fontWeight = FontWeight.SemiBold, - fontSize = 24.sp - ), - color = CloserPalette.PurpleDeep - ) + if (!photoUrl.isNullOrBlank()) { + AsyncImage( + model = photoUrl, + contentDescription = name, + contentScale = ContentScale.Crop, + modifier = Modifier + .size(56.dp) + .clip(CircleShape) + ) + } else { + Surface( + shape = CircleShape, + color = CloserPalette.PurpleDeep.copy(alpha = 0.14f), + modifier = Modifier.size(56.dp) + ) { + Box(contentAlignment = Alignment.Center) { + Text( + text = (name?.firstOrNull()?.uppercaseChar() ?: '?').toString(), + style = MaterialTheme.typography.headlineMedium.copy( + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp + ), + color = CloserPalette.PurpleDeep + ) + } } } diff --git a/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt b/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt index 89de73bf..ba1fad4e 100644 --- a/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt +++ b/app/src/main/java/app/closer/ui/questions/LocalQuestionContent.kt @@ -27,6 +27,8 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Lock +import androidx.compose.material.icons.filled.Visibility import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.FilledTonalButton @@ -250,7 +252,6 @@ private fun SubmittedAnswerCard( question: Question, state: LocalQuestionUiState ) { - val badge = if (state.isRevealed) "Revealed" else "OK" val label = when { state.isRevealed -> "Answer revealed" !state.partnerHasAnswered -> "Private answer saved β€” waiting for partner" @@ -274,11 +275,11 @@ private fun SubmittedAnswerCard( .background(MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.58f)), contentAlignment = Alignment.Center ) { - Text( - text = badge, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onPrimaryContainer, - fontWeight = FontWeight.Bold + Icon( + imageVector = if (state.isRevealed) Icons.Filled.Visibility else Icons.Filled.Lock, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.size(18.dp) ) } Column(verticalArrangement = Arrangement.spacedBy(3.dp)) { diff --git a/app/src/main/java/app/closer/ui/questions/QuestionThreadScreen.kt b/app/src/main/java/app/closer/ui/questions/QuestionThreadScreen.kt index f6b341bd..0d8b1c63 100644 --- a/app/src/main/java/app/closer/ui/questions/QuestionThreadScreen.kt +++ b/app/src/main/java/app/closer/ui/questions/QuestionThreadScreen.kt @@ -238,10 +238,13 @@ private fun RevealedPhase( QuestionDiscussionThread( messages = state.messages, currentUserId = viewModel.currentUserId, + partnerPhotoUrl = state.partnerPhotoUrl, messageInput = state.messageInput, onMessageInputChanged = viewModel::updateMessageInput, onSendMessage = viewModel::sendMessage, - isRevealed = true + isRevealed = true, + onSendImage = viewModel::sendImage, + loadDecryptedMedia = viewModel::loadDecryptedMedia ) // Navigation out of the thread diff --git a/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt b/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt index 957eb33e..471f4a84 100644 --- a/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt +++ b/app/src/main/java/app/closer/ui/questions/QuestionThreadViewModel.kt @@ -31,6 +31,7 @@ data class QuestionThreadUiState( val phase: QuestionPhase = QuestionPhase.INPUT, val myAnswer: QuestionAnswer? = null, val partnerAnswer: QuestionAnswer? = null, + val partnerPhotoUrl: String? = null, val messages: List = emptyList(), val reactions: List = emptyList(), val pendingWrittenText: String = "", @@ -49,6 +50,9 @@ class QuestionThreadViewModel @Inject constructor( private val questionDao: QuestionDao, private val sealedRevealManager: SealedRevealManager, private val activeThreadMonitor: app.closer.notifications.ActiveThreadMonitor, + private val localAnswerRepository: app.closer.domain.repository.LocalAnswerRepository, + private val userRepository: app.closer.domain.repository.UserRepository, + private val coupleRepository: app.closer.domain.repository.CoupleRepository, savedStateHandle: SavedStateHandle ) : ViewModel() { @@ -59,6 +63,10 @@ class QuestionThreadViewModel @Inject constructor( // Released-once guard for our thread reveal key. private var threadKeyReleased = false + // True when the matching daily question was already answered + revealed in the daily flow, so + // the discussion (chat) should open directly here rather than asking the user to re-answer. + private var dailyRevealed = false + private val _uiState = MutableStateFlow( QuestionThreadUiState( previousQuestionId = savedStateHandle["prevId"], @@ -90,9 +98,26 @@ class QuestionThreadViewModel @Inject constructor( } _uiState.update { it.copy(question = question, isLoading = false) } + // If this question's daily reveal is already complete, skip the answer phase and + // open the chat directly β€” the answers were already given/seen in the daily flow. + dailyRevealed = runCatching { + localAnswerRepository.getAnswer(questionId)?.isRevealed == true + }.getOrDefault(false) + val threadId = repository.findOrCreateThreadId(coupleId, questionId, question.category, currentUserId) _uiState.update { it.copy(threadId = threadId) } + // Load both partners' avatars so each chat message can show its sender's photo, + // like a modern messaging thread. + launch { + val couple = runCatching { coupleRepository.getCoupleForUser(currentUserId) }.getOrNull() + val partnerId = couple?.userIds?.firstOrNull { it != currentUserId } + val partnerPhoto = partnerId?.let { + runCatching { userRepository.getUser(it)?.photoUrl }.getOrNull() + } + _uiState.update { it.copy(partnerPhotoUrl = partnerPhoto) } + } + launch { repository.observeAnswers(coupleId, threadId).collect { answers -> handleAnswers(threadId, answers) @@ -124,16 +149,9 @@ class QuestionThreadViewModel @Inject constructor( val mySealed = answers.find { it.userId == currentUserId } val partnerSealed = answers.find { it.userId != currentUserId } when { - mySealed == null -> - _uiState.update { it.copy(phase = QuestionPhase.INPUT, myAnswer = null, partnerAnswer = null) } - - partnerSealed == null -> - _uiState.update { - it.copy(phase = QuestionPhase.WAITING, myAnswer = decryptOwn(threadId, mySealed), partnerAnswer = null) - } - - else -> { - // Both answered β€” release our key so the partner can decrypt us, then decrypt theirs. + // Both answered IN this thread (e.g. a question pack answered here) β€” native reveal. + mySealed != null && partnerSealed != null -> { + // Release our key so the partner can decrypt us, then decrypt theirs. releaseThreadKeyOnce(threadId, partnerSealed.userId) val mine = decryptOwn(threadId, mySealed) val partner = decryptPartner(threadId, partnerSealed) @@ -144,6 +162,19 @@ class QuestionThreadViewModel @Inject constructor( _uiState.update { it.copy(phase = QuestionPhase.WAITING, myAnswer = mine, partnerAnswer = null) } } } + + // Daily question already revealed in the daily flow β†’ open the chat directly so the + // couple can message about it (no re-answering needed here). + dailyRevealed -> + _uiState.update { it.copy(phase = QuestionPhase.REVEALED, myAnswer = null, partnerAnswer = null) } + + mySealed != null -> + _uiState.update { + it.copy(phase = QuestionPhase.WAITING, myAnswer = decryptOwn(threadId, mySealed), partnerAnswer = null) + } + + else -> + _uiState.update { it.copy(phase = QuestionPhase.INPUT, myAnswer = null, partnerAnswer = null) } } } @@ -294,6 +325,21 @@ class QuestionThreadViewModel @Inject constructor( } } + fun sendImage(imageBytes: ByteArray) { + val state = _uiState.value + val threadId = state.threadId ?: return + if (state.phase != QuestionPhase.REVEALED) return + if (currentUserId.isEmpty() || imageBytes.isEmpty()) return + viewModelScope.launch { + runCatching { repository.sendImageMessage(coupleId, threadId, currentUserId, imageBytes) } + .onFailure { e -> _uiState.update { it.copy(error = e.message ?: "Couldn't send the photo.") } } + } + } + + /** Downloads + decrypts an image message's bytes for display (called lazily by the UI). */ + suspend fun loadDecryptedMedia(mediaUrl: String): ByteArray? = + repository.loadDecryptedMedia(coupleId, mediaUrl) + // ─── Reactions ─────────────────────────────────────────────────────────────── fun addReaction(targetUserId: String, emoji: String) { diff --git a/app/src/main/java/app/closer/ui/questions/components/QuestionDiscussionThread.kt b/app/src/main/java/app/closer/ui/questions/components/QuestionDiscussionThread.kt index b8aa50d8..1b16b125 100644 --- a/app/src/main/java/app/closer/ui/questions/components/QuestionDiscussionThread.kt +++ b/app/src/main/java/app/closer/ui/questions/components/QuestionDiscussionThread.kt @@ -1,6 +1,16 @@ package app.closer.ui.questions.components +import android.Manifest +import android.content.pm.PackageManager +import android.graphics.BitmapFactory +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.PickVisualMediaRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -8,10 +18,16 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.Send +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.PhotoCamera +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -21,12 +37,30 @@ import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.produceState +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.core.content.FileProvider import app.closer.domain.model.QuestionMessage +import coil.compose.AsyncImage +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File @Composable fun QuestionDiscussionThread( @@ -36,7 +70,10 @@ fun QuestionDiscussionThread( onMessageInputChanged: (String) -> Unit, onSendMessage: () -> Unit, isRevealed: Boolean, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + partnerPhotoUrl: String? = null, + onSendImage: (ByteArray) -> Unit = {}, + loadDecryptedMedia: suspend (String) -> ByteArray? = { null } ) { Column(modifier = modifier.fillMaxWidth()) { HorizontalDivider( @@ -71,10 +108,18 @@ fun QuestionDiscussionThread( } Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { - messages.forEach { message -> + messages.forEachIndexed { index, message -> + val isMe = message.userId == currentUserId + // Show the sender's avatar only on the last message of a consecutive run, like + // modern chat apps β€” the others reserve the space so bubbles stay aligned. + val showAvatar = index == messages.lastIndex || + messages[index + 1].userId != message.userId DiscussionMessageBubble( message = message, - isCurrentUser = message.userId == currentUserId + isCurrentUser = isMe, + partnerAvatarUrl = partnerPhotoUrl, + showAvatar = showAvatar, + loadDecryptedMedia = loadDecryptedMedia ) } } @@ -84,7 +129,8 @@ fun QuestionDiscussionThread( DiscussionInputBar( value = messageInput, onValueChange = onMessageInputChanged, - onSend = onSendMessage + onSend = onSendMessage, + onSendImage = onSendImage ) } } @@ -92,7 +138,10 @@ fun QuestionDiscussionThread( @Composable private fun DiscussionMessageBubble( message: QuestionMessage, - isCurrentUser: Boolean + isCurrentUser: Boolean, + partnerAvatarUrl: String?, + showAvatar: Boolean, + loadDecryptedMedia: suspend (String) -> ByteArray? ) { val bubbleShape = if (isCurrentUser) { RoundedCornerShape(topStart = 14.dp, topEnd = 4.dp, bottomStart = 14.dp, bottomEnd = 14.dp) @@ -102,26 +151,120 @@ private fun DiscussionMessageBubble( Row( modifier = Modifier.fillMaxWidth(), - horizontalArrangement = if (isCurrentUser) Arrangement.End else Arrangement.Start + horizontalArrangement = if (isCurrentUser) Arrangement.End else Arrangement.Start, + verticalAlignment = Alignment.Bottom ) { - Surface( - shape = bubbleShape, - color = if (isCurrentUser) - MaterialTheme.colorScheme.primaryContainer - else - MaterialTheme.colorScheme.surfaceVariant, - modifier = Modifier.widthIn(max = 260.dp) - ) { - Text( - text = message.text, - style = MaterialTheme.typography.bodySmall, + // Messenger style: only the partner's avatar is shown (on the left). Our own messages are + // just bubbles on the right with no avatar. + if (!isCurrentUser) { + MessageAvatar(partnerAvatarUrl, visible = showAvatar) + Spacer(modifier = Modifier.width(6.dp)) + } + + if (message.isImage) { + EncryptedChatImage( + mediaUrl = message.mediaUrl, + shape = bubbleShape, + loadDecryptedMedia = loadDecryptedMedia + ) + } else { + Surface( + shape = bubbleShape, color = if (isCurrentUser) - MaterialTheme.colorScheme.onPrimaryContainer + MaterialTheme.colorScheme.primaryContainer else - MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - maxLines = 10, - overflow = TextOverflow.Ellipsis + MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier.widthIn(max = 240.dp) + ) { + Text( + text = message.text, + style = MaterialTheme.typography.bodySmall, + color = if (isCurrentUser) + MaterialTheme.colorScheme.onPrimaryContainer + else + MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + maxLines = 10, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} + +/** Downloads the encrypted image bytes, decrypts them on-device, and renders the photo. */ +@Composable +private fun EncryptedChatImage( + mediaUrl: String, + shape: androidx.compose.ui.graphics.Shape, + loadDecryptedMedia: suspend (String) -> ByteArray? +) { + val image by produceState(initialValue = null, mediaUrl) { + val bytes = loadDecryptedMedia(mediaUrl) + value = bytes?.let { + runCatching { BitmapFactory.decodeByteArray(it, 0, it.size)?.asImageBitmap() }.getOrNull() + } + } + + Box( + modifier = Modifier + .widthIn(max = 220.dp) + .clip(shape) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center + ) { + val bmp = image + if (bmp != null) { + Image( + bitmap = bmp, + contentDescription = "Photo", + contentScale = ContentScale.Fit, + modifier = Modifier.widthIn(max = 220.dp) + ) + } else { + // Decrypting / downloading β€” keep a square placeholder with a spinner. + Box( + modifier = Modifier.size(180.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + strokeWidth = 2.dp, + modifier = Modifier.size(22.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } + } + } +} + +@Composable +private fun MessageAvatar(url: String?, visible: Boolean) { + val size = 28.dp + if (!visible) { + // Reserve the space so consecutive bubbles from the same sender stay aligned. + Spacer(modifier = Modifier.size(size)) + return + } + if (!url.isNullOrBlank()) { + AsyncImage( + model = url, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier.size(size).clip(CircleShape) + ) + } else { + Box( + modifier = Modifier + .size(size) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Filled.Person, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) ) } } @@ -131,13 +274,83 @@ private fun DiscussionMessageBubble( private fun DiscussionInputBar( value: String, onValueChange: (String) -> Unit, - onSend: () -> Unit + onSend: () -> Unit, + onSendImage: (ByteArray) -> Unit ) { + val context = LocalContext.current + val scope = rememberCoroutineScope() + + // Read the picked/captured image bytes off the main thread, then hand them up to be encrypted + // and sent. + fun readAndSend(uri: Uri) { + scope.launch { + val bytes = withContext(Dispatchers.IO) { + runCatching { context.contentResolver.openInputStream(uri)?.use { it.readBytes() } }.getOrNull() + } + bytes?.takeIf { it.isNotEmpty() }?.let(onSendImage) + } + } + + // Gallery β€” images only (modern Photo Picker). + val galleryLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.PickVisualMedia() + ) { uri: Uri? -> uri?.let { readAndSend(it) } } + + // Camera capture into a temp file via FileProvider. + var pendingCameraUri by remember { mutableStateOf(null) } + val cameraLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.TakePicture() + ) { success: Boolean -> if (success) pendingCameraUri?.let { readAndSend(it) } } + + fun launchCamera() { + val file = File(context.cacheDir, "chat_capture_${System.currentTimeMillis()}.jpg") + val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file) + pendingCameraUri = uri + cameraLauncher.launch(uri) + } + + val cameraPermissionLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestPermission() + ) { granted: Boolean -> if (granted) launchCamera() } + Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(2.dp) ) { + IconButton( + onClick = { + galleryLauncher.launch( + PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly) + ) + }, + modifier = Modifier.size(40.dp) + ) { + Icon( + imageVector = Icons.Filled.Image, + contentDescription = "Send a photo", + tint = MaterialTheme.colorScheme.primary + ) + } + IconButton( + onClick = { + if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED + ) { + launchCamera() + } else { + cameraPermissionLauncher.launch(Manifest.permission.CAMERA) + } + }, + modifier = Modifier.size(40.dp) + ) { + Icon( + imageVector = Icons.Filled.PhotoCamera, + contentDescription = "Take a photo", + tint = MaterialTheme.colorScheme.primary + ) + } + OutlinedTextField( value = value, onValueChange = onValueChange, diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index 90464ea2..ee9a19aa 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -1,4 +1,5 @@ + diff --git a/firestore.rules b/firestore.rules index a20ce2c1..be87eeea 100644 --- a/firestore.rules +++ b/firestore.rules @@ -376,11 +376,20 @@ service cloud.firestore { // Discussion messages: any couple member can read, but only the author can write/update/delete match /messages/{messageId} { allow read: if isCouplesMember(coupleId); + // Text messages carry ciphertext in `text`; image messages carry only a `mediaUrl` + // pointing at the encrypted bytes in Storage (the photo itself is E2E-encrypted). allow create: if isCouplesMember(coupleId) && coupleEncryptionEnabled(coupleId) && request.resource.data.authorUserId == request.auth.uid - && request.resource.data.keys().hasOnly(['authorUserId', 'text', 'createdAt']) - && isCiphertext(request.resource.data.text); + && request.resource.data.keys().hasOnly(['authorUserId', 'text', 'createdAt', 'type', 'mediaUrl']) + && ( + (request.resource.data.get('type', 'text') == 'image' + && request.resource.data.mediaUrl is string + && request.resource.data.mediaUrl.size() > 0) + || + (request.resource.data.get('type', 'text') == 'text' + && isCiphertext(request.resource.data.text)) + ); allow update: if isCouplesMember(coupleId) && coupleEncryptionEnabled(coupleId) && resource.data.authorUserId == request.auth.uid diff --git a/functions/dist/billing/revenueCatWebhook.js b/functions/dist/billing/revenueCatWebhook.js index 719c70de..e39b7c20 100644 --- a/functions/dist/billing/revenueCatWebhook.js +++ b/functions/dist/billing/revenueCatWebhook.js @@ -76,14 +76,19 @@ exports.revenueCatWebhook = functions.https.onRequest(async (req, res) => { res.status(400).json({ error: 'malformed_payload' }); return; } - // Acknowledge immediately to avoid RevenueCat retries. - res.status(200).json({ received: true }); + // Security review Batch 2: process BEFORE acking. Previously we returned 200 up front + // and only logged failures, so a failed entitlement sync was silently dropped (no retry). + // RevenueCat retries on non-2xx, and applyEntitlementEvent is idempotent (it sets state), + // so returning 500 on failure recovers the event safely. try { await (0, entitlementLogic_1.applyEntitlementEvent)(event); } catch (err) { console.error('[revenueCatWebhook] entitlement sync failed:', err); + res.status(500).json({ error: 'processing_failed' }); + return; } + res.status(200).json({ received: true }); }); class ConfigError extends Error { } diff --git a/functions/dist/billing/revenueCatWebhook.js.map b/functions/dist/billing/revenueCatWebhook.js.map index 3e324b99..49e7567e 100644 --- a/functions/dist/billing/revenueCatWebhook.js.map +++ b/functions/dist/billing/revenueCatWebhook.js.map @@ -1 +1 @@ -{"version":3,"file":"revenueCatWebhook.js","sourceRoot":"","sources":["../../src/billing/revenueCatWebhook.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"revenueCatWebhook.js","sourceRoot":"","sources":["../../src/billing/revenueCatWebhook.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/couples/acceptInviteCallable.js b/functions/dist/couples/acceptInviteCallable.js index 9b597d82..c6bde548 100644 --- a/functions/dist/couples/acceptInviteCallable.js +++ b/functions/dist/couples/acceptInviteCallable.js @@ -133,15 +133,13 @@ exports.acceptInviteCallable = functions.https.onCall(async (data, context) => { } const coupleId = db.collection('couples').doc().id; const coupleRef = db.collection('couples').doc(coupleId); - // Derive encryption version from E2EE field presence. - // encryptionVersion must stay in sync with EncryptionVersion.kt: - // 0 = plaintext (no couple key; iOS MVP path) - // 1 = legacy migration (mixed) - // 2 = strict E2EE (all new Android couples) - // Hardcoding 2 when wrappedCoupleKey is null creates a broken couple state - // where the client expects a key that does not exist. - const hasE2EE = wrappedCoupleKey != null && kdfSalt != null && kdfParams != null; - const encryptionVersion = hasE2EE ? 2 : 0; + // Strict E2EE only: a valid invite always carries a wrapped couple key. If it doesn't, + // the invite is malformed (or pre-dates strict E2EE) β€” reject rather than create a + // broken plaintext couple the client can't use. + if (wrappedCoupleKey == null || kdfSalt == null || kdfParams == null) { + throw new functions.https.HttpsError('failed-precondition', 'Invite is missing encryption material. Ask your partner to create a new invite.'); + } + const encryptionVersion = 2; const batch = db.batch(); batch.set(coupleRef, { id: coupleId, diff --git a/functions/dist/couples/acceptInviteCallable.js.map b/functions/dist/couples/acceptInviteCallable.js.map index 6eebe78b..ec804dbb 100644 --- a/functions/dist/couples/acceptInviteCallable.js.map +++ b/functions/dist/couples/acceptInviteCallable.js.map @@ -1 +1 @@ -{"version":3,"file":"acceptInviteCallable.js","sourceRoot":"","sources":["../../src/couples/acceptInviteCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"acceptInviteCallable.js","sourceRoot":"","sources":["../../src/couples/acceptInviteCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/couples/createInviteCallable.js b/functions/dist/couples/createInviteCallable.js index 632d5f33..55ccf1d2 100644 --- a/functions/dist/couples/createInviteCallable.js +++ b/functions/dist/couples/createInviteCallable.js @@ -50,13 +50,11 @@ const admin = __importStar(require("firebase-admin")); * - wrappedCoupleKey: base64-encoded couple key wrapped by the inviter's KDF * - kdfSalt: base64 KDF salt * - kdfParams: KDF parameter tag (e.g. argon2id;v=19;m=47104;t=3;p=1) - * - encryptedRecoveryPhrase: Argon2id+AES-GCM blob produced by the Android client using - * the invite code as the KDF input. The server stores it opaquely and never sees the - * plaintext phrase. Omitted by iOS until iOS implements E2EE parity. + * - encryptedRecoveryPhrase: Argon2id+AES-GCM blob produced by the client using the invite + * code as the KDF input. The server stores it opaquely and never sees the plaintext phrase. * - * When E2EE fields are omitted the function writes nulls; iOS MVP creates - * plaintext couples (encryptionVersion=0 on the resulting couple) and does not - * supply these fields. Android always supplies them. + * Strict E2EE: code, wrappedCoupleKey, kdfSalt, kdfParams, and encryptedRecoveryPhrase are + * all required. There is no plaintext-couple path. * * Response: { code: string, expiresAt: Timestamp } * @@ -115,11 +113,19 @@ exports.createInviteCallable = functions.https.onCall(async (data, context) => { const kdfSalt = data === null || data === void 0 ? void 0 : data.kdfSalt; const kdfParams = data === null || data === void 0 ? void 0 : data.kdfParams; const encryptedRecoveryPhrase = data === null || data === void 0 ? void 0 : data.encryptedRecoveryPhrase; - // E2EE fields must be supplied together or omitted together. - const e2eeFields = [wrappedCoupleKey, kdfSalt, kdfParams]; - const suppliedE2ee = e2eeFields.filter((v) => v != null).length; - if (suppliedE2ee > 0 && suppliedE2ee < e2eeFields.length) { - throw new functions.https.HttpsError('invalid-argument', 'E2EE fields (wrappedCoupleKey, kdfSalt, kdfParams) must all be supplied together or omitted together.'); + // Strict E2EE: every couple must be created with a wrapped couple key. The client-supplied + // code, wrapped key, KDF salt/params, and encrypted recovery phrase are all required. + if (!clientCode) { + throw new functions.https.HttpsError('invalid-argument', 'code is required.'); + } + // Security review Batch 2: validate the code is exactly the 6-char Crockford-style + // alphabet the client generates (CODE_CHARS, no I/O/0/1). Rejects malformed/oversized + // codes and anything that could be abused as the document id. + if (!/^[A-HJ-NP-Z2-9]{6}$/.test(clientCode)) { + throw new functions.https.HttpsError('invalid-argument', 'code must be 6 valid characters.'); + } + if (wrappedCoupleKey == null || kdfSalt == null || kdfParams == null || encryptedRecoveryPhrase == null) { + throw new functions.https.HttpsError('invalid-argument', 'E2EE fields (wrappedCoupleKey, kdfSalt, kdfParams, encryptedRecoveryPhrase) are required.'); } const expiresAt = admin.firestore.Timestamp.fromMillis(now.toMillis() + INVITE_TTL_MS); // Android supplies its own code (used as the KDF input for phrase encryption, so the server diff --git a/functions/dist/couples/createInviteCallable.js.map b/functions/dist/couples/createInviteCallable.js.map index d07e727f..232ca6e0 100644 --- a/functions/dist/couples/createInviteCallable.js.map +++ b/functions/dist/couples/createInviteCallable.js.map @@ -1 +1 @@ -{"version":3,"file":"createInviteCallable.js","sourceRoot":"","sources":["../../src/couples/createInviteCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"createInviteCallable.js","sourceRoot":"","sources":["../../src/couples/createInviteCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/couples/leaveCoupleCallable.js b/functions/dist/couples/leaveCoupleCallable.js index 2a42e9cc..f9d679c4 100644 --- a/functions/dist/couples/leaveCoupleCallable.js +++ b/functions/dist/couples/leaveCoupleCallable.js @@ -51,7 +51,7 @@ const admin = __importStar(require("firebase-admin")); * partner notification, so we don't duplicate that here. */ exports.leaveCoupleCallable = functions.https.onCall(async (_data, context) => { - var _a, _b, _c, _d; + var _a, _b; const callerId = (_a = context.auth) === null || _a === void 0 ? void 0 : _a.uid; if (!callerId) { throw new functions.https.HttpsError('unauthenticated', 'Must be signed in.'); @@ -67,23 +67,41 @@ exports.leaveCoupleCallable = functions.https.onCall(async (_data, context) => { return { success: true }; } const coupleRef = db.collection('couples').doc(coupleId); - const coupleDoc = await coupleRef.get(); - if (!coupleDoc.exists) { - // Couple doc gone β€” just clear caller's field. - await db.collection('users').doc(callerId).update({ coupleId: null }); - return { success: true }; - } - const userIds = ((_d = (_c = coupleDoc.data()) === null || _c === void 0 ? void 0 : _c.userIds) !== null && _d !== void 0 ? _d : []); - if (!userIds.includes(callerId)) { + // Security review Batch 2: do the membership check, member-clearing, and couple-doc + // delete in one transaction so two partners leaving concurrently can't clobber state. + // Critically, only clear a member's coupleId if it STILL points at this couple β€” a + // stale concurrent call must never wipe a coupleId set by a fresh re-pair. + const result = await db.runTransaction(async (tx) => { + var _a, _b, _c; + const coupleSnap = await tx.get(coupleRef); + if (!coupleSnap.exists) { + const callerRef = db.collection('users').doc(callerId); + const callerSnap = await tx.get(callerRef); + if (((_a = callerSnap.data()) === null || _a === void 0 ? void 0 : _a.coupleId) === coupleId) { + tx.update(callerRef, { coupleId: null }); + } + return { membership: true }; + } + const userIds = ((_c = (_b = coupleSnap.data()) === null || _b === void 0 ? void 0 : _b.userIds) !== null && _c !== void 0 ? _c : []); + if (!userIds.includes(callerId)) { + return { membership: false }; + } + // Reads must precede writes in a transaction: snapshot every member first. + const memberSnaps = await Promise.all(userIds.map((uid) => tx.get(db.collection('users').doc(uid)))); + memberSnaps.forEach((snap, i) => { + var _a; + if (((_a = snap.data()) === null || _a === void 0 ? void 0 : _a.coupleId) === coupleId) { + tx.update(db.collection('users').doc(userIds[i]), { coupleId: null }); + } + }); + tx.delete(coupleRef); + return { membership: true }; + }); + if (!result.membership) { throw new functions.https.HttpsError('permission-denied', 'Not a member of this couple.'); } - // Clear coupleId for all members atomically. - const batch = db.batch(); - for (const uid of userIds) { - batch.update(db.collection('users').doc(uid), { coupleId: null }); - } - await batch.commit(); - // Recursively delete the couple document and every subcollection beneath it. + // Couple doc is deleted in the transaction; sweep any subcollections left behind. + // Idempotent if a concurrent caller already removed them. await db.recursiveDelete(coupleRef); console.log(`[leaveCoupleCallable] user ${callerId} left couple ${coupleId}`); return { success: true }; diff --git a/functions/dist/couples/leaveCoupleCallable.js.map b/functions/dist/couples/leaveCoupleCallable.js.map index 4abb6aef..edba7587 100644 --- a/functions/dist/couples/leaveCoupleCallable.js.map +++ b/functions/dist/couples/leaveCoupleCallable.js.map @@ -1 +1 @@ -{"version":3,"file":"leaveCoupleCallable.js","sourceRoot":"","sources":["../../src/couples/leaveCoupleCallable.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AAEvC;;;;;;;;;;;;;GAaG;AACU,QAAA,mBAAmB,GAAG,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;;IACjF,MAAM,QAAQ,GAAG,MAAA,OAAO,CAAC,IAAI,0CAAE,GAAG,CAAA;IAClC,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,CAAA;IAC/E,CAAC;IACD,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;QACjB,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,qBAAqB,EAAE,kCAAkC,CAAC,CAAA;IACjG,CAAC;IAED,MAAM,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAA;IAE5B,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,GAAG,EAAE,CAAA;IAChE,MAAM,QAAQ,GAAG,MAAA,OAAO,CAAC,IAAI,EAAE,0CAAE,QAA8B,CAAA;IAC/D,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,yCAAyC;QACzC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;IAC1B,CAAC;IAED,MAAM,SAAS,GAAG,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAA;IACxD,MAAM,SAAS,GAAG,MAAM,SAAS,CAAC,GAAG,EAAE,CAAA;IAEvC,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QACtB,+CAA+C;QAC/C,MAAM,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;QACrE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;IAC1B,CAAC;IAED,MAAM,OAAO,GAAG,CAAC,MAAA,MAAA,SAAS,CAAC,IAAI,EAAE,0CAAE,OAAO,mCAAI,EAAE,CAAa,CAAA;IAC7D,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;QAChC,MAAM,IAAI,SAAS,CAAC,KAAK,CAAC,UAAU,CAAC,mBAAmB,EAAE,8BAA8B,CAAC,CAAA;IAC3F,CAAC;IAED,6CAA6C;IAC7C,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,EAAE,CAAA;IACxB,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;QAC1B,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;IACnE,CAAC;IACD,MAAM,KAAK,CAAC,MAAM,EAAE,CAAA;IAEpB,6EAA6E;IAC7E,MAAM,EAAE,CAAC,eAAe,CAAC,SAAS,CAAC,CAAA;IAEnC,OAAO,CAAC,GAAG,CAAC,8BAA8B,QAAQ,gBAAgB,QAAQ,EAAE,CAAC,CAAA;IAC7E,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;AAC1B,CAAC,CAAC,CAAA"} \ No newline at end of file +{"version":3,"file":"leaveCoupleCallable.js","sourceRoot":"","sources":["../../src/couples/leaveCoupleCallable.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/dates/createDateMatch.js b/functions/dist/dates/createDateMatch.js index 5b453053..6c6481ee 100644 --- a/functions/dist/dates/createDateMatch.js +++ b/functions/dist/dates/createDateMatch.js @@ -33,59 +33,33 @@ var __importStar = (this && this.__importStar) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -exports.createDateMatchOnMutualLove = void 0; +exports.notifyOnDateMatch = void 0; const functions = __importStar(require("firebase-functions")); const admin = __importStar(require("firebase-admin")); -const LOVE = 'love'; /** - * Creates a revealed date match when both partners have swiped LOVE on the - * same date idea. + * Fires the "It's a match!" notification when a date match is created. * - * Trigger: couples/{coupleId}/date_swipes/{dateIdeaId} (onWrite) + * Trigger: couples/{coupleId}/date_matches/{dateIdeaId} (onCreate) * - * The `date_matches` collection is server-write-only β€” Firestore rules deny all - * client writes (`allow create, update, delete: if false`). This trigger is - * therefore the single source of truth for match creation. The client only - * records swipes and observes `date_matches` for the result. + * Date swipes are E2E-encrypted, so the server can no longer detect mutual love. + * Mutual-match detection now happens client-side (whichever partner records the + * second LOVE writes the match marker, validated by Firestore rules). This trigger + * only sends the push to both partners β€” it never reads swipe content. * - * Idempotency: the match document id is the date idea id and creation runs in a - * transaction, so repeated swipes on the same idea and concurrent invocations - * never produce a duplicate match. + * Idempotency: `fcmNotified` is claimed in a transaction so concurrent invocations + * (or a client retry) never double-send. The match doc id is the date idea id, so + * the marker itself is already de-duplicated by the client transaction + rules. */ -exports.createDateMatchOnMutualLove = functions.firestore - .document('couples/{coupleId}/date_swipes/{dateIdeaId}') - .onWrite(async (change, context) => { - var _a, _b, _c; - const after = change.after.data(); - if (!after) - return; // swipe document was deleted - const actions = ((_a = after.actions) !== null && _a !== void 0 ? _a : {}); - const lovedBy = Object.entries(actions) - .filter(([, entry]) => (entry === null || entry === void 0 ? void 0 : entry.action) === LOVE) - .map(([uid]) => uid) - .sort(); - // A match needs both partners to have loved the same idea. - if (lovedBy.length < 2) +exports.notifyOnDateMatch = functions.firestore + .document('couples/{coupleId}/date_matches/{dateIdeaId}') + .onCreate(async (snap, context) => { + var _a, _b; + if (!snap.exists) return; const { coupleId, dateIdeaId } = context.params; const db = admin.firestore(); - const matchRef = db - .collection('couples') - .doc(coupleId) - .collection('date_matches') - .doc(dateIdeaId); - await db.runTransaction(async (tx) => { - const existing = await tx.get(matchRef); - if (existing.exists) - return; - tx.set(matchRef, { - dateIdeaId, - matchedBy: lovedBy, - revealedAt: admin.firestore.FieldValue.serverTimestamp(), - fcmNotified: false, - }); - }); - // Atomically claim FCM send so concurrent trigger invocations don't double-send. + const matchRef = snap.ref; + // Atomically claim the FCM send so concurrent invocations don't double-send. const shouldSend = await db.runTransaction(async (tx) => { var _a; const doc = await tx.get(matchRef); @@ -94,11 +68,11 @@ exports.createDateMatchOnMutualLove = functions.firestore tx.update(matchRef, { fcmNotified: true }); return true; }); - if (shouldSend) { - const coupleDoc = await db.collection('couples').doc(coupleId).get(); - const userIds = ((_c = (_b = coupleDoc.data()) === null || _b === void 0 ? void 0 : _b.userIds) !== null && _c !== void 0 ? _c : []); - await Promise.all(userIds.map((uid) => notifyDateMatch(db, uid, coupleId, dateIdeaId))); - } + if (!shouldSend) + return; + const coupleDoc = await db.collection('couples').doc(coupleId).get(); + const userIds = ((_b = (_a = coupleDoc.data()) === null || _a === void 0 ? void 0 : _a.userIds) !== null && _b !== void 0 ? _b : []); + await Promise.all(userIds.map((uid) => notifyDateMatch(db, uid, coupleId, dateIdeaId))); }); async function notifyDateMatch(db, userId, coupleId, dateIdeaId) { await db.collection('users').doc(userId).collection('notification_queue').add({ diff --git a/functions/dist/dates/createDateMatch.js.map b/functions/dist/dates/createDateMatch.js.map index 78cfcf0e..05e6ffe4 100644 --- a/functions/dist/dates/createDateMatch.js.map +++ b/functions/dist/dates/createDateMatch.js.map @@ -1 +1 @@ -{"version":3,"file":"createDateMatch.js","sourceRoot":"","sources":["../../src/dates/createDateMatch.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"createDateMatch.js","sourceRoot":"","sources":["../../src/dates/createDateMatch.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/games/onGameSessionUpdate.js b/functions/dist/games/onGameSessionUpdate.js index b4a2ae30..86f6f452 100644 --- a/functions/dist/games/onGameSessionUpdate.js +++ b/functions/dist/games/onGameSessionUpdate.js @@ -45,7 +45,7 @@ const admin = __importStar(require("firebase-admin")); 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, _j, _k, _l, _m, _o, _p; const { coupleId, sessionId } = context.params; const db = admin.firestore(); const messaging = admin.messaging(); @@ -75,47 +75,46 @@ exports.onGameSessionUpdate = functions.firestore 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; // Check if session was just created (status = "active") - const previousData = (_g = change.before.data()) !== null && _g !== void 0 ? _g : {}; - const currentData = (_h = change.after.data()) !== null && _h !== void 0 ? _h : {}; - const wasInactive = ((_j = previousData.status) !== null && _j !== void 0 ? _j : '') !== 'active'; + const previousData = (_j = change.before.data()) !== null && _j !== void 0 ? _j : {}; + const currentData = (_k = change.after.data()) !== null && _k !== void 0 ? _k : {}; + const wasInactive = ((_l = previousData.status) !== null && _l !== void 0 ? _l : '') !== 'active'; const isActiveNow = currentData.status === 'active'; if (wasInactive && isActiveNow) { - // New session started - notify the other partner + // New session started β€” notify the OTHER partner, naming the person who started it. const startedBy = currentData.startedByUserId; - const gameType = (_k = currentData.gameType) !== null && _k !== void 0 ? _k : 'wheel'; - const partnerId = startedBy === partnerA ? partnerB : partnerA; - const partnerName = startedBy === partnerA ? partnerBName : partnerAName; - await notifyPartner(db, messaging, partnerId, partnerName, gameType, 'partner_started_game', `${partnerName} has started a game. Tap to join!`, coupleId); + const gameType = (_m = currentData.gameType) !== null && _m !== void 0 ? _m : 'wheel'; + const recipientId = startedBy === partnerA ? partnerB : partnerA; + const starterName = startedBy === partnerA ? partnerAName : partnerBName; + const starterAvatar = startedBy === partnerA ? avatarA : avatarB; + await notifyPartner(db, messaging, recipientId, starterName, gameType, 'partner_started_game', `${starterName} has started a game. Tap to join!`, coupleId, starterAvatar); return; } // Check if session was completed - const wasActive = ((_l = previousData.status) !== null && _l !== void 0 ? _l : '') === 'active'; + const wasActive = ((_o = previousData.status) !== null && _o !== void 0 ? _o : '') === 'active'; const isCompletedNow = currentData.status === 'completed'; if (wasActive && isCompletedNow) { - const completedBy = currentData.startedByUserId; - const partnerId = completedBy === partnerA ? partnerB : partnerA; - const completingPartnerName = completedBy === partnerA ? partnerAName : partnerBName; - const gt = (_m = currentData.gameType) !== null && _m !== void 0 ? _m : 'wheel'; - const partnerCompletedAt = currentData.partnerCompletedAt; - if (partnerCompletedAt) { - await notifyPartner(db, messaging, partnerA, partnerAName, gt, 'partner_finished_game', `${partnerBName} has finished the game. Tap to see the results!`, coupleId); - await notifyPartner(db, messaging, partnerB, partnerBName, gt, 'partner_finished_game', `${partnerAName} has finished the game. Tap to see the results!`, coupleId); - } - else { - await notifyPartner(db, messaging, partnerId, completingPartnerName, gt, 'partner_finished_game', `${completingPartnerName} has finished. Tap to continue playing!`, coupleId); - } + // The session is complete (both partners have answered) β€” the reveal is ready for each of + // them, so notify BOTH, each naming the OTHER partner. + const gt = (_p = currentData.gameType) !== null && _p !== void 0 ? _p : 'wheel'; + await notifyPartner(db, messaging, partnerA, partnerBName, gt, 'partner_finished_game', `${partnerBName} finished β€” tap to see your results!`, coupleId, avatarB); + await notifyPartner(db, messaging, partnerB, partnerAName, gt, 'partner_finished_game', `${partnerAName} finished β€” tap to see your results!`, coupleId, avatarA); return; } }); /** * Send notification to partner via FCM and write to notification_queue. */ -async function notifyPartner(db, messaging, partnerId, partnerName, gameType, notificationType, body, coupleId) { +async function notifyPartner(db, messaging, partnerId, partnerName, gameType, notificationType, body, coupleId, senderAvatarUrl) { var _a; + const title = notificationType === 'partner_finished_game' + ? `${partnerName} finished the game` + : `${partnerName} is playing`; const notificationPayload = { type: notificationType, - title: `${partnerName} is playing`, + title, body: body, }; // Write an in-app notification record for the partner @@ -155,11 +154,9 @@ async function notifyPartner(db, messaging, partnerId, partnerName, gameType, no title: notificationPayload.title, body: notificationPayload.body, }, - data: { - type: notificationPayload.type, - couple_id: coupleId, - game_type: gameType, - }, + data: Object.assign({ type: notificationPayload.type, couple_id: coupleId, game_type: gameType }, (senderAvatarUrl && senderAvatarUrl.length > 0 + ? { sender_avatar_url: senderAvatarUrl } + : {})), }; const sendResults = await Promise.allSettled(tokens.map((token) => messaging.send(Object.assign(Object.assign({}, fcmMessage), { token })))); const failures = []; diff --git a/functions/dist/games/onGameSessionUpdate.js.map b/functions/dist/games/onGameSessionUpdate.js.map index 3b35de88..ed7cc7ee 100644 --- a/functions/dist/games/onGameSessionUpdate.js.map +++ b/functions/dist/games/onGameSessionUpdate.js.map @@ -1 +1 @@ -{"version":3,"file":"onGameSessionUpdate.js","sourceRoot":"","sources":["../../src/games/onGameSessionUpdate.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"onGameSessionUpdate.js","sourceRoot":"","sources":["../../src/games/onGameSessionUpdate.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/index.js b/functions/dist/index.js index 0441e1dc..c81d628b 100644 --- a/functions/dist/index.js +++ b/functions/dist/index.js @@ -33,8 +33,7 @@ var __importStar = (this && this.__importStar) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -exports.health = exports.onGameSessionUpdate = exports.onUserDelete = exports.scheduledOutcomesReminder = exports.submitOutcomeCallable = exports.createInviteCallable = exports.acceptInviteCallable = exports.leaveCoupleCallable = exports.onCoupleLeave = exports.onMessageWritten = exports.onAnswerWritten = exports.assignDailyQuestionCallable = exports.assignDailyQuestion = exports.createDateMatchOnMutualLove = exports.checkDeviceIntegrity = exports.sendReengagementReminder = exports.sendDailyQuestionProactiveReminder = exports.unlockDueMemoryCapsules = exports.sendChallengeDayReminders = exports.sendGentleReminderCallable = exports.sendPartnerAnsweredNotification = exports.sendDailyQuestionReminder = exports.syncEntitlement = exports.revenueCatWebhook = void 0; -const functions = __importStar(require("firebase-functions")); +exports.onGameSessionUpdate = exports.onUserDelete = exports.scheduledOutcomesReminder = exports.submitOutcomeCallable = exports.createInviteCallable = exports.acceptInviteCallable = exports.leaveCoupleCallable = exports.onCoupleLeave = exports.onMessageWritten = exports.onAnswerWritten = exports.assignDailyQuestionCallable = exports.assignDailyQuestion = exports.notifyOnDateMatch = exports.checkDeviceIntegrity = exports.sendReengagementReminder = exports.sendDailyQuestionProactiveReminder = exports.unlockDueMemoryCapsules = exports.sendChallengeDayReminders = exports.sendGentleReminderCallable = exports.sendPartnerAnsweredNotification = exports.sendDailyQuestionReminder = exports.syncEntitlement = exports.revenueCatWebhook = void 0; const admin = __importStar(require("firebase-admin")); // Initialize the Admin SDK once for every function in this codebase. // Handlers call admin.firestore()/messaging() lazily at invocation time, so a @@ -61,7 +60,7 @@ Object.defineProperty(exports, "sendReengagementReminder", { enumerable: true, g var checkDeviceIntegrity_1 = require("./security/checkDeviceIntegrity"); Object.defineProperty(exports, "checkDeviceIntegrity", { enumerable: true, get: function () { return checkDeviceIntegrity_1.checkDeviceIntegrity; } }); var createDateMatch_1 = require("./dates/createDateMatch"); -Object.defineProperty(exports, "createDateMatchOnMutualLove", { enumerable: true, get: function () { return createDateMatch_1.createDateMatchOnMutualLove; } }); +Object.defineProperty(exports, "notifyOnDateMatch", { enumerable: true, get: function () { return createDateMatch_1.notifyOnDateMatch; } }); var assignDailyQuestion_1 = require("./questions/assignDailyQuestion"); Object.defineProperty(exports, "assignDailyQuestion", { enumerable: true, get: function () { return assignDailyQuestion_1.assignDailyQuestion; } }); Object.defineProperty(exports, "assignDailyQuestionCallable", { enumerable: true, get: function () { return assignDailyQuestion_1.assignDailyQuestionCallable; } }); @@ -85,11 +84,8 @@ var onUserDelete_1 = require("./users/onUserDelete"); Object.defineProperty(exports, "onUserDelete", { enumerable: true, get: function () { return onUserDelete_1.onUserDelete; } }); var onGameSessionUpdate_1 = require("./games/onGameSessionUpdate"); Object.defineProperty(exports, "onGameSessionUpdate", { enumerable: true, get: function () { return onGameSessionUpdate_1.onGameSessionUpdate; } }); -/** - * Basic health check callable. - * Useful for verifying function deployment and firebase-tools wiring. - */ -exports.health = functions.https.onRequest((req, res) => { - res.status(200).json({ status: 'ok' }); -}); +// NOTE (security review Batch 2): the unauthenticated public `health` HTTP endpoint +// was removed to shrink attack surface. Deployment can be verified via +// `firebase functions:list`. If an uptime probe is ever needed, re-add it behind +// auth / a shared secret rather than as an open endpoint. //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/functions/dist/index.js.map b/functions/dist/index.js.map index 6d5d7bbc..3590082e 100644 --- a/functions/dist/index.js.map +++ b/functions/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,8DAA+C;AAC/C,sDAAuC;AAEvC,qEAAqE;AACrE,8EAA8E;AAC9E,gFAAgF;AAChF,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC5B,KAAK,CAAC,aAAa,EAAE,CAAA;AACvB,CAAC;AAED,iEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAC1B,6DAA2D;AAAlD,kHAAA,eAAe,OAAA;AACxB,uDAGkC;AAFhC,sHAAA,yBAAyB,OAAA;AACzB,4HAAA,+BAA+B,OAAA;AAEjC,yFAAuF;AAA9E,wIAAA,0BAA0B,OAAA;AACnC,+DAGsC;AAFpC,0HAAA,yBAAyB,OAAA;AACzB,wHAAA,uBAAuB,OAAA;AAEzB,+EAA0F;AAAjF,2IAAA,kCAAkC,OAAA;AAC3C,6DAAuE;AAA9D,wHAAA,wBAAwB,OAAA;AACjC,wEAAsE;AAA7D,4HAAA,oBAAoB,OAAA;AAC7B,2DAAqE;AAA5D,8HAAA,2BAA2B,OAAA;AACpC,uEAGwC;AAFtC,0HAAA,mBAAmB,OAAA;AACnB,kIAAA,2BAA2B,OAAA;AAE7B,+DAA6D;AAApD,kHAAA,eAAe,OAAA;AACxB,iEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AACzB,yDAAuD;AAA9C,8GAAA,aAAa,OAAA;AACtB,qEAAmE;AAA1D,0HAAA,mBAAmB,OAAA;AAC5B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,yEAAuE;AAA9D,8HAAA,qBAAqB,OAAA;AAC9B,iFAA+E;AAAtE,sIAAA,yBAAyB,OAAA;AAClC,qDAAmD;AAA1C,4GAAA,YAAY,OAAA;AACrB,mEAAiE;AAAxD,0HAAA,mBAAmB,OAAA;AAE5B;;;GAGG;AACU,QAAA,MAAM,GAAG,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC3D,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,CAAA;AACxC,CAAC,CAAC,CAAA"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,sDAAuC;AAEvC,qEAAqE;AACrE,8EAA8E;AAC9E,gFAAgF;AAChF,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;IAC5B,KAAK,CAAC,aAAa,EAAE,CAAA;AACvB,CAAC;AAED,iEAA+D;AAAtD,sHAAA,iBAAiB,OAAA;AAC1B,6DAA2D;AAAlD,kHAAA,eAAe,OAAA;AACxB,uDAGkC;AAFhC,sHAAA,yBAAyB,OAAA;AACzB,4HAAA,+BAA+B,OAAA;AAEjC,yFAAuF;AAA9E,wIAAA,0BAA0B,OAAA;AACnC,+DAGsC;AAFpC,0HAAA,yBAAyB,OAAA;AACzB,wHAAA,uBAAuB,OAAA;AAEzB,+EAA0F;AAAjF,2IAAA,kCAAkC,OAAA;AAC3C,6DAAuE;AAA9D,wHAAA,wBAAwB,OAAA;AACjC,wEAAsE;AAA7D,4HAAA,oBAAoB,OAAA;AAC7B,2DAA2D;AAAlD,oHAAA,iBAAiB,OAAA;AAC1B,uEAGwC;AAFtC,0HAAA,mBAAmB,OAAA;AACnB,kIAAA,2BAA2B,OAAA;AAE7B,+DAA6D;AAApD,kHAAA,eAAe,OAAA;AACxB,iEAA+D;AAAtD,oHAAA,gBAAgB,OAAA;AACzB,yDAAuD;AAA9C,8GAAA,aAAa,OAAA;AACtB,qEAAmE;AAA1D,0HAAA,mBAAmB,OAAA;AAC5B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,uEAAqE;AAA5D,4HAAA,oBAAoB,OAAA;AAC7B,yEAAuE;AAA9D,8HAAA,qBAAqB,OAAA;AAC9B,iFAA+E;AAAtE,sIAAA,yBAAyB,OAAA;AAClC,qDAAmD;AAA1C,4GAAA,YAAY,OAAA;AACrB,mEAAiE;AAAxD,0HAAA,mBAAmB,OAAA;AAE5B,oFAAoF;AACpF,uEAAuE;AACvE,iFAAiF;AACjF,0DAA0D"} \ No newline at end of file diff --git a/functions/dist/questions/assignDailyQuestion.js b/functions/dist/questions/assignDailyQuestion.js index d61ada6a..31b7cd70 100644 --- a/functions/dist/questions/assignDailyQuestion.js +++ b/functions/dist/questions/assignDailyQuestion.js @@ -123,7 +123,18 @@ exports.assignDailyQuestionCallable = functions.https.onCall(async (data, contex if (!userIds.includes(callerId)) { throw new functions.https.HttpsError('permission-denied', 'Caller is not a couple member.'); } - const date = (data === null || data === void 0 ? void 0 : data.date) && typeof data.date === 'string' ? data.date : cstDateString(); + // Security review Batch 2: constrain the client-supplied date. Only today's CST date + // may be assigned on demand β€” this blocks creating arbitrary past/future daily_question + // docs and, combined with create()'s ALREADY_EXISTS guard, caps it to one per day + // (effective rate limit; repeat calls return already-exists). + const today = cstDateString(); + const date = (data === null || data === void 0 ? void 0 : data.date) && typeof data.date === 'string' ? data.date : today; + if (!/^\d{4}-\d{2}-\d{2}$/.test(date)) { + throw new functions.https.HttpsError('invalid-argument', 'date must be YYYY-MM-DD.'); + } + if (date !== today) { + throw new functions.https.HttpsError('invalid-argument', 'Daily question can only be assigned for today.'); + } const questionId = await pickRandomQuestionId(); if (!questionId) { throw new functions.https.HttpsError('internal', 'No active questions available.'); diff --git a/functions/dist/questions/assignDailyQuestion.js.map b/functions/dist/questions/assignDailyQuestion.js.map index 7095d063..f9d22a6d 100644 --- a/functions/dist/questions/assignDailyQuestion.js.map +++ b/functions/dist/questions/assignDailyQuestion.js.map @@ -1 +1 @@ -{"version":3,"file":"assignDailyQuestion.js","sourceRoot":"","sources":["../../src/questions/assignDailyQuestion.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"assignDailyQuestion.js","sourceRoot":"","sources":["../../src/questions/assignDailyQuestion.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/questions/onAnswerWritten.js b/functions/dist/questions/onAnswerWritten.js index 844ca310..ffa416ab 100644 --- a/functions/dist/questions/onAnswerWritten.js +++ b/functions/dist/questions/onAnswerWritten.js @@ -48,7 +48,7 @@ const admin = __importStar(require("firebase-admin")); exports.onAnswerWritten = functions.firestore .document('couples/{coupleId}/daily_question/{date}/answers/{userId}') .onCreate(async (snap, context) => { - var _a, _b, _c, _d; + var _a, _b, _c, _d, _e; const { coupleId, date, userId } = context.params; const db = admin.firestore(); const coupleDoc = await db.collection('couples').doc(coupleId).get(); @@ -57,6 +57,13 @@ exports.onAnswerWritten = functions.firestore return; } const userIds = ((_b = (_a = coupleDoc.data()) === null || _a === void 0 ? void 0 : _a.userIds) !== null && _b !== void 0 ? _b : []); + // Security review Batch 2: re-verify the writer actually belongs to this couple + // before sending a cross-user notification. Firestore rules already enforce this, + // but defense-in-depth ensures a stray/forged answer doc can't trigger a partner ping. + if (!userIds.includes(userId)) { + console.warn(`[onAnswerWritten] writer ${userId} is not a member of couple ${coupleId}`); + return; + } const partnerId = userIds.find((uid) => uid !== userId); if (!partnerId) { console.warn(`[onAnswerWritten] no partner found for couple ${coupleId}`); @@ -96,17 +103,17 @@ exports.onAnswerWritten = functions.firestore } const answerData = snap.data(); const questionId = typeof answerData.questionId === 'string' ? answerData.questionId : ''; + // Sender (the partner who just answered) avatar β€” used as the notification large icon. + const senderDoc = await db.collection('users').doc(userId).get(); + const senderAvatar = (_e = senderDoc.data()) === null || _e === void 0 ? void 0 : _e.photoUrl; const payload = { notification: { title: 'Your partner just answered!', body: "See what they shared for tonight's prompt.", }, - data: { - type: 'partner_answered', - couple_id: coupleId, - question_id: questionId, - date, - }, + data: Object.assign({ type: 'partner_answered', couple_id: coupleId, question_id: questionId, date }, (typeof senderAvatar === 'string' && senderAvatar.length > 0 + ? { sender_avatar_url: senderAvatar } + : {})), }; const sendResults = await Promise.allSettled(tokens.map((token) => admin.messaging().send(Object.assign(Object.assign({}, payload), { token })))); const failures = []; diff --git a/functions/dist/questions/onAnswerWritten.js.map b/functions/dist/questions/onAnswerWritten.js.map index 652ee140..330ddeb5 100644 --- a/functions/dist/questions/onAnswerWritten.js.map +++ b/functions/dist/questions/onAnswerWritten.js.map @@ -1 +1 @@ -{"version":3,"file":"onAnswerWritten.js","sourceRoot":"","sources":["../../src/questions/onAnswerWritten.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"onAnswerWritten.js","sourceRoot":"","sources":["../../src/questions/onAnswerWritten.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/dist/questions/onMessageWritten.js b/functions/dist/questions/onMessageWritten.js index 43d86481..0c9d8a41 100644 --- a/functions/dist/questions/onMessageWritten.js +++ b/functions/dist/questions/onMessageWritten.js @@ -47,7 +47,7 @@ const admin = __importStar(require("firebase-admin")); exports.onMessageWritten = functions.firestore .document('couples/{coupleId}/question_threads/{threadId}/messages/{messageId}') .onCreate(async (snap, context) => { - var _a, _b, _c, _d; + var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; const { coupleId, threadId, messageId } = context.params; const db = admin.firestore(); const messageData = snap.data(); @@ -67,16 +67,23 @@ exports.onMessageWritten = functions.firestore console.warn(`[onMessageWritten] no partner found for couple ${coupleId}`); return; } + // The conversation deep link + the client's "am I already in this thread?" suppression both + // key off questionId, so resolve it from the thread doc and pass it through. + const threadDoc = await db + .collection('couples').doc(coupleId) + .collection('question_threads').doc(threadId) + .get(); + const questionId = (_d = (_c = threadDoc.data()) === null || _c === void 0 ? void 0 : _c.questionId) !== null && _d !== void 0 ? _d : ''; const partnerUserDoc = await db.collection('users').doc(partnerId).get(); // Respect the partner's notification preference (opt-out; default is enabled). - const notifEnabled = (_c = partnerUserDoc.data()) === null || _c === void 0 ? void 0 : _c.notifChatMessage; + const notifEnabled = (_e = partnerUserDoc.data()) === null || _e === void 0 ? void 0 : _e.notifChatMessage; if (notifEnabled === false) { console.log(`[onMessageWritten] partner ${partnerId} has chat notifications off`); return; } const tokens = []; if (partnerUserDoc.exists) { - const legacyToken = (_d = partnerUserDoc.data()) === null || _d === void 0 ? void 0 : _d.fcmToken; + const legacyToken = (_f = partnerUserDoc.data()) === null || _f === void 0 ? void 0 : _f.fcmToken; if (typeof legacyToken === 'string' && legacyToken.length > 0) { tokens.push(legacyToken); } @@ -97,16 +104,17 @@ 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. + const authorDoc = await db.collection('users').doc(authorId).get(); + const authorPhotoUrl = (_h = (_g = authorDoc.data()) === null || _g === void 0 ? void 0 : _g.photoUrl) !== null && _h !== void 0 ? _h : ''; + const authorName = (_k = (_j = authorDoc.data()) === null || _j === void 0 ? void 0 : _j.displayName) !== null && _k !== void 0 ? _k : ''; const payload = { notification: { - title: 'Your partner sent a message', + title: authorName ? `${authorName} sent a message` : 'Your partner sent a message', body: 'Tap to read and reply.', }, - data: { - type: 'chat_message', - couple_id: coupleId, - thread_id: threadId, - }, + data: Object.assign(Object.assign({ type: 'chat_message', couple_id: coupleId, thread_id: threadId }, (questionId ? { question_id: questionId } : {})), (authorPhotoUrl ? { sender_avatar_url: authorPhotoUrl } : {})), }; const sendResults = await Promise.allSettled(tokens.map((token) => admin.messaging().send(Object.assign(Object.assign({}, payload), { token })))); const failures = []; diff --git a/functions/dist/questions/onMessageWritten.js.map b/functions/dist/questions/onMessageWritten.js.map index 8c1f363f..5409e36e 100644 --- a/functions/dist/questions/onMessageWritten.js.map +++ b/functions/dist/questions/onMessageWritten.js.map @@ -1 +1 @@ -{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"onMessageWritten.js","sourceRoot":"","sources":["../../src/questions/onMessageWritten.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/functions/src/questions/onMessageWritten.ts b/functions/src/questions/onMessageWritten.ts index 1d53b319..a8d46fab 100644 --- a/functions/src/questions/onMessageWritten.ts +++ b/functions/src/questions/onMessageWritten.ts @@ -81,9 +81,15 @@ 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. + 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: 'Your partner sent a message', + title: authorName ? `${authorName} sent a message` : 'Your partner sent a message', body: 'Tap to read and reply.', }, data: { @@ -91,6 +97,7 @@ export const onMessageWritten = functions.firestore couple_id: coupleId, thread_id: threadId, ...(questionId ? { question_id: questionId } : {}), + ...(authorPhotoUrl ? { sender_avatar_url: authorPhotoUrl } : {}), }, } diff --git a/storage.rules b/storage.rules index 0d24e27f..0815bd36 100644 --- a/storage.rules +++ b/storage.rules @@ -20,6 +20,16 @@ service firebase.storage { allow read: if request.auth != null && request.auth.uid == uid; } + // Encrypted chat media: the author writes under their own path (already E2E-encrypted + // ciphertext, so Storage never holds anything readable). The partner reads via the tokenized + // download URL, which bypasses these rules β€” same model as profile photos. 15 MB cap. + match /users/{uid}/chat_media/{file} { + allow write: if request.auth != null + && request.auth.uid == uid + && request.resource.size < 15 * 1024 * 1024; + allow read: if request.auth != null && request.auth.uid == uid; + } + // Deny all other paths by default. match /{allPaths=**} { allow read, write: if false;