diff --git a/app/src/foss/java/eu/darken/capod/common/upgrade/ui/UpgradeViewModel.kt b/app/src/foss/java/eu/darken/capod/common/upgrade/ui/UpgradeViewModel.kt index 482dfbbe..1bea647f 100644 --- a/app/src/foss/java/eu/darken/capod/common/upgrade/ui/UpgradeViewModel.kt +++ b/app/src/foss/java/eu/darken/capod/common/upgrade/ui/UpgradeViewModel.kt @@ -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 = 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 } diff --git a/app/src/testFoss/java/eu/darken/capod/common/upgrade/ui/FossUpgradeViewModelTest.kt b/app/src/testFoss/java/eu/darken/capod/common/upgrade/ui/FossUpgradeViewModelTest.kt index 6bfd37c3..86e45943 100644 --- a/app/src/testFoss/java/eu/darken/capod/common/upgrade/ui/FossUpgradeViewModelTest.kt +++ b/app/src/testFoss/java/eu/darken/capod/common/upgrade/ui/FossUpgradeViewModelTest.kt @@ -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() + 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())))