mirror of
https://github.com/d4rken-org/capod.git
synced 2026-09-14 18:26:11 -04:00
fix(upgrade): Report empty Play product results as a merchandising error
When both the IAP and SUB queries came back with nothing, the screen always reported a connectivity failure, telling users to clear Play's cache and reboot. Play can answer OK and simply have no sellable offer (region, account eligibility, pulled product), where that advice is futile. Both causes are now inspected: only when BOTH are OfferUnavailableBillingException does the merchandising copy surface. A single non-merchandising failure can't rule out a real Play problem, so the conservative copy stays.
This commit is contained in:
@@ -18,6 +18,7 @@ import eu.darken.capod.common.uix.ViewModel4
|
||||
import eu.darken.capod.common.upgrade.core.OurSku
|
||||
import eu.darken.capod.common.upgrade.core.UpgradeRepoGplay
|
||||
import eu.darken.capod.common.upgrade.core.billing.GplayServiceUnavailableException
|
||||
import eu.darken.capod.common.upgrade.core.billing.OfferUnavailableBillingException
|
||||
import eu.darken.capod.common.upgrade.core.billing.Sku
|
||||
import eu.darken.capod.common.upgrade.core.billing.SkuDetails
|
||||
import kotlinx.coroutines.CancellationException
|
||||
@@ -186,9 +187,22 @@ class UpgradeViewModel @Inject constructor(
|
||||
|
||||
if (done != null) {
|
||||
if (iap == null && sub == null) {
|
||||
val serviceUnavailableError = GplayServiceUnavailableException(
|
||||
done.iap.exceptionOrNull() ?: RuntimeException("IAP and SUB data request failed.")
|
||||
)
|
||||
val iapCause = done.iap.exceptionOrNull()
|
||||
val subCause = done.sub.exceptionOrNull()
|
||||
// Play answered fine and simply has nothing to sell here (region, account
|
||||
// eligibility, pulled product): reporting that as a connectivity failure sends the
|
||||
// user chasing futile advice (clear Play's cache, reboot). Only when BOTH causes
|
||||
// are merchandising — a single connectivity failure can't rule out a real Play
|
||||
// problem, so the conservative "can't reach Play" copy stays correct.
|
||||
val queryError = if (
|
||||
iapCause is OfferUnavailableBillingException && subCause is OfferUnavailableBillingException
|
||||
) {
|
||||
iapCause
|
||||
} else {
|
||||
GplayServiceUnavailableException(
|
||||
iapCause ?: RuntimeException("IAP and SUB data request failed.")
|
||||
)
|
||||
}
|
||||
// Grace users and owners are excluded: during an outage (exactly when grace
|
||||
// matters) they must keep the Loaded presentation with their status/grace card,
|
||||
// not an acquisition-style error state or dialog.
|
||||
@@ -197,9 +211,9 @@ class UpgradeViewModel @Inject constructor(
|
||||
// toggling) — emit once per failure episode, not once per recombination.
|
||||
if (!hasShownServiceUnavailableError) {
|
||||
hasShownServiceUnavailableError = true
|
||||
errorEvents.tryEmit(serviceUnavailableError)
|
||||
errorEvents.tryEmit(queryError)
|
||||
}
|
||||
return@combine GplayUpgradeUiState.Unavailable(serviceUnavailableError)
|
||||
return@combine GplayUpgradeUiState.Unavailable(queryError)
|
||||
}
|
||||
} else {
|
||||
hasShownServiceUnavailableError = false
|
||||
|
||||
@@ -10,6 +10,7 @@ import eu.darken.capod.common.upgrade.core.OurSku
|
||||
import eu.darken.capod.common.upgrade.core.UpgradeRepoGplay
|
||||
import eu.darken.capod.common.upgrade.core.billing.BillingData
|
||||
import eu.darken.capod.common.upgrade.core.billing.GplayServiceUnavailableException
|
||||
import eu.darken.capod.common.upgrade.core.billing.OfferUnavailableBillingException
|
||||
import eu.darken.capod.common.upgrade.core.billing.Sku
|
||||
import io.kotest.matchers.booleans.shouldBeTrue
|
||||
import io.kotest.matchers.collections.shouldBeEmpty
|
||||
@@ -92,6 +93,67 @@ class GplayUpgradeViewModelTest : BaseTest() {
|
||||
coVerify(exactly = 1) { repo.querySkus(OurSku.Sub.PRO_UPGRADE) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `both product types unavailable surfaces the merchandising error, not a connectivity one`() = runTest2(
|
||||
context = testDispatcher,
|
||||
) {
|
||||
// Play answered OK and simply has no sellable offer here (region, account eligibility,
|
||||
// pulled product). Reporting that as "can't connect to Google Play" tells the user to
|
||||
// clear Play's cache and reboot, which cannot help.
|
||||
val repo = mockRepo()
|
||||
coEvery { repo.querySkus(OurSku.Iap.PRO_UPGRADE) } throws
|
||||
OfferUnavailableBillingException(OurSku.Iap.PRO_UPGRADE, null)
|
||||
coEvery { repo.querySkus(OurSku.Sub.PRO_UPGRADE) } throws
|
||||
OfferUnavailableBillingException(OurSku.Sub.PRO_UPGRADE, null)
|
||||
val vm = buildVm(repo)
|
||||
|
||||
val unavailableState = async { vm.state.first { it is GplayUpgradeUiState.Unavailable } }
|
||||
val forwardedError = async { vm.errorEvents.first() }
|
||||
advanceUntilIdle()
|
||||
|
||||
forwardedError.await().shouldBeInstanceOf<OfferUnavailableBillingException>()
|
||||
val state = unavailableState.await().shouldBeInstanceOf<GplayUpgradeUiState.Unavailable>()
|
||||
state.error.shouldBeInstanceOf<OfferUnavailableBillingException>()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a mixed failure keeps the conservative connectivity error`() = runTest2(
|
||||
context = testDispatcher,
|
||||
) {
|
||||
// One sku failed for a non-merchandising reason: a real Play problem can't be ruled out,
|
||||
// so the conservative "can't reach Play" copy stays.
|
||||
val repo = mockRepo()
|
||||
coEvery { repo.querySkus(OurSku.Iap.PRO_UPGRADE) } throws
|
||||
OfferUnavailableBillingException(OurSku.Iap.PRO_UPGRADE, null)
|
||||
coEvery { repo.querySkus(OurSku.Sub.PRO_UPGRADE) } throws IllegalStateException("Play unavailable")
|
||||
val vm = buildVm(repo)
|
||||
|
||||
val unavailableState = async { vm.state.first { it is GplayUpgradeUiState.Unavailable } }
|
||||
val forwardedError = async { vm.errorEvents.first() }
|
||||
advanceUntilIdle()
|
||||
|
||||
forwardedError.await().shouldBeInstanceOf<GplayServiceUnavailableException>()
|
||||
val state = unavailableState.await().shouldBeInstanceOf<GplayUpgradeUiState.Unavailable>()
|
||||
state.error.shouldBeInstanceOf<GplayServiceUnavailableException>()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a connectivity failure on both product types stays a connectivity error`() = runTest2(
|
||||
context = testDispatcher,
|
||||
) {
|
||||
val repo = mockRepo()
|
||||
coEvery { repo.querySkus(any()) } throws IllegalStateException("Play unavailable")
|
||||
val vm = buildVm(repo)
|
||||
|
||||
val unavailableState = async { vm.state.first { it is GplayUpgradeUiState.Unavailable } }
|
||||
val forwardedError = async { vm.errorEvents.first() }
|
||||
advanceUntilIdle()
|
||||
|
||||
forwardedError.await().shouldBeInstanceOf<GplayServiceUnavailableException>()
|
||||
val state = unavailableState.await().shouldBeInstanceOf<GplayUpgradeUiState.Unavailable>()
|
||||
state.error.shouldBeInstanceOf<GplayServiceUnavailableException>()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a slow but healthy Play store loads instead of tripping the timeout`() = runTest2(
|
||||
context = testDispatcher,
|
||||
|
||||
Reference in New Issue
Block a user