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.FirebaseFirestore
|
||||||
import com.google.firebase.firestore.SetOptions
|
import com.google.firebase.firestore.SetOptions
|
||||||
import com.google.firebase.functions.FirebaseFunctions
|
import com.google.firebase.functions.FirebaseFunctions
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class FirestoreCoupleDataSource @Inject constructor(
|
class FirestoreCoupleDataSource @Inject constructor(
|
||||||
|
|
@ -45,7 +42,7 @@ class FirestoreCoupleDataSource @Inject constructor(
|
||||||
inviteCode: String,
|
inviteCode: String,
|
||||||
now: Long,
|
now: Long,
|
||||||
wrappedKey: RecoveryKeyManager.WrappedKey
|
wrappedKey: RecoveryKeyManager.WrappedKey
|
||||||
): Unit = suspendCancellableCoroutine { cont ->
|
) {
|
||||||
val data = mutableMapOf<String, Any>(
|
val data = mutableMapOf<String, Any>(
|
||||||
"id" to coupleId,
|
"id" to coupleId,
|
||||||
"userIds" to listOf(inviterUserId, acceptorUserId),
|
"userIds" to listOf(inviterUserId, acceptorUserId),
|
||||||
|
|
@ -57,13 +54,10 @@ class FirestoreCoupleDataSource @Inject constructor(
|
||||||
data["wrappedCoupleKey"] = wrappedKey.cipherB64
|
data["wrappedCoupleKey"] = wrappedKey.cipherB64
|
||||||
data["kdfSalt"] = wrappedKey.saltB64
|
data["kdfSalt"] = wrappedKey.saltB64
|
||||||
data["kdfParams"] = wrappedKey.params
|
data["kdfParams"] = wrappedKey.params
|
||||||
coupleRef(coupleId).set(data)
|
coupleRef(coupleId).set(data).await()
|
||||||
.addOnSuccessListener { cont.resume(Unit) }
|
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateWrappedKey(coupleId: String, wrappedKey: RecoveryKeyManager.WrappedKey): Unit =
|
suspend fun updateWrappedKey(coupleId: String, wrappedKey: RecoveryKeyManager.WrappedKey) {
|
||||||
suspendCancellableCoroutine { cont ->
|
|
||||||
coupleRef(coupleId).set(
|
coupleRef(coupleId).set(
|
||||||
mapOf(
|
mapOf(
|
||||||
"wrappedCoupleKey" to wrappedKey.cipherB64,
|
"wrappedCoupleKey" to wrappedKey.cipherB64,
|
||||||
|
|
@ -71,37 +65,20 @@ class FirestoreCoupleDataSource @Inject constructor(
|
||||||
"kdfParams" to wrappedKey.params
|
"kdfParams" to wrappedKey.params
|
||||||
),
|
),
|
||||||
SetOptions.merge()
|
SetOptions.merge()
|
||||||
)
|
).await()
|
||||||
.addOnSuccessListener { cont.resume(Unit) }
|
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateUserCoupleId(uid: String, coupleId: String): Unit =
|
private suspend fun updateUserCoupleId(uid: String, coupleId: String) {
|
||||||
suspendCancellableCoroutine { cont ->
|
userRef(uid).set(mapOf("coupleId" to coupleId), SetOptions.merge()).await()
|
||||||
userRef(uid).set(
|
|
||||||
mapOf("coupleId" to coupleId),
|
|
||||||
SetOptions.merge()
|
|
||||||
)
|
|
||||||
.addOnSuccessListener { cont.resume(Unit) }
|
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getCoupleById(coupleId: String): Couple? =
|
suspend fun getCoupleById(coupleId: String): Couple? {
|
||||||
suspendCancellableCoroutine { cont ->
|
val snap = coupleRef(coupleId).get().await()
|
||||||
coupleRef(coupleId).get()
|
return if (snap.exists()) snap.toCouple() else null
|
||||||
.addOnSuccessListener { snap ->
|
|
||||||
if (!snap.exists()) { cont.resume(null); return@addOnSuccessListener }
|
|
||||||
cont.resume(snap.toCouple())
|
|
||||||
}
|
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun updateStreak(coupleId: String) {
|
suspend fun updateStreak(coupleId: String) {
|
||||||
val snap = suspendCancellableCoroutine<DocumentSnapshot> { cont ->
|
val snap = coupleRef(coupleId).get().await()
|
||||||
coupleRef(coupleId).get()
|
|
||||||
.addOnSuccessListener { cont.resume(it) }
|
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
|
||||||
}
|
|
||||||
val lastAnsweredAt = snap.millisOrNull("lastAnsweredAt") ?: 0L
|
val lastAnsweredAt = snap.millisOrNull("lastAnsweredAt") ?: 0L
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
val current = (snap.getLong("streakCount") ?: 0L).toInt()
|
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.
|
// Same-day answer → nothing to change; skip the redundant write.
|
||||||
if (!next.changed) return
|
if (!next.changed) return
|
||||||
suspendCancellableCoroutine<Unit> { cont ->
|
|
||||||
coupleRef(coupleId).set(
|
coupleRef(coupleId).set(
|
||||||
mapOf("streakCount" to next.newStreak, "lastAnsweredAt" to now),
|
mapOf("streakCount" to next.newStreak, "lastAnsweredAt" to now),
|
||||||
SetOptions.merge()
|
SetOptions.merge()
|
||||||
)
|
).await()
|
||||||
.addOnSuccessListener { cont.resume(Unit) }
|
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun leaveCouple() {
|
suspend fun leaveCouple() {
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,9 @@ import com.google.firebase.firestore.DocumentSnapshot
|
||||||
import com.google.firebase.firestore.FieldPath
|
import com.google.firebase.firestore.FieldPath
|
||||||
import com.google.firebase.firestore.FirebaseFirestore
|
import com.google.firebase.firestore.FirebaseFirestore
|
||||||
import com.google.firebase.functions.FirebaseFunctions
|
import com.google.firebase.functions.FirebaseFunctions
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
|
||||||
import kotlinx.coroutines.tasks.await
|
import kotlinx.coroutines.tasks.await
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class FirestoreOutcomeDataSource @Inject constructor(
|
class FirestoreOutcomeDataSource @Inject constructor(
|
||||||
|
|
@ -25,8 +22,7 @@ class FirestoreOutcomeDataSource @Inject constructor(
|
||||||
val OUTCOME_DAY_KEYS: List<OutcomeDayKey> = OutcomeDay.entries.map { it.key }
|
val OUTCOME_DAY_KEYS: List<OutcomeDayKey> = OutcomeDay.entries.map { it.key }
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun submitOutcome(coupleId: String, dayKey: OutcomeDayKey, scores: OutcomeScores): Unit =
|
suspend fun submitOutcome(coupleId: String, dayKey: OutcomeDayKey, scores: OutcomeScores) {
|
||||||
suspendCancellableCoroutine { cont ->
|
|
||||||
functions.getHttpsCallable("submitOutcomeCallable")
|
functions.getHttpsCallable("submitOutcomeCallable")
|
||||||
.call(
|
.call(
|
||||||
mapOf(
|
mapOf(
|
||||||
|
|
@ -35,18 +31,12 @@ class FirestoreOutcomeDataSource @Inject constructor(
|
||||||
"scores" to scores.toMap()
|
"scores" to scores.toMap()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.addOnSuccessListener { cont.resume(Unit) }
|
.await()
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getOutcome(coupleId: String, dayKey: OutcomeDayKey): Outcome? =
|
suspend fun getOutcome(coupleId: String, dayKey: OutcomeDayKey): Outcome? {
|
||||||
suspendCancellableCoroutine { cont ->
|
val snap = outcomeRef(coupleId, dayKey).get().await()
|
||||||
outcomeRef(coupleId, dayKey)
|
return if (snap.exists()) snap.toOutcome() else null
|
||||||
.get()
|
|
||||||
.addOnSuccessListener { snap ->
|
|
||||||
cont.resume(if (snap.exists()) snap.toOutcome() else null)
|
|
||||||
}
|
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getOutcomes(coupleId: String): List<Outcome> {
|
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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.tasks.await
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
import kotlin.coroutines.resume
|
|
||||||
import kotlin.coroutines.resumeWithException
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks device integrity using the Play Integrity API.
|
* 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(
|
val nonce = Base64.encodeToString(
|
||||||
"${UUID.randomUUID()}-${System.currentTimeMillis()}".toByteArray(Charsets.UTF_8),
|
"${UUID.randomUUID()}-${System.currentTimeMillis()}".toByteArray(Charsets.UTF_8),
|
||||||
Base64.URL_SAFE or Base64.NO_WRAP,
|
Base64.URL_SAFE or Base64.NO_WRAP,
|
||||||
)
|
)
|
||||||
IntegrityManagerFactory.create(context)
|
return IntegrityManagerFactory.create(context)
|
||||||
.requestIntegrityToken(IntegrityTokenRequest.builder().setNonce(nonce).build())
|
.requestIntegrityToken(IntegrityTokenRequest.builder().setNonce(nonce).build())
|
||||||
.addOnSuccessListener { cont.resume(it.token()) }
|
.await()
|
||||||
.addOnFailureListener { cont.resumeWithException(it) }
|
.token()
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Returns the server verdict, or null when verification couldn't be completed. */
|
/**
|
||||||
private suspend fun verifyWithServer(token: String): Boolean? =
|
* Returns the server verdict, or null when verification couldn't be completed. A failure
|
||||||
suspendCancellableCoroutine { cont ->
|
* (server/network) maps to null — NOT an exception — so we don't assert PASSED; Firebase App
|
||||||
functions
|
* 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")
|
.getHttpsCallable("checkDeviceIntegrity")
|
||||||
.call(mapOf("token" to token))
|
.call(mapOf("token" to token))
|
||||||
.addOnSuccessListener { result ->
|
.await()
|
||||||
val passed = (result.getData() as? Map<*, *>)?.get("passed") as? Boolean
|
(result.getData() as? Map<*, *>)?.get("passed") as? Boolean
|
||||||
cont.resume(passed) // null if the server returned no explicit verdict
|
}.getOrNull()
|
||||||
}
|
|
||||||
.addOnFailureListener {
|
|
||||||
// Couldn't verify (server/network). Don't assert PASSED — surface as
|
|
||||||
// UNAVAILABLE. Firebase App Check remains the server-side gatekeeper.
|
|
||||||
cont.resume(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue