From 28eb10f6c9d73a3de7d9c24fe3989d7b6266a4ff Mon Sep 17 00:00:00 2001 From: null Date: Tue, 30 Jun 2026 18:13:00 -0500 Subject: [PATCH] Revert "feat(date-memories): add DateMemory/DateReflection domain models and Firestore data sources (batch 1/8)" This reverts commit 18ffdcdbafd42049cc61d367a5902db54d606121. --- .../data/remote/FirestoreCollections.kt | 2 - .../remote/FirestoreDateMemoryDataSource.kt | 74 ----------- .../FirestoreDateReflectionDataSource.kt | 118 ------------------ .../app/closer/domain/model/DateMemory.kt | 18 --- .../app/closer/domain/model/DateReflection.kt | 34 ----- 5 files changed, 246 deletions(-) delete mode 100644 app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt delete mode 100644 app/src/main/java/app/closer/data/remote/FirestoreDateReflectionDataSource.kt delete mode 100644 app/src/main/java/app/closer/domain/model/DateMemory.kt delete mode 100644 app/src/main/java/app/closer/domain/model/DateReflection.kt diff --git a/app/src/main/java/app/closer/data/remote/FirestoreCollections.kt b/app/src/main/java/app/closer/data/remote/FirestoreCollections.kt index 21aaf1e5..3e13ff39 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreCollections.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreCollections.kt @@ -31,8 +31,6 @@ object FirestoreCollections { const val DATE_MATCHES = "date_matches" const val DATE_PLAN_PREFERENCES = "date_plan_preferences" const val DATE_PLANS = "date_plans" - const val DATE_HISTORY = "date_history" - const val DATE_REFLECTIONS = "date_reflections" const val BUCKET_LIST = "bucket_list" const val DAILY_QUESTION = "daily_question" const val CHALLENGES = "challenges" diff --git a/app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt deleted file mode 100644 index 12941762..00000000 --- a/app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt +++ /dev/null @@ -1,74 +0,0 @@ -package app.closer.data.remote - -import app.closer.domain.model.DateMemory -import com.google.firebase.firestore.FirebaseFirestore -import com.google.firebase.firestore.Query -import com.google.firebase.firestore.SetOptions -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.suspendCancellableCoroutine -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -/** - * Completed-date log for the Date Replay timeline: `couples/{coupleId}/date_history/{id}`. - * PLAINTEXT (date-idea title/category + timestamp — coarse metadata, not private words; the private - * reflection lives E2EE in [FirestoreDateReflectionDataSource]). Doc id = source matchId → idempotent. - */ -@Singleton -class FirestoreDateMemoryDataSource @Inject constructor(private val db: FirebaseFirestore) { - - private fun historyRef(coupleId: String) = - db.collection(FirestoreCollections.COUPLES).document(coupleId) - .collection(FirestoreCollections.Couples.DATE_HISTORY) - - private fun toMemory(d: com.google.firebase.firestore.DocumentSnapshot) = DateMemory( - id = d.id, - dateIdeaId = d.getString("dateIdeaId") ?: "", - title = d.getString("title") ?: "", - category = d.getString("category") ?: "", - completedAt = d.getLong("completedAt") ?: 0L, - addedBy = d.getString("addedBy") ?: "" - ) - - /** Idempotent (merge on doc id = matchId — both partners marking the same date = one record). */ - suspend fun markCompleted(coupleId: String, memory: DateMemory): Unit = - suspendCancellableCoroutine { cont -> - historyRef(coupleId).document(memory.id).set( - mapOf( - "dateIdeaId" to memory.dateIdeaId, - "title" to memory.title, - "category" to memory.category, - "completedAt" to memory.completedAt, - "addedBy" to memory.addedBy - ), - SetOptions.merge() - ).addOnSuccessListener { cont.resume(Unit) }.addOnFailureListener { cont.resumeWithException(it) } - } - - fun observeHistory(coupleId: String): Flow> = callbackFlow { - val reg = historyRef(coupleId) - .orderBy("completedAt", Query.Direction.DESCENDING) - .addSnapshotListener { snap, err -> - if (err != null) { close(err); return@addSnapshotListener } - trySend(snap?.documents?.map(::toMemory) ?: emptyList()) - } - awaitClose { reg.remove() } - } - - suspend fun getHistoryOnce(coupleId: String): List = - suspendCancellableCoroutine { cont -> - historyRef(coupleId).orderBy("completedAt", Query.Direction.DESCENDING).get() - .addOnSuccessListener { snap -> cont.resume(snap.documents.map(::toMemory)) } - .addOnFailureListener { cont.resumeWithException(it) } - } - - suspend fun delete(coupleId: String, id: String): Unit = - suspendCancellableCoroutine { cont -> - historyRef(coupleId).document(id).delete() - .addOnSuccessListener { cont.resume(Unit) }.addOnFailureListener { cont.resumeWithException(it) } - } -} diff --git a/app/src/main/java/app/closer/data/remote/FirestoreDateReflectionDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreDateReflectionDataSource.kt deleted file mode 100644 index 85ee8208..00000000 --- a/app/src/main/java/app/closer/data/remote/FirestoreDateReflectionDataSource.kt +++ /dev/null @@ -1,118 +0,0 @@ -package app.closer.data.remote - -import app.closer.crypto.CoupleEncryptionManager -import app.closer.crypto.FieldEncryptor -import app.closer.domain.model.DateReflection -import com.google.firebase.firestore.FirebaseFirestore -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.suspendCancellableCoroutine -import org.json.JSONObject -import javax.inject.Inject -import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -/** - * Post-date reflections — mirrors the daily-question couple-key reveal: - * `couples/{coupleId}/date_reflections/{dateId}/answers/{userId}` (+ read-gated `secure/payload`). - * "Neither partner reads the other until both reflect" is enforced by the Firestore rule, not client code. - */ -@Singleton -class FirestoreDateReflectionDataSource @Inject constructor( - private val db: FirebaseFirestore, - private val encryptionManager: CoupleEncryptionManager, - private val fieldEncryptor: FieldEncryptor -) { - private fun answerRef(coupleId: String, dateId: String, userId: String) = - db.collection(FirestoreCollections.COUPLES).document(coupleId) - .collection(FirestoreCollections.Couples.DATE_REFLECTIONS).document(dateId) - .collection(FirestoreCollections.DailyQuestion.ANSWERS).document(userId) - - private fun securePayloadRef(coupleId: String, dateId: String, userId: String) = - answerRef(coupleId, dateId, userId).collection("secure").document("payload") - - /** Save my reflection: encrypted content in the gated secure subdoc + a content-free metadata doc. */ - suspend fun saveReflection(coupleId: String, dateId: String, userId: String, reflection: DateReflection) { - val aead = encryptionManager.aeadFor(coupleId) - ?: throw IllegalStateException("Couple key unavailable for $coupleId") - val payload = fieldEncryptor.encrypt(encode(reflection), aead, coupleId) - val now = System.currentTimeMillis() - suspendCancellableCoroutine { cont -> - securePayloadRef(coupleId, dateId, userId).set(mapOf("encryptedPayload" to payload)) - .addOnSuccessListener { cont.resume(Unit) }.addOnFailureListener { cont.resumeWithException(it) } - } - suspendCancellableCoroutine { cont -> - answerRef(coupleId, dateId, userId).set( - mapOf( - "userId" to userId, - "schemaVersion" to DateReflection.SCHEMA_VERSION, - "createdAt" to now, - "updatedAt" to now, - "isRevealed" to false - ) - ).addOnSuccessListener { cont.resume(Unit) }.addOnFailureListener { cont.resumeWithException(it) } - } - } - - /** True once a user has reflected (metadata doc readable by both → drives the "your turn" state). */ - suspend fun hasReflected(coupleId: String, dateId: String, userId: String): Boolean = - suspendCancellableCoroutine { cont -> - answerRef(coupleId, dateId, userId).get() - .addOnSuccessListener { cont.resume(it.exists()) } - .addOnFailureListener { cont.resume(false) } - } - - /** Live "has this user reflected?" — lets the reflection screen complete the reveal in real time. */ - fun observeReflected(coupleId: String, dateId: String, userId: String): Flow = callbackFlow { - val reg = answerRef(coupleId, dateId, userId).addSnapshotListener { snap, err -> - if (err != null) return@addSnapshotListener - trySend(snap?.exists() == true) - } - awaitClose { reg.remove() } - } - - /** - * Read + decrypt a user's reflection content from the gated `secure` subdoc. Returns null when the - * read is denied (partner hasn't reflected yet → privacy gate) or the key is unavailable here. - */ - suspend fun decryptReflectionFor(coupleId: String, dateId: String, userId: String): DateReflection? { - val payload = runCatching { - suspendCancellableCoroutine { cont -> - securePayloadRef(coupleId, dateId, userId).get() - .addOnSuccessListener { cont.resume(it.getString("encryptedPayload")) } - .addOnFailureListener { cont.resumeWithException(it) } - } - }.getOrNull() ?: return null - val aead = encryptionManager.aeadFor(coupleId) ?: return null - val json = fieldEncryptor.decrypt(payload, aead, coupleId) ?: return null - return runCatching { - val o = JSONObject(json) - DateReflection( - dateId = dateId, - userId = userId, - favoriteMoment = o.optString("favoriteMoment"), - surprise = o.optString("surprise"), - appreciated = o.optString("appreciated"), - isRevealed = true, - schemaVersion = o.optInt("schemaVersion", DateReflection.SCHEMA_VERSION) - ) - }.getOrNull() - } - - /** Flag my reflection revealed — drives the optional "[partner] opened your reflection" push. */ - suspend fun markRevealed(coupleId: String, dateId: String, userId: String): Unit = - suspendCancellableCoroutine { cont -> - answerRef(coupleId, dateId, userId).update(mapOf("isRevealed" to true)) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resume(Unit) } // best-effort - } - - private fun encode(r: DateReflection): String = JSONObject().apply { - put("favoriteMoment", r.favoriteMoment) - put("surprise", r.surprise) - put("appreciated", r.appreciated) - put("schemaVersion", DateReflection.SCHEMA_VERSION) - }.toString() -} diff --git a/app/src/main/java/app/closer/domain/model/DateMemory.kt b/app/src/main/java/app/closer/domain/model/DateMemory.kt deleted file mode 100644 index 6d39cadc..00000000 --- a/app/src/main/java/app/closer/domain/model/DateMemory.kt +++ /dev/null @@ -1,18 +0,0 @@ -package app.closer.domain.model - -/** - * A completed date the couple logged, for the Date Replay timeline. - * - * Stored at `couples/{coupleId}/date_history/{id}` as PLAINTEXT (app content / coarse metadata — the - * date-idea title + category, not the partners' private words; consistent with the metadata stance). - * The private post-date words live separately in [DateReflection] (E2EE). Doc id = the source matchId so - * "mark done" is idempotent. - */ -data class DateMemory( - val id: String = "", - val dateIdeaId: String = "", - val title: String = "", - val category: String = "", - val completedAt: Long = 0L, - val addedBy: String = "" -) diff --git a/app/src/main/java/app/closer/domain/model/DateReflection.kt b/app/src/main/java/app/closer/domain/model/DateReflection.kt deleted file mode 100644 index 9df63a1d..00000000 --- a/app/src/main/java/app/closer/domain/model/DateReflection.kt +++ /dev/null @@ -1,34 +0,0 @@ -package app.closer.domain.model - -/** - * A partner's private post-date reflection (3 prompts). The content is couple-key encrypted and lives in - * a read-gated `secure/payload` subdoc — neither partner can read the other's until BOTH have reflected - * (enforced by the Firestore rule, mirroring the daily-question reveal). Privacy-native by design. - */ -data class DateReflection( - val dateId: String = "", - val userId: String = "", - val favoriteMoment: String = "", - val surprise: String = "", - val appreciated: String = "", - val isRevealed: Boolean = false, - val createdAt: Long = 0L, - val schemaVersion: Int = SCHEMA_VERSION -) { - val isEmpty: Boolean - get() = favoriteMoment.isBlank() && surprise.isBlank() && appreciated.isBlank() - - companion object { - const val SCHEMA_VERSION = 1 - } -} - -/** Per-user reflection progress for a completed date, derived at read time (not stored). */ -enum class DateReflectionState { - /** Neither this user has reflected. */ - NONE, - /** This user reflected; waiting on the partner. */ - AWAITING_PARTNER, - /** Both reflected — ready to reveal / revealed. */ - BOTH_DONE -}