fix(upgrade): Only count a sponsor visit when the page actually opened

The sponsor unlock heuristic armed itself on every tap, regardless of
whether a browser ever opened the page. An unrelated later background
round-trip could then hand out supporter status with no page ever shown.

WebpageTool.open() now reports whether an activity was actually started
and the FOSS repo passes that through synchronously, so the ViewModel can
only arm after a successful launch. A second tap while a launch is still
pending is ignored, and the upgraded status view's donate button gets its
own unarmed entry point - an existing supporter has nothing left to
unlock, and re-persisting would rewrite their "supporter since" date.
This commit is contained in:
darken
2026-08-02 12:40:32 +02:00
committed by Matthias Urhahn
parent 7cb31ce783
commit 31c3b47bcd
8 changed files with 169 additions and 9 deletions
@@ -13,7 +13,6 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import java.time.Instant
import java.util.UUID
import javax.inject.Inject
@@ -49,9 +48,11 @@ class UpgradeRepoFoss @Inject constructor(
.setupCommonEventHandlers(TAG) { "upgradeInfo" }
.shareIn(appScope, SharingStarted.WhileSubscribed(3000L, 0L), replay = 1)
fun openGithubSponsorsPage() = appScope.launch {
// Synchronous so the caller learns whether the page actually opened: the FOSS unlock heuristic
// only arms on a successful launch, and a fire-and-forget coroutine can't report that back.
fun openGithubSponsorsPage(): Boolean {
log(TAG) { "openGithubSponsorsPage()" }
webpageTool.open(upgradeSite)
return webpageTool.open(upgradeSite)
}
// Writes capod's RETAINED persistence schema: existing supporter records are serialized with
@@ -95,6 +95,7 @@ fun UpgradeScreenHost(
supporterSince = state?.supporterSince,
snackbarHostState = snackbarHostState,
onGithubSponsors = vm::goGithubSponsors,
onOpenSponsors = vm::openSponsors,
onShowUpgradeOptions = vm::onShowUpgradeOptions,
onNavigateUp = vm::navUp,
)
@@ -106,6 +107,7 @@ internal fun UpgradeScreen(
supporterSince: Instant? = null,
snackbarHostState: SnackbarHostState = remember { SnackbarHostState() },
onGithubSponsors: () -> Unit = {},
onOpenSponsors: () -> Unit = {},
onShowUpgradeOptions: () -> Unit = {},
onNavigateUp: () -> Unit = {},
) {
@@ -140,7 +142,7 @@ internal fun UpgradeScreen(
FossUpgradeView.STATUS_UPGRADED -> UpgradeStatusUpgradedContent(
paddingValues = paddingValues,
supporterSince = supporterSince,
onGithubSponsors = onGithubSponsors,
onOpenSponsors = onOpenSponsors,
)
}
}
@@ -234,7 +236,7 @@ private fun UpgradeStatusFreeContent(
private fun UpgradeStatusUpgradedContent(
paddingValues: PaddingValues,
supporterSince: Instant? = null,
onGithubSponsors: () -> Unit,
onOpenSponsors: () -> Unit,
) {
UpgradeScreenContent(
paddingValues = paddingValues,
@@ -273,7 +275,7 @@ private fun UpgradeStatusUpgradedContent(
) {
UpgradeSectionBody(text = stringResource(R.string.upgrade_screen_recurring_body))
OutlinedButton(
onClick = onGithubSponsors,
onClick = onOpenSponsors,
modifier = Modifier
.fillMaxWidth()
.testTag(UpgradeScreenTags.FOSS_DONATE),
@@ -106,9 +106,28 @@ class UpgradeViewModel @Inject constructor(
handle[KEY_SHOW_UPGRADE_OPTIONS] = true
}
/** Armed variant: the pitch's sponsor button, which starts the return-after-5s unlock heuristic. */
fun goGithubSponsors() {
log(TAG) { "goGithubSponsors()" }
if (hasPendingSponsorLaunch()) {
log(TAG) { "A sponsor launch is already awaiting its return" }
return
}
// Only arm the heuristic if the page actually opened; otherwise an unrelated later
// background/foreground round-trip would grant supporter status with no page ever shown.
if (!upgradeRepo.openGithubSponsorsPage()) {
log(TAG) { "Sponsor page didn't open; not arming the unlock heuristic" }
return
}
handle[KEY_SPONSOR_PRESSED_AT] = SystemClock.elapsedRealtime()
}
/**
* Unarmed variant: the status view's donate button. An existing supporter re-visiting the page
* must not re-arm the unlock heuristic — there is nothing left to unlock.
*/
fun openSponsors() {
log(TAG) { "openSponsors()" }
upgradeRepo.openGithubSponsorsPage()
}