diff --git a/app/src/main/java/app/closer/data/remote/FirestoreCoupleDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreCoupleDataSource.kt index 3016a950..b1f2283e 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreCoupleDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreCoupleDataSource.kt @@ -8,12 +8,9 @@ import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.SetOptions import com.google.firebase.functions.FirebaseFunctions -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 FirestoreCoupleDataSource @Inject constructor( @@ -45,7 +42,7 @@ class FirestoreCoupleDataSource @Inject constructor( inviteCode: String, now: Long, wrappedKey: RecoveryKeyManager.WrappedKey - ): Unit = suspendCancellableCoroutine { cont -> + ) { val data = mutableMapOf( "id" to coupleId, "userIds" to listOf(inviterUserId, acceptorUserId), @@ -57,51 +54,31 @@ class FirestoreCoupleDataSource @Inject constructor( data["wrappedCoupleKey"] = wrappedKey.cipherB64 data["kdfSalt"] = wrappedKey.saltB64 data["kdfParams"] = wrappedKey.params - coupleRef(coupleId).set(data) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } + coupleRef(coupleId).set(data).await() } - suspend fun updateWrappedKey(coupleId: String, wrappedKey: RecoveryKeyManager.WrappedKey): Unit = - suspendCancellableCoroutine { cont -> - coupleRef(coupleId).set( - mapOf( - "wrappedCoupleKey" to wrappedKey.cipherB64, - "kdfSalt" to wrappedKey.saltB64, - "kdfParams" to wrappedKey.params - ), - SetOptions.merge() - ) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun updateWrappedKey(coupleId: String, wrappedKey: RecoveryKeyManager.WrappedKey) { + coupleRef(coupleId).set( + mapOf( + "wrappedCoupleKey" to wrappedKey.cipherB64, + "kdfSalt" to wrappedKey.saltB64, + "kdfParams" to wrappedKey.params + ), + SetOptions.merge() + ).await() + } - private suspend fun updateUserCoupleId(uid: String, coupleId: String): Unit = - suspendCancellableCoroutine { cont -> - userRef(uid).set( - mapOf("coupleId" to coupleId), - SetOptions.merge() - ) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + private suspend fun updateUserCoupleId(uid: String, coupleId: String) { + userRef(uid).set(mapOf("coupleId" to coupleId), SetOptions.merge()).await() + } - suspend fun getCoupleById(coupleId: String): Couple? = - suspendCancellableCoroutine { cont -> - coupleRef(coupleId).get() - .addOnSuccessListener { snap -> - if (!snap.exists()) { cont.resume(null); return@addOnSuccessListener } - cont.resume(snap.toCouple()) - } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun getCoupleById(coupleId: String): Couple? { + val snap = coupleRef(coupleId).get().await() + return if (snap.exists()) snap.toCouple() else null + } suspend fun updateStreak(coupleId: String) { - val snap = suspendCancellableCoroutine { cont -> - coupleRef(coupleId).get() - .addOnSuccessListener { cont.resume(it) } - .addOnFailureListener { cont.resumeWithException(it) } - } + val snap = coupleRef(coupleId).get().await() val lastAnsweredAt = snap.millisOrNull("lastAnsweredAt") ?: 0L val now = System.currentTimeMillis() val current = (snap.getLong("streakCount") ?: 0L).toInt() @@ -114,14 +91,10 @@ class FirestoreCoupleDataSource @Inject constructor( ) // Same-day answer → nothing to change; skip the redundant write. if (!next.changed) return - suspendCancellableCoroutine { cont -> - coupleRef(coupleId).set( - mapOf("streakCount" to next.newStreak, "lastAnsweredAt" to now), - SetOptions.merge() - ) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + coupleRef(coupleId).set( + mapOf("streakCount" to next.newStreak, "lastAnsweredAt" to now), + SetOptions.merge() + ).await() } suspend fun leaveCouple() { diff --git a/app/src/main/java/app/closer/data/remote/FirestoreOutcomeDataSource.kt b/app/src/main/java/app/closer/data/remote/FirestoreOutcomeDataSource.kt index 8b3335d8..ddf05c33 100644 --- a/app/src/main/java/app/closer/data/remote/FirestoreOutcomeDataSource.kt +++ b/app/src/main/java/app/closer/data/remote/FirestoreOutcomeDataSource.kt @@ -8,12 +8,9 @@ import com.google.firebase.firestore.DocumentSnapshot import com.google.firebase.firestore.FieldPath import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.functions.FirebaseFunctions -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 FirestoreOutcomeDataSource @Inject constructor( @@ -25,29 +22,22 @@ class FirestoreOutcomeDataSource @Inject constructor( val OUTCOME_DAY_KEYS: List = OutcomeDay.entries.map { it.key } } - suspend fun submitOutcome(coupleId: String, dayKey: OutcomeDayKey, scores: OutcomeScores): Unit = - suspendCancellableCoroutine { cont -> - functions.getHttpsCallable("submitOutcomeCallable") - .call( - mapOf( - "coupleId" to coupleId, - "dayKey" to dayKey, - "scores" to scores.toMap() - ) + suspend fun submitOutcome(coupleId: String, dayKey: OutcomeDayKey, scores: OutcomeScores) { + functions.getHttpsCallable("submitOutcomeCallable") + .call( + mapOf( + "coupleId" to coupleId, + "dayKey" to dayKey, + "scores" to scores.toMap() ) - .addOnSuccessListener { cont.resume(Unit) } - .addOnFailureListener { cont.resumeWithException(it) } - } + ) + .await() + } - suspend fun getOutcome(coupleId: String, dayKey: OutcomeDayKey): Outcome? = - suspendCancellableCoroutine { cont -> - outcomeRef(coupleId, dayKey) - .get() - .addOnSuccessListener { snap -> - cont.resume(if (snap.exists()) snap.toOutcome() else null) - } - .addOnFailureListener { cont.resumeWithException(it) } - } + suspend fun getOutcome(coupleId: String, dayKey: OutcomeDayKey): Outcome? { + val snap = outcomeRef(coupleId, dayKey).get().await() + return if (snap.exists()) snap.toOutcome() else null + } suspend fun getOutcomes(coupleId: String): List { // Security rules permit reading only the fixed dayKey docs and DENY an diff --git a/app/src/main/java/app/closer/data/security/PlayIntegrityChecker.kt b/app/src/main/java/app/closer/data/security/PlayIntegrityChecker.kt index 5f7d9900..3110e8a7 100644 --- a/app/src/main/java/app/closer/data/security/PlayIntegrityChecker.kt +++ b/app/src/main/java/app/closer/data/security/PlayIntegrityChecker.kt @@ -12,12 +12,10 @@ import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.tasks.await import java.util.UUID import javax.inject.Inject import javax.inject.Singleton -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException /** * Checks device integrity using the Play Integrity API. @@ -73,31 +71,27 @@ class PlayIntegrityChecker @Inject constructor( } } - private suspend fun requestToken(): String = suspendCancellableCoroutine { cont -> + private suspend fun requestToken(): String { val nonce = Base64.encodeToString( "${UUID.randomUUID()}-${System.currentTimeMillis()}".toByteArray(Charsets.UTF_8), Base64.URL_SAFE or Base64.NO_WRAP, ) - IntegrityManagerFactory.create(context) + return IntegrityManagerFactory.create(context) .requestIntegrityToken(IntegrityTokenRequest.builder().setNonce(nonce).build()) - .addOnSuccessListener { cont.resume(it.token()) } - .addOnFailureListener { cont.resumeWithException(it) } + .await() + .token() } - /** Returns the server verdict, or null when verification couldn't be completed. */ - private suspend fun verifyWithServer(token: String): Boolean? = - suspendCancellableCoroutine { cont -> - functions - .getHttpsCallable("checkDeviceIntegrity") - .call(mapOf("token" to token)) - .addOnSuccessListener { result -> - val passed = (result.getData() as? Map<*, *>)?.get("passed") as? Boolean - cont.resume(passed) // null if the server returned no explicit verdict - } - .addOnFailureListener { - // Couldn't verify (server/network). Don't assert PASSED — surface as - // UNAVAILABLE. Firebase App Check remains the server-side gatekeeper. - cont.resume(null) - } - } + /** + * Returns the server verdict, or null when verification couldn't be completed. A failure + * (server/network) maps to null — NOT an exception — so we don't assert PASSED; Firebase App + * Check remains the server-side gatekeeper. (Hence runCatching rather than a bare await.) + */ + private suspend fun verifyWithServer(token: String): Boolean? = runCatching { + val result = functions + .getHttpsCallable("checkDeviceIntegrity") + .call(mapOf("token" to token)) + .await() + (result.getData() as? Map<*, *>)?.get("passed") as? Boolean + }.getOrNull() }