From fcc0d699fb3a857d075f256f0df4aabb6e624ac5 Mon Sep 17 00:00:00 2001 From: null Date: Mon, 6 Jul 2026 22:00:55 -0500 Subject: [PATCH] =?UTF-8?q?refactor(data):=20finish=20Firebase=20Tasks?= =?UTF-8?q?=E2=86=92.await()=20across=20the=20data=20layer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert the remaining 10 data sources (Bucket, Conversation, QuestionThread, DatePlan, DateMemory, DateReflection, Answer, User, Auth, DateMatch) from verbose suspendCancellableCoroutine{cont-> ...addOnSuccess/addOnFailure} wrappers (and the per-file getDoc/voidAwait/queryAwait/refAwait helper duplicates) to the kotlinx-coroutines-play-services .await() extension. The data layer now has ZERO Task-callback wrappers. Per-block review preserved non-trivial semantics: callbackFlow snapshot listeners and the DateMatch transaction are untouched; failure→null/false best-effort blocks became runCatching{ ...await() }.getOrNull()/getOrDefault() (e.g. markRevealed, hasProfile, PlayIntegrity.verifyWithServer); no-signed-in-user guards became `?: throw`. Verified live (real Firebase, paired account): auth + Home reads (couple/user/ answer/challenge), daily-question answer WRITE (secure payload + metadata sets), and the streak update all succeed — no crash, no PERMISSION_DENIED. Unit suite green. Co-Authored-By: Claude Fable 5 --- .../data/remote/FirebaseAuthDataSource.kt | 122 +++++---------- .../data/remote/FirestoreAnswerDataSource.kt | 127 +++++----------- .../remote/FirestoreBucketListDataSource.kt | 40 +---- .../remote/FirestoreConversationDataSource.kt | 43 ++---- .../remote/FirestoreDateMatchDataSource.kt | 14 +- .../remote/FirestoreDateMemoryDataSource.kt | 44 +++--- .../remote/FirestoreDatePlanDataSource.kt | 40 +---- .../FirestoreDateReflectionDataSource.kt | 37 ++--- .../FirestoreQuestionThreadDataSource.kt | 48 ++---- .../data/remote/FirestoreUserDataSource.kt | 143 +++++++----------- 10 files changed, 196 insertions(+), 462 deletions(-) diff --git a/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt b/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt index 88374ce0..997e9958 100644 --- a/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirebaseAuthDataSource.kt @@ -16,11 +16,9 @@ import com.google.firebase.auth.GoogleAuthProvider import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.tasks.await import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException @Singleton class FirebaseAuthDataSource @Inject constructor() { @@ -57,43 +55,21 @@ class FirebaseAuthDataSource @Inject constructor() { ?: AuthState.Unauthenticated suspend fun signInWithEmail(email: String, password: String): String = - suspendCancellableCoroutine { cont -> - auth.signInWithEmailAndPassword(email, password) - .addOnSuccessListener { cont.resume(it.user?.uid ?: "") } - .addOnFailureListener { cont.resumeWithException(it) } - } + auth.signInWithEmailAndPassword(email, password).await().user?.uid ?: "" suspend fun signUpWithEmail(email: String, password: String): String = - suspendCancellableCoroutine { cont -> - auth.createUserWithEmailAndPassword(email, password) - .addOnSuccessListener { cont.resume(it.user?.uid ?: "") } - .addOnFailureListener { cont.resumeWithException(it) } - } + auth.createUserWithEmailAndPassword(email, password).await().user?.uid ?: "" - suspend fun sendEmailVerification(): Unit = - suspendCancellableCoroutine { cont -> - val user = auth.currentUser - if (user == null) { - cont.resumeWithException(IllegalStateException("No signed-in user")) - return@suspendCancellableCoroutine - } - user.sendEmailVerification() - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun sendEmailVerification() { + val user = auth.currentUser ?: throw IllegalStateException("No signed-in user") + user.sendEmailVerification().await() + } /** Refreshes the cached FirebaseUser so [isEmailVerified] reflects server state. */ - suspend fun reloadUser(): Unit = - suspendCancellableCoroutine { cont -> - val user = auth.currentUser - if (user == null) { - cont.resumeWithException(IllegalStateException("No signed-in user")) - return@suspendCancellableCoroutine - } - user.reload() - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun reloadUser() { + val user = auth.currentUser ?: throw IllegalStateException("No signed-in user") + user.reload().await() + } /** * Sends a password-reset email, translating Firebase specifics into typed [PasswordResetException]s @@ -116,12 +92,9 @@ class FirebaseAuthDataSource @Inject constructor() { } } - private suspend fun sendResetEmail(email: String): Unit = - suspendCancellableCoroutine { cont -> - auth.sendPasswordResetEmail(email) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + private suspend fun sendResetEmail(email: String) { + auth.sendPasswordResetEmail(email).await() + } /** * Changes the signed-in user's password: re-authenticates with [currentPassword] (Firebase requires @@ -162,58 +135,39 @@ class FirebaseAuthDataSource @Inject constructor() { private suspend fun reauthenticate( user: com.google.firebase.auth.FirebaseUser, credential: com.google.firebase.auth.AuthCredential - ): Unit = suspendCancellableCoroutine { cont -> - user.reauthenticate(credential) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } + ) { + user.reauthenticate(credential).await() } private suspend fun updatePassword( user: com.google.firebase.auth.FirebaseUser, newPassword: String - ): Unit = suspendCancellableCoroutine { cont -> - user.updatePassword(newPassword) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } + ) { + user.updatePassword(newPassword).await() } - suspend fun signInWithGoogle(idToken: String): GoogleSignInResult = - suspendCancellableCoroutine { cont -> - val credential = GoogleAuthProvider.getCredential(idToken, null) - auth.signInWithCredential(credential) - .addOnSuccessListener { result -> - val user = auth.currentUser ?: result.user - cont.resume( - GoogleSignInResult( - uid = user?.uid ?: "", - displayName = user?.displayName ?: "", - photoUrl = user?.photoUrl?.toString() ?: "", - email = user?.email ?: "", - isAnonymous = user?.isAnonymous ?: false - ) - ) - } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun signInWithGoogle(idToken: String): GoogleSignInResult { + val credential = GoogleAuthProvider.getCredential(idToken, null) + val result = auth.signInWithCredential(credential).await() + val user = auth.currentUser ?: result.user + return GoogleSignInResult( + uid = user?.uid ?: "", + displayName = user?.displayName ?: "", + photoUrl = user?.photoUrl?.toString() ?: "", + email = user?.email ?: "", + isAnonymous = user?.isAnonymous ?: false + ) + } fun signOut() = auth.signOut() - suspend fun reauthenticateWithEmail(email: String, password: String): Unit = - suspendCancellableCoroutine { cont -> - val credential = EmailAuthProvider.getCredential(email, password) - auth.currentUser - ?.reauthenticate(credential) - ?.addOnSuccessListener { cont.resume(Unit) } - ?.addOnFailureListener { cont.resumeWithException(it) } - ?: cont.resumeWithException(IllegalStateException("No signed-in user")) - } + suspend fun reauthenticateWithEmail(email: String, password: String) { + val user = auth.currentUser ?: throw IllegalStateException("No signed-in user") + user.reauthenticate(EmailAuthProvider.getCredential(email, password)).await() + } - suspend fun deleteAccount(): Unit = - suspendCancellableCoroutine { cont -> - auth.currentUser - ?.delete() - ?.addOnSuccessListener { cont.resume(Unit) } - ?.addOnFailureListener { cont.resumeWithException(it) } - ?: cont.resumeWithException(IllegalStateException("No signed-in user")) - } + suspend fun deleteAccount() { + val user = auth.currentUser ?: throw IllegalStateException("No signed-in user") + user.delete().await() + } } diff --git a/app/src/main/java/app/closer/data/remote/FirestoreAnswerDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreAnswerDataSource.kt index a8ac7d8e..197fdefb 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreAnswerDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreAnswerDataSource.kt @@ -11,7 +11,7 @@ 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 kotlinx.coroutines.tasks.await import org.json.JSONArray import org.json.JSONObject import java.time.Clock @@ -21,8 +21,6 @@ import java.time.ZoneId import java.time.format.DateTimeFormatter import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException /** * Syncs daily-question answers to Firestore under the couple's daily question doc. @@ -103,18 +101,9 @@ class FirestoreAnswerDataSource @Inject constructor( "isRevealed" to false ) // Encrypted content lives in the read-gated `secure` subdoc (no peek before both answer). - suspendCancellableCoroutine { cont -> - securePayloadRef(coupleId, date, userId) - .set(mapOf("encryptedPayload" to encryptedPayload)) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } - suspendCancellableCoroutine { cont -> - answerRef(coupleId, date, userId) - .set(metadata) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + securePayloadRef(coupleId, date, userId) + .set(mapOf("encryptedPayload" to encryptedPayload)).await() + answerRef(coupleId, date, userId).set(metadata).await() } private fun securePayloadRef(coupleId: String, date: String, userId: String) = @@ -123,24 +112,17 @@ class FirestoreAnswerDataSource @Inject constructor( /** Reads + decrypts a partner's couple-key answer content from the read-gated `secure` subdoc. */ suspend fun decryptCoupleKeyAnswerFor(coupleId: String, date: String, userId: String): DecodedAnswer? { val encryptedPayload = runCatching { - suspendCancellableCoroutine { cont -> - securePayloadRef(coupleId, date, userId) - .get() - .addOnSuccessListener { snap -> cont.resume(snap.getString("encryptedPayload")) } - .addOnFailureListener { cont.resumeWithException(it) } - } + securePayloadRef(coupleId, date, userId).get().await().getString("encryptedPayload") }.getOrNull() return decryptCoupleKeyAnswer(coupleId, encryptedPayload) } /** Marks the caller's own answer revealed in Firestore — drives the partner-opened push. */ - suspend fun markRevealed(coupleId: String, date: String, userId: String): Unit = - suspendCancellableCoroutine { cont -> - answerRef(coupleId, date, userId) - .update(mapOf("isRevealed" to true, "updatedAt" to System.currentTimeMillis())) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun markRevealed(coupleId: String, date: String, userId: String) { + answerRef(coupleId, date, userId) + .update(mapOf("isRevealed" to true, "updatedAt" to System.currentTimeMillis())) + .await() + } /** Decrypts a couple-key (enc:v1:) answer payload. Returns null if the key/payload is unavailable. */ fun decryptCoupleKeyAnswer(coupleId: String, encryptedPayload: String?): DecodedAnswer? { @@ -202,25 +184,18 @@ class FirestoreAnswerDataSource @Inject constructor( "isRevealed" to answer.isRevealed ) - suspendCancellableCoroutine { cont -> - answerRef(coupleId, date, userId) - .set(data) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + answerRef(coupleId, date, userId).set(data).await() } /** Updates the answerKeyReleased flag after the one-time key is sent to the partner. */ - suspend fun markAnswerKeyReleased(coupleId: String, date: String, userId: String): Unit = - suspendCancellableCoroutine { cont -> - answerRef(coupleId, date, userId) - // updatedAt is stored as epoch millis everywhere else; a serverTimestamp here made - // it a Firestore Timestamp, which crashed toLocalAnswer's getLong("updatedAt") when - // the partner read this answer during reveal. Keep it a Long. - .update(mapOf("answerKeyReleased" to true, "updatedAt" to System.currentTimeMillis())) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun markAnswerKeyReleased(coupleId: String, date: String, userId: String) { + answerRef(coupleId, date, userId) + // updatedAt is stored as epoch millis everywhere else; a serverTimestamp here made + // it a Firestore Timestamp, which crashed toLocalAnswer's getLong("updatedAt") when + // the partner read this answer during reveal. Keep it a Long. + .update(mapOf("answerKeyReleased" to true, "updatedAt" to System.currentTimeMillis())) + .await() + } /** * Fetches a partner's answer for the current local date. @@ -229,17 +204,9 @@ class FirestoreAnswerDataSource @Inject constructor( coupleId: String, userId: String, date: String = todayLocalDateString() - ): LocalAnswer? = suspendCancellableCoroutine { cont -> - answerRef(coupleId, date, userId) - .get() - .addOnSuccessListener { snap -> - if (!snap.exists()) { - cont.resume(null) - return@addOnSuccessListener - } - cont.resume(snap.toLocalAnswer()) - } - .addOnFailureListener { cont.resumeWithException(it) } + ): LocalAnswer? { + val snap = answerRef(coupleId, date, userId).get().await() + return if (snap.exists()) snap.toLocalAnswer() else null } /** @@ -263,40 +230,27 @@ class FirestoreAnswerDataSource @Inject constructor( /** * Reads the couple-scoped daily question assignment for today. */ - suspend fun getDailyQuestionAssignment(coupleId: String, date: String = todayLocalDateString()): DailyQuestionAssignment? = - suspendCancellableCoroutine { cont -> - dailyQuestionRef(coupleId, date) - .get() - .addOnSuccessListener { snap -> - if (!snap.exists()) { - cont.resume(null) - return@addOnSuccessListener - } - cont.resume( - DailyQuestionAssignment( - questionId = snap.getString("questionId") ?: "", - date = snap.getString("date") ?: date, - assignedAt = snap.getTimestamp("assignedAt"), - expiresAt = snap.getTimestamp("expiresAt") - ) - ) - } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun getDailyQuestionAssignment(coupleId: String, date: String = todayLocalDateString()): DailyQuestionAssignment? { + val snap = dailyQuestionRef(coupleId, date).get().await() + if (!snap.exists()) return null + return DailyQuestionAssignment( + questionId = snap.getString("questionId") ?: "", + date = snap.getString("date") ?: date, + assignedAt = snap.getTimestamp("assignedAt"), + expiresAt = snap.getTimestamp("expiresAt") + ) + } /** * Calls the cloud function to assign a daily question for the couple immediately. * Used when a couple is newly created and has no assignment yet. */ - suspend fun requestDailyQuestionAssignment(coupleId: String, date: String = todayLocalDateString()): Unit = - suspendCancellableCoroutine { cont -> - val functions = com.google.firebase.functions.FirebaseFunctions.getInstance() - functions - .getHttpsCallable("assignDailyQuestionCallable") - .call(mapOf("coupleId" to coupleId, "date" to date)) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun requestDailyQuestionAssignment(coupleId: String, date: String = todayLocalDateString()) { + com.google.firebase.functions.FirebaseFunctions.getInstance() + .getHttpsCallable("assignDailyQuestionCallable") + .call(mapOf("coupleId" to coupleId, "date" to date)) + .await() + } private fun com.google.firebase.firestore.DocumentSnapshot.toLocalAnswer(): LocalAnswer { // All answers are sealed (schemaVersion 3): content lives in encryptedPayload and is @@ -341,7 +295,7 @@ class FirestoreAnswerDataSource @Inject constructor( partnerAnswer: String?, modeTag: String?, date: String - ): Unit = suspendCancellableCoroutine { cont -> + ) { val aead = encryptionManager.requireAead(coupleId) val doc = mapOf( "questionId" to questionId, @@ -358,8 +312,7 @@ class FirestoreAnswerDataSource @Inject constructor( .collection("lore") .document(questionId) .set(doc) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } + .await() } private suspend fun ensureUserPublicKeyPublished(userId: String) { diff --git a/app/src/main/java/app/closer/data/remote/FirestoreBucketListDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreBucketListDataSource.kt index c8a4e7ac..e1500063 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreBucketListDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreBucketListDataSource.kt @@ -8,11 +8,9 @@ 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 kotlinx.coroutines.tasks.await import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException /** * Firestore data source for couple bucket lists. @@ -50,7 +48,7 @@ class FirestoreBucketListDataSource @Inject constructor( "completedAt" to item.completedAt, "isCompleted" to item.isCompleted ) - doc.set(data, SetOptions.merge()).voidAwait() + doc.set(data, SetOptions.merge()).await() return doc.id } @@ -65,11 +63,11 @@ class FirestoreBucketListDataSource @Inject constructor( "completedAt" to item.completedAt, "isCompleted" to item.isCompleted ) - path.set(data, SetOptions.merge()).voidAwait() + path.set(data, SetOptions.merge()).await() } suspend fun getItem(coupleId: String, itemId: String): BucketListItem? { - val snap = itemsRef(coupleId).document(itemId).getDoc() + val snap = itemsRef(coupleId).document(itemId).get().await() return snap.toBucketListItem(coupleId) } @@ -86,12 +84,12 @@ class FirestoreBucketListDataSource @Inject constructor( } suspend fun getItems(coupleId: String): List { - val snap = itemsRef(coupleId).queryAwait() + val snap = itemsRef(coupleId).get().await() return snap.documents.mapNotNull { it.toBucketListItem(coupleId) } } suspend fun deleteItem(coupleId: String, itemId: String) { - itemsRef(coupleId).document(itemId).delete().voidAwait() + itemsRef(coupleId).document(itemId).delete().await() } suspend fun completeItem(coupleId: String, itemId: String, completedBy: String) { @@ -101,7 +99,7 @@ class FirestoreBucketListDataSource @Inject constructor( "completedAt" to System.currentTimeMillis(), "isCompleted" to true ) - path.set(data, SetOptions.merge()).voidAwait() + path.set(data, SetOptions.merge()).await() } // ─── Category queries ──────────────────────────────────────────────────── @@ -123,32 +121,10 @@ class FirestoreBucketListDataSource @Inject constructor( val snap = itemsRef(coupleId) .whereEqualTo("category", category) .orderBy("addedAt", com.google.firebase.firestore.Query.Direction.DESCENDING) - .queryAwait() + .get().await() return snap.documents.mapNotNull { it.toBucketListItem(coupleId) } } - // ─── Coroutine helpers ─────────────────────────────────────────────────── - - private suspend fun com.google.firebase.firestore.DocumentReference.getDoc() = - suspendCancellableCoroutine { cont -> - get() - .addOnSuccessListener { cont.resume(it) } - .addOnFailureListener { cont.resumeWithException(it) } - } - - private suspend fun com.google.firebase.firestore.Query.queryAwait() = - suspendCancellableCoroutine { cont -> - get() - .addOnSuccessListener { cont.resume(it) } - .addOnFailureListener { cont.resumeWithException(it) } - } - - private suspend fun com.google.android.gms.tasks.Task.voidAwait() = - suspendCancellableCoroutine { cont -> - addOnSuccessListener { cont.resume(Unit) } - addOnFailureListener { cont.resumeWithException(it) } - } - // ─── Mapper ────────────────────────────────────────────────────────────── @Suppress("UNCHECKED_CAST") diff --git a/app/src/main/java/app/closer/data/remote/FirestoreConversationDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreConversationDataSource.kt index 686aefb9..64e5f4ae 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreConversationDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreConversationDataSource.kt @@ -13,11 +13,9 @@ 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 kotlinx.coroutines.tasks.await import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException /** * Backs the Messages inbox + each conversation's chat. Every conversation lives under @@ -65,7 +63,7 @@ class FirestoreConversationDataSource @Inject constructor( "createdAt" to FieldValue.serverTimestamp() ), SetOptions.merge() - ).voidAwait() + ).await() } /** Creates the per-question conversation if needed and returns its id. */ @@ -80,7 +78,7 @@ class FirestoreConversationDataSource @Inject constructor( "createdAt" to FieldValue.serverTimestamp() ), SetOptions.merge() - ).voidAwait() + ).await() } return convId } @@ -88,7 +86,7 @@ class FirestoreConversationDataSource @Inject constructor( suspend fun markRead(coupleId: String, conversationId: String, userId: String) { conversationsRef(coupleId).document(conversationId) .set(mapOf("reads" to mapOf(userId to FieldValue.serverTimestamp())), SetOptions.merge()) - .voidAwait() + .await() } // ─── Messages ────────────────────────────────────────────────────────────────── @@ -103,7 +101,7 @@ class FirestoreConversationDataSource @Inject constructor( "text" to cipher, "createdAt" to FieldValue.serverTimestamp() ) - ).refAwait() + ).await() updateLastMessage(coupleId, conversationId, userId, cipher) } @@ -118,7 +116,7 @@ class FirestoreConversationDataSource @Inject constructor( "mediaUrl" to url, "createdAt" to FieldValue.serverTimestamp() ) - ).refAwait() + ).await() // Preview a photo with a fixed (still encrypted) label so the inbox decrypt path is uniform. updateLastMessage(coupleId, conversationId, userId, fieldEncryptor.encrypt("📷 Photo", aead, coupleId)) } @@ -135,7 +133,7 @@ class FirestoreConversationDataSource @Inject constructor( "durationMs" to durationMs, "createdAt" to FieldValue.serverTimestamp() ) - ).refAwait() + ).await() updateLastMessage(coupleId, conversationId, userId, fieldEncryptor.encrypt("🎤 Voice message", aead, coupleId)) } @@ -147,7 +145,7 @@ class FirestoreConversationDataSource @Inject constructor( "lastMessageSenderId" to userId ), SetOptions.merge() - ).voidAwait() + ).await() } /** @@ -171,15 +169,15 @@ class FirestoreConversationDataSource @Inject constructor( suspend fun setReaction(coupleId: String, conversationId: String, messageId: String, userId: String, emoji: String?) { val ref = messagesRef(coupleId, conversationId).document(messageId) if (emoji == null) { - ref.update("reactions.$userId", FieldValue.delete()).voidAwait() + ref.update("reactions.$userId", FieldValue.delete()).await() } else { - ref.update(mapOf("reactions.$userId" to emoji)).voidAwait() + ref.update(mapOf("reactions.$userId" to emoji)).await() } } /** Unsend: tombstones a message (author-only) and reflects it in the inbox preview if it was last. */ suspend fun deleteMessage(coupleId: String, conversationId: String, messageId: String) { - messagesRef(coupleId, conversationId).document(messageId).update("deleted", true).voidAwait() + messagesRef(coupleId, conversationId).document(messageId).update("deleted", true).await() val latest = runCatching { messagesRef(coupleId, conversationId) .orderBy("createdAt", Query.Direction.DESCENDING).limit(1).get().await() @@ -189,7 +187,7 @@ class FirestoreConversationDataSource @Inject constructor( conversationsRef(coupleId).document(conversationId).set( mapOf("lastMessagePreview" to fieldEncryptor.encrypt("Message deleted", aead, coupleId)), SetOptions.merge() - ).voidAwait() + ).await() } } @@ -330,21 +328,4 @@ class FirestoreConversationDataSource @Inject constructor( ) } - private suspend fun com.google.android.gms.tasks.Task.await(): T = - suspendCancellableCoroutine { cont -> - addOnSuccessListener { cont.resume(it) } - addOnFailureListener { cont.resumeWithException(it) } - } - - private suspend fun com.google.android.gms.tasks.Task.voidAwait() = - suspendCancellableCoroutine { cont -> - addOnSuccessListener { cont.resume(Unit) } - addOnFailureListener { cont.resumeWithException(it) } - } - - private suspend fun com.google.android.gms.tasks.Task.refAwait() = - suspendCancellableCoroutine { cont -> - addOnSuccessListener { cont.resume(Unit) } - addOnFailureListener { cont.resumeWithException(it) } - } } diff --git a/app/src/main/java/app/closer/data/remote/FirestoreDateMatchDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreDateMatchDataSource.kt index d122cce7..a09709aa 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreDateMatchDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreDateMatchDataSource.kt @@ -8,12 +8,9 @@ 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 kotlinx.coroutines.tasks.await import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException /** * Firestore data source for revealed mutual date matches. @@ -67,7 +64,7 @@ class FirestoreDateMatchDataSource @Inject constructor(private val db: FirebaseF val snap = matchesRef(coupleId) .whereEqualTo("dateIdeaId", dateIdeaId) .limit(1) - .queryAwait() + .get().await() return snap.documents.firstOrNull()?.toDateMatch(coupleId) } @@ -81,15 +78,6 @@ class FirestoreDateMatchDataSource @Inject constructor(private val db: FirebaseF awaitClose { listener.remove() } } - // ─── Coroutine helpers ─────────────────────────────────────────────────── - - private suspend fun com.google.firebase.firestore.Query.queryAwait() = - suspendCancellableCoroutine { cont -> - get() - .addOnSuccessListener { cont.resume(it) } - .addOnFailureListener { cont.resumeWithException(it) } - } - // ─── Mapper ────────────────────────────────────────────────────────────── @Suppress("UNCHECKED_CAST") diff --git a/app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt index 12941762..72e31f41 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreDateMemoryDataSource.kt @@ -7,11 +7,9 @@ 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 kotlinx.coroutines.tasks.await 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}`. @@ -35,19 +33,18 @@ class FirestoreDateMemoryDataSource @Inject constructor(private val db: Firebase ) /** 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) } - } + suspend fun markCompleted(coupleId: String, memory: DateMemory) { + 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() + ).await() + } fun observeHistory(coupleId: String): Flow> = callbackFlow { val reg = historyRef(coupleId) @@ -60,15 +57,10 @@ class FirestoreDateMemoryDataSource @Inject constructor(private val db: Firebase } 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) } - } + historyRef(coupleId).orderBy("completedAt", Query.Direction.DESCENDING).get().await() + .documents.map(::toMemory) - suspend fun delete(coupleId: String, id: String): Unit = - suspendCancellableCoroutine { cont -> - historyRef(coupleId).document(id).delete() - .addOnSuccessListener { cont.resume(Unit) }.addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun delete(coupleId: String, id: String) { + historyRef(coupleId).document(id).delete().await() + } } diff --git a/app/src/main/java/app/closer/data/remote/FirestoreDatePlanDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreDatePlanDataSource.kt index 4aec9ab2..92fdf538 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreDatePlanDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreDatePlanDataSource.kt @@ -11,12 +11,10 @@ import org.json.JSONArray import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.tasks.await import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.suspendCancellableCoroutine import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException /** * Firestore data source for date plan preferences and plans. @@ -57,7 +55,7 @@ class FirestoreDatePlanDataSource @Inject constructor( "createdAt" to preference.createdAt, "updatedAt" to preference.updatedAt ) - path.set(data, SetOptions.merge()).voidAwait() + path.set(data, SetOptions.merge()).await() } suspend fun getPreference(coupleId: String, dateIdeaId: String): DatePlanPreference? { @@ -65,7 +63,7 @@ class FirestoreDatePlanDataSource @Inject constructor( val snap = preferencesRef(coupleId) .whereEqualTo("dateIdeaId", dateIdeaId) .limit(1) - .queryAwait() + .get().await() return snap.documents.firstOrNull()?.toDatePlanPreference(coupleId) } @@ -84,13 +82,13 @@ class FirestoreDatePlanDataSource @Inject constructor( suspend fun createPlan(coupleId: String, plan: DatePlan): String { val doc = plansRef(coupleId).document() - doc.set(plan.toEncryptedData(coupleId, includeCreatedAt = true), SetOptions.merge()).voidAwait() + doc.set(plan.toEncryptedData(coupleId, includeCreatedAt = true), SetOptions.merge()).await() return doc.id } suspend fun updatePlan(coupleId: String, plan: DatePlan) { val path = plansRef(coupleId).document(plan.id) - path.set(plan.toEncryptedData(coupleId, includeCreatedAt = false), SetOptions.merge()).voidAwait() + path.set(plan.toEncryptedData(coupleId, includeCreatedAt = false), SetOptions.merge()).await() } /** @@ -117,7 +115,7 @@ class FirestoreDatePlanDataSource @Inject constructor( } suspend fun getPlan(coupleId: String, planId: String): DatePlan? { - val snap = plansRef(coupleId).document(planId).getDoc() + val snap = plansRef(coupleId).document(planId).get().await() return snap.toDatePlan(coupleId) } @@ -137,36 +135,14 @@ class FirestoreDatePlanDataSource @Inject constructor( val snap = plansRef(coupleId) .whereEqualTo("dateIdeaId", dateIdeaId) .limit(1) - .queryAwait() + .get().await() return snap.documents.firstOrNull()?.toDatePlan(coupleId) } suspend fun deletePlan(coupleId: String, planId: String) { - plansRef(coupleId).document(planId).delete().voidAwait() + plansRef(coupleId).document(planId).delete().await() } - // ─── Coroutine helpers ─────────────────────────────────────────────────── - - private suspend fun com.google.firebase.firestore.Query.queryAwait() = - suspendCancellableCoroutine { cont -> - get() - .addOnSuccessListener { cont.resume(it) } - .addOnFailureListener { cont.resumeWithException(it) } - } - - private suspend fun com.google.android.gms.tasks.Task.voidAwait() = - suspendCancellableCoroutine { cont -> - addOnSuccessListener { cont.resume(Unit) } - addOnFailureListener { cont.resumeWithException(it) } - } - - private suspend fun com.google.firebase.firestore.DocumentReference.getDoc() = - suspendCancellableCoroutine { cont -> - get() - .addOnSuccessListener { cont.resume(it) } - .addOnFailureListener { cont.resumeWithException(it) } - } - // ─── Mappers ───────────────────────────────────────────────────────────── private fun com.google.firebase.firestore.DocumentSnapshot.toDatePlanPreference(coupleId: String): DatePlanPreference? { diff --git a/app/src/main/java/app/closer/data/remote/FirestoreDateReflectionDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreDateReflectionDataSource.kt index bec32e4b..ec8673ea 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreDateReflectionDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreDateReflectionDataSource.kt @@ -7,12 +7,10 @@ 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 kotlinx.coroutines.tasks.await 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: @@ -55,11 +53,7 @@ class FirestoreDateReflectionDataSource @Inject constructor( "isRevealed" to false ) ) - suspendCancellableCoroutine { cont -> - batch.commit() - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + batch.commit().await() } /** @@ -71,10 +65,7 @@ class FirestoreDateReflectionDataSource @Inject constructor( val aead = encryptionManager.aeadFor(coupleId) ?: throw IllegalStateException("Couple key unavailable for $coupleId") val payload = fieldEncryptor.encrypt(encode(reflection), aead, coupleId) - suspendCancellableCoroutine { cont -> - securePayloadRef(coupleId, dateId, userId).set(mapOf("encryptedPayload" to payload)) - .addOnSuccessListener { cont.resume(Unit) }.addOnFailureListener { cont.resumeWithException(it) } - } + securePayloadRef(coupleId, dateId, userId).set(mapOf("encryptedPayload" to payload)).await() } /** @@ -85,11 +76,7 @@ class FirestoreDateReflectionDataSource @Inject constructor( * Best-effort callers can still `runCatching { … }.getOrDefault(false)`. */ suspend fun hasReflected(coupleId: String, dateId: String, userId: String): Boolean = - suspendCancellableCoroutine { cont -> - answerRef(coupleId, dateId, userId).get() - .addOnSuccessListener { cont.resume(it.exists()) } - .addOnFailureListener { cont.resumeWithException(it) } - } + answerRef(coupleId, dateId, userId).get().await().exists() /** 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 { @@ -106,11 +93,7 @@ class FirestoreDateReflectionDataSource @Inject constructor( */ 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) } - } + securePayloadRef(coupleId, dateId, userId).get().await().getString("encryptedPayload") }.getOrNull() ?: return null val aead = encryptionManager.aeadFor(coupleId) ?: return null val json = fieldEncryptor.decrypt(payload, aead, coupleId) ?: return null @@ -130,12 +113,10 @@ class FirestoreDateReflectionDataSource @Inject constructor( } /** 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 - } + suspend fun markRevealed(coupleId: String, dateId: String, userId: String) { + // Best-effort: a failed reveal flag just skips the optional push. + runCatching { answerRef(coupleId, dateId, userId).update(mapOf("isRevealed" to true)).await() } + } private fun encode(r: DateReflection): String = JSONObject().apply { put("favoriteMoment", r.favoriteMoment) 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 10ecc203..cc654f4d 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreQuestionThreadDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreQuestionThreadDataSource.kt @@ -19,11 +19,9 @@ import com.google.firebase.firestore.Query import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow -import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.tasks.await import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException @Singleton class FirestoreQuestionThreadDataSource @Inject constructor( @@ -48,7 +46,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor( val snap = threadsRef(coupleId) .whereEqualTo("questionId", questionId) .limit(1) - .getAwait() + .get().await() return snap.documents.firstOrNull()?.id } @@ -65,7 +63,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor( "createdAt" to now, "updatedAt" to now ) - ).voidAwait() + ).await() return doc.id } @@ -81,7 +79,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor( suspend fun updateThreadStatus(coupleId: String, threadId: String, status: QuestionThreadStatus) { threadsRef(coupleId).document(threadId) .update("status", status.toFirestoreValue()) - .voidAwait() + .await() } // ─── Answers ───────────────────────────────────────────────────────────────── @@ -123,7 +121,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor( "createdAt" to FieldValue.serverTimestamp(), "updatedAt" to FieldValue.serverTimestamp() ) - ).voidAwait() + ).await() } // Call after releasing the one-time key so the answer doc reflects the released state. @@ -133,14 +131,14 @@ class FirestoreQuestionThreadDataSource @Inject constructor( .collection(FirestoreCollections.QuestionThreads.ANSWERS) .document(userId) .update(mapOf("answerKeyReleased" to true, "updatedAt" to FieldValue.serverTimestamp())) - .voidAwait() + .await() } suspend fun getAnswerCount(coupleId: String, threadId: String): Int { val snap = threadsRef(coupleId) .document(threadId) .collection(FirestoreCollections.QuestionThreads.ANSWERS) - .getAwait() + .get().await() return snap.size() } @@ -169,7 +167,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor( "text" to fieldEncryptor.encrypt(message.text, aead, coupleId), "createdAt" to FieldValue.serverTimestamp() ) - ).refAwait() + ).await() } /** @@ -190,7 +188,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor( "mediaUrl" to url, "createdAt" to FieldValue.serverTimestamp() ) - ).refAwait() + ).await() } /** Downloads + decrypts an image message's bytes for display (couple key, on-device). */ @@ -228,7 +226,7 @@ class FirestoreQuestionThreadDataSource @Inject constructor( "emoji" to reaction.emoji, "createdAt" to FieldValue.serverTimestamp() ) - ).voidAwait() + ).await() } fun observeReactions(coupleId: String, threadId: String): Flow> = callbackFlow { @@ -242,32 +240,6 @@ class FirestoreQuestionThreadDataSource @Inject constructor( awaitClose { listener.remove() } } - // ─── Coroutine helpers ─────────────────────────────────────────────────────── - - private suspend fun com.google.firebase.firestore.CollectionReference.getAwait() = - get().queryAwait() - - private suspend fun com.google.firebase.firestore.Query.getAwait() = - get().queryAwait() - - private suspend fun com.google.android.gms.tasks.Task.queryAwait() = - suspendCancellableCoroutine { cont -> - addOnSuccessListener { cont.resume(it) } - addOnFailureListener { cont.resumeWithException(it) } - } - - private suspend fun com.google.android.gms.tasks.Task.voidAwait() = - suspendCancellableCoroutine { cont -> - addOnSuccessListener { cont.resume(Unit) } - addOnFailureListener { cont.resumeWithException(it) } - } - - private suspend fun com.google.android.gms.tasks.Task.refAwait() = - suspendCancellableCoroutine { cont -> - addOnSuccessListener { cont.resume(Unit) } - addOnFailureListener { cont.resumeWithException(it) } - } - // ─── Document mappers ──────────────────────────────────────────────────────── private fun DocumentSnapshot.toQuestionThread(coupleId: String) = QuestionThread( diff --git a/app/src/main/java/app/closer/data/remote/FirestoreUserDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreUserDataSource.kt index 2d4ab486..daee12fd 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreUserDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreUserDataSource.kt @@ -10,11 +10,9 @@ 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 kotlinx.coroutines.tasks.await import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException @Singleton class FirestoreUserDataSource @Inject constructor( @@ -44,11 +42,7 @@ class FirestoreUserDataSource @Inject constructor( } private suspend fun getSnapshot(uid: String): DocumentSnapshot? = - suspendCancellableCoroutine { cont -> - userRef(uid).get() - .addOnSuccessListener { cont.resume(it.takeIf { s -> s.exists() }) } - .addOnFailureListener { cont.resumeWithException(it) } - } + userRef(uid).get().await().takeIf { it.exists() } private fun snapshotToUser(id: String, data: DocumentSnapshot): User { val coupleId = data.getString("coupleId") @@ -67,15 +61,10 @@ class FirestoreUserDataSource @Inject constructor( ) } - suspend fun getUser(uid: String): User? = - suspendCancellableCoroutine { cont -> - userRef(uid).get() - .addOnSuccessListener { snap -> - if (!snap.exists()) { cont.resume(null); return@addOnSuccessListener } - cont.resume(snapshotToUser(snap.id, snap)) - } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun getUser(uid: String): User? { + val snap = userRef(uid).get().await() + return if (snap.exists()) snapshotToUser(snap.id, snap) else null + } fun observeUser(uid: String): Flow = callbackFlow { val listener = userRef(uid).addSnapshotListener { snap, error -> @@ -88,25 +77,22 @@ class FirestoreUserDataSource @Inject constructor( awaitClose { listener.remove() } } - suspend fun createUser(user: User): Unit = - suspendCancellableCoroutine { cont -> - userRef(user.id).set( - mapOf( - "email" to user.email, - "displayName" to encryptProfileField(user.displayName, user.coupleId), - "photoUrl" to user.photoUrl, - "sex" to encryptProfileField(user.sex, user.coupleId), - "partnerId" to user.partnerId, - "coupleId" to user.coupleId, - "plan" to user.plan, - "birthDate" to user.birthDate, - "createdAt" to user.createdAt, - "lastActiveAt" to user.lastActiveAt - ) + suspend fun createUser(user: User) { + userRef(user.id).set( + mapOf( + "email" to user.email, + "displayName" to encryptProfileField(user.displayName, user.coupleId), + "photoUrl" to user.photoUrl, + "sex" to encryptProfileField(user.sex, user.coupleId), + "partnerId" to user.partnerId, + "coupleId" to user.coupleId, + "plan" to user.plan, + "birthDate" to user.birthDate, + "createdAt" to user.createdAt, + "lastActiveAt" to user.lastActiveAt ) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + ).await() + } /** Age-gate DOB (O-AGE-001). Set once when a Google/legacy user has no birthDate yet. */ suspend fun updateBirthDate(uid: String, birthDateMillis: Long): Unit = @@ -126,15 +112,12 @@ class FirestoreUserDataSource @Inject constructor( ) } - suspend fun updatePhotoUrl(uid: String, photoUrl: String): Unit = - suspendCancellableCoroutine { cont -> - userRef(uid).set( - mapOf("photoUrl" to photoUrl, "lastActiveAt" to System.currentTimeMillis()), - SetOptions.merge() - ) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun updatePhotoUrl(uid: String, photoUrl: String) { + userRef(uid).set( + mapOf("photoUrl" to photoUrl, "lastActiveAt" to System.currentTimeMillis()), + SetOptions.merge() + ).await() + } suspend fun updateSex(uid: String, sex: String) { // Never write the locked placeholder back as the value (would corrupt + then encrypt it). @@ -149,12 +132,9 @@ class FirestoreUserDataSource @Inject constructor( ) } - private suspend fun setMerge(uid: String, data: Map): Unit = - suspendCancellableCoroutine { cont -> - userRef(uid).set(data, SetOptions.merge()) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + private suspend fun setMerge(uid: String, data: Map) { + userRef(uid).set(data, SetOptions.merge()).await() + } /** * Encrypt any still-plaintext profile fields once the couple key is available (idempotent — skips @@ -198,31 +178,23 @@ class FirestoreUserDataSource @Inject constructor( val ENCRYPTED_PROFILE_FIELDS = listOf("displayName", "sex") } - suspend fun hasProfile(uid: String): Boolean = - suspendCancellableCoroutine { cont -> - userRef(uid).get() - .addOnSuccessListener { snap -> - val name = snap.getString("displayName") ?: "" - cont.resume(snap.exists() && name.isNotBlank()) - } - .addOnFailureListener { cont.resume(false) } - } + suspend fun hasProfile(uid: String): Boolean = runCatching { + val snap = userRef(uid).get().await() + snap.exists() && !snap.getString("displayName").isNullOrBlank() + }.getOrDefault(false) - suspend fun storeFcmToken(uid: String, token: String): Unit = - suspendCancellableCoroutine { cont -> - userRef(uid).set( - mapOf("fcmToken" to token, "lastActiveAt" to System.currentTimeMillis()), - SetOptions.merge() - ) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun storeFcmToken(uid: String, token: String) { + userRef(uid).set( + mapOf("fcmToken" to token, "lastActiveAt" to System.currentTimeMillis()), + SetOptions.merge() + ).await() + } suspend fun storeTokenMetadata( uid: String, token: String, metadata: DeviceMetadata - ): Unit = suspendCancellableCoroutine { cont -> + ) { userRef(uid).collection(FirestoreCollections.Users.FCM_TOKENS).document(token) .set( mapOf( @@ -234,16 +206,12 @@ class FirestoreUserDataSource @Inject constructor( "updatedAt" to metadata.timestamp ) ) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } + .await() } - suspend fun clearCoupleId(uid: String): Unit = - suspendCancellableCoroutine { cont -> - userRef(uid).update(mapOf("coupleId" to null)) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun clearCoupleId(uid: String) { + userRef(uid).update(mapOf("coupleId" to null)).await() + } suspend fun updateNotificationPrefs( uid: String, @@ -252,7 +220,7 @@ class FirestoreUserDataSource @Inject constructor( dailyReminder: Boolean, streakReminder: Boolean, promotional: Boolean - ): Unit = suspendCancellableCoroutine { cont -> + ) { userRef(uid).set( mapOf( "notifPartnerAnswered" to partnerAnswered, @@ -262,9 +230,7 @@ class FirestoreUserDataSource @Inject constructor( "notifPromotional" to promotional ), SetOptions.merge() - ) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } + ).await() } /** @@ -279,7 +245,7 @@ class FirestoreUserDataSource @Inject constructor( startMinutes: Int, endMinutes: Int, timezone: String - ): Unit = suspendCancellableCoroutine { cont -> + ) { userRef(uid).set( mapOf( "quietHoursEnabled" to enabled, @@ -288,15 +254,10 @@ class FirestoreUserDataSource @Inject constructor( "timezone" to timezone ), SetOptions.merge() - ) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } + ).await() } - suspend fun deleteUserData(uid: String): Unit = - suspendCancellableCoroutine { cont -> - userRef(uid).delete() - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun deleteUserData(uid: String) { + userRef(uid).delete().await() + } }