fix(upgrade): Show supporter status on every upgrade screen route

The FOSS upgrade screen's view mapping only surfaced STATUS_UPGRADED on
the manage route. Forced routes (the Pro-locked settings entry)
deliberately don't auto-close, so a supporter completing the sponsor
flow from there stayed on the sales pitch with a live sponsor button --
reading as if sponsoring didn't work, with only the transient thanks
toast saying otherwise. The gplay flavour already renders ownership
state route-independently on this path.

The isPro branch now wins on every route, matching the adjacent
comment's stated intent and gplay's behaviour. Forced routes keep their
don't-auto-close semantics; the durable status view is what
acknowledges the upgrade.
This commit is contained in:
darken
2026-08-16 17:44:02 +02:00
committed by Matthias Urhahn
parent 3e6541deec
commit abc82b0f09
2 changed files with 34 additions and 3 deletions
@@ -42,8 +42,10 @@ class UpgradeViewModel @Inject constructor(
// Which presentation the screen shows. The manage route (settings "upgrade status" entry)
// gets a status view first; the pitch only appears once a free user asks for the upgrade
// options. Upgrading wins over that choice — completing the sponsor flow from the pitch must
// land on the upgraded status, not back on the ask. null until the route is bound.
// options. Upgrading wins on EVERY route, not just manage: forced routes (Pro-locked settings)
// stay open after the sponsor flow completes, and the pitch with its live sponsor button reads
// as "sponsoring didn't work" to a fresh supporter — the toast alone is too transient for a
// money moment without a receipt behind it. null until the route is bound.
internal val state: StateFlow<State?> = combine(
routeFlow,
upgradeRepo.upgradeInfo,
@@ -51,7 +53,7 @@ class UpgradeViewModel @Inject constructor(
) { route, info, showOptions ->
val view = when {
route == null -> null
route.manage && info.isPro -> FossUpgradeView.STATUS_UPGRADED
info.isPro -> FossUpgradeView.STATUS_UPGRADED
route.manage && !showOptions -> FossUpgradeView.STATUS_FREE
else -> FossUpgradeView.PITCH
}
@@ -191,6 +191,35 @@ class FossUpgradeViewModelTest : BaseTest() {
upgradedView.await().view shouldBe FossUpgradeView.STATUS_UPGRADED
}
@Test
fun `forced route lands on the upgraded status when the sponsor flow completes`() = runTest2(
context = testDispatcher,
) {
// Forced routes (Pro-locked settings entry) deliberately don't auto-close, so the screen is
// still up when the unlock lands — it must flip to the supporter status instead of keeping
// the sales pitch, which reads as "sponsoring didn't work".
val info = MutableStateFlow(UpgradeRepoFoss.Info())
val vm = buildVm(repo = mockRepo(info))
val navEvents = mutableListOf<NavEvent>()
val collector = launch(start = CoroutineStart.UNDISPATCHED) { vm.navEvents.collect { navEvents.add(it) } }
val pitchView = async { vm.state.first { it != null }!! }
vm.bindRoute(Nav.Main.Upgrade(forced = true))
advanceUntilIdle()
pitchView.await().view shouldBe FossUpgradeView.PITCH
val upgradedView = async { vm.state.first { it?.view == FossUpgradeView.STATUS_UPGRADED }!! }
info.value = upgradedInfo()
advanceUntilIdle()
upgradedView.await().view shouldBe FossUpgradeView.STATUS_UPGRADED
// The don't-auto-close semantics of forced routes are unchanged — status, not navigation,
// is what acknowledges the upgrade here.
navEvents.shouldBeEmpty()
collector.cancel()
}
@Test
fun `default route bounces an upgraded user out of the screen`() = runTest2(context = testDispatcher) {
val vm = buildVm(repo = mockRepo(MutableStateFlow(upgradedInfo())))