refactor(data): modernize Firebase Tasks to .await() (safe subset)
Replace verbose suspendCancellableCoroutine{ cont -> task.addOnSuccessListener
{cont.resume}.addOnFailureListener{cont.resumeWithException} } wrappers with the
kotlinx-coroutines-play-services .await() extension in the files whose blocks are
all one-shot Tasks (no snapshot listeners / transactions to endanger):
FirestoreOutcomeDataSource, FirestoreCoupleDataSource, PlayIntegrityChecker.
Notably fixes FirestoreOutcomeDataSource, which mixed both styles in one file.
PlayIntegrityChecker.verifyWithServer keeps its failure→null semantics via
runCatching{ ... .await() }.getOrNull() (a bare await would throw instead).
The remaining ~11 data sources also use the callback pattern but mix in
callbackFlow snapshot listeners / transactions, so they need per-block review —
tracked in Future.md rather than bulk-rewritten on untested I/O.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
61a8609298
commit
7507ca63e4
|
|
@ -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<String, Any>(
|
||||
"id" to coupleId,
|
||||
"userIds" to listOf(inviterUserId, acceptorUserId),
|
||||
|
|
@ -57,13 +54,10 @@ 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 ->
|
||||
suspend fun updateWrappedKey(coupleId: String, wrappedKey: RecoveryKeyManager.WrappedKey) {
|
||||
coupleRef(coupleId).set(
|
||||
mapOf(
|
||||
"wrappedCoupleKey" to wrappedKey.cipherB64,
|
||||
|
|
@ -71,37 +65,20 @@ class FirestoreCoupleDataSource @Inject constructor(
|
|||
"kdfParams" to wrappedKey.params
|
||||
),
|
||||
SetOptions.merge()
|
||||
)
|
||||
.addOnSuccessListener { cont.resume(Unit) }
|
||||
.addOnFailureListener { cont.resumeWithException(it) }
|
||||
).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<DocumentSnapshot> { 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<Unit> { cont ->
|
||||
coupleRef(coupleId).set(
|
||||
mapOf("streakCount" to next.newStreak, "lastAnsweredAt" to now),
|
||||
SetOptions.merge()
|
||||
)
|
||||
.addOnSuccessListener { cont.resume(Unit) }
|
||||
.addOnFailureListener { cont.resumeWithException(it) }
|
||||
}
|
||||
).await()
|
||||
}
|
||||
|
||||
suspend fun leaveCouple() {
|
||||
|
|
|
|||
|
|
@ -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,8 +22,7 @@ class FirestoreOutcomeDataSource @Inject constructor(
|
|||
val OUTCOME_DAY_KEYS: List<OutcomeDayKey> = OutcomeDay.entries.map { it.key }
|
||||
}
|
||||
|
||||
suspend fun submitOutcome(coupleId: String, dayKey: OutcomeDayKey, scores: OutcomeScores): Unit =
|
||||
suspendCancellableCoroutine { cont ->
|
||||
suspend fun submitOutcome(coupleId: String, dayKey: OutcomeDayKey, scores: OutcomeScores) {
|
||||
functions.getHttpsCallable("submitOutcomeCallable")
|
||||
.call(
|
||||
mapOf(
|
||||
|
|
@ -35,18 +31,12 @@ class FirestoreOutcomeDataSource @Inject constructor(
|
|||
"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<Outcome> {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/**
|
||||
* 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))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
.await()
|
||||
(result.getData() as? Map<*, *>)?.get("passed") as? Boolean
|
||||
}.getOrNull()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue