Skip to content

Commit 16e66eb

Browse files
author
Fix Bot
committed
Fix: retry onFreedomPurchaseStateChanged delivery to survive WebView-not-ready race
Root cause found: evaluateWebViewJs() silently no-ops if webViewInstance is null or the page/JS context isn't fully attached at that exact moment (webViewInstance?.post{...} just does nothing). The Freedom purchase callback was previously fired exactly once, right when Google Play's billing callback returns - often right as the Activity is still transitioning back from the Play billing bottom sheet, i.e. exactly when the WebView is least likely to be reliably attached. If that single delivery was dropped, nothing resent it: queryActiveSubscriptions() on the next onResume only re-sends the callback when '!TrialManager.isFreedomPurchased(this)' - but the native flag was already set to true by the original (dropped) handlePurchase() call, so that guard silently prevented any retry. The WebView was then stuck showing the Pro-only UI until a full app restart, where onAndroidReady's parameterless updateDonationCard() finally read the correct native state fresh. Fix: new evaluateWebViewJsWithRetry() re-sends the same JS up to 5 times over ~2 seconds. All 4 call sites for onFreedomPurchaseStateChanged now use it, so a dropped first delivery is very likely followed by a successful one shortly after, without requiring the user to restart the app.
1 parent 476e689 commit 16e66eb

1 file changed

Lines changed: 25 additions & 4 deletions

File tree

app/src/main/kotlin/com/google/ai/sample/MainActivity.kt

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,7 +1297,7 @@ class MainActivity : ComponentActivity() {
12971297
TrialManager.markAsFreedomPurchased(this)
12981298
TrialManager.markAsPurchased(this)
12991299
updateTrialState(TrialManager.getTrialState(this, null))
1300-
evaluateWebViewJs("window.onFreedomPurchaseStateChanged && window.onFreedomPurchaseStateChanged(true)")
1300+
evaluateWebViewJsWithRetry("window.onFreedomPurchaseStateChanged && window.onFreedomPurchaseStateChanged(true)")
13011301
Log.d(TAG, "handlePurchase Freedom: Stopping TrialTimerService.")
13021302
val stopIntent = Intent(this, TrialTimerService::class.java)
13031303
stopIntent.action = TrialTimerService.ACTION_STOP_TIMER
@@ -1313,7 +1313,7 @@ class MainActivity : ComponentActivity() {
13131313
TrialManager.markAsFreedomPurchased(this)
13141314
TrialManager.markAsPurchased(this)
13151315
updateTrialState(TrialManager.getTrialState(this, null))
1316-
evaluateWebViewJs("window.onFreedomPurchaseStateChanged && window.onFreedomPurchaseStateChanged(true)")
1316+
evaluateWebViewJsWithRetry("window.onFreedomPurchaseStateChanged && window.onFreedomPurchaseStateChanged(true)")
13171317
}
13181318
} else {
13191319
Log.w(TAG, "handlePurchase: Purchase is PURCHASED but does not contain any known product ID. Products: ${purchase.products}")
@@ -1372,7 +1372,7 @@ class MainActivity : ComponentActivity() {
13721372
TrialManager.markAsFreedomPurchased(this)
13731373
TrialManager.markAsPurchased(this)
13741374
updateTrialState(TrialManager.getTrialState(this, null))
1375-
evaluateWebViewJs("window.onFreedomPurchaseStateChanged && window.onFreedomPurchaseStateChanged(true)")
1375+
evaluateWebViewJsWithRetry("window.onFreedomPurchaseStateChanged && window.onFreedomPurchaseStateChanged(true)")
13761376
}
13771377
val stopIntent = Intent(this, TrialTimerService::class.java)
13781378
stopIntent.action = TrialTimerService.ACTION_STOP_TIMER
@@ -1395,7 +1395,7 @@ class MainActivity : ComponentActivity() {
13951395
if (TrialManager.isFreedomPurchased(this@MainActivity)) {
13961396
Log.w(TAG, "queryActiveSubscriptions: No active Freedom subscription found by Google Play Billing, but was previously marked. Clearing Freedom mark.")
13971397
TrialManager.clearFreedomMark(this@MainActivity)
1398-
evaluateWebViewJs("window.onFreedomPurchaseStateChanged && window.onFreedomPurchaseStateChanged(false)")
1398+
evaluateWebViewJsWithRetry("window.onFreedomPurchaseStateChanged && window.onFreedomPurchaseStateChanged(false)")
13991399
}
14001400
if (TrialManager.isPurchased(this@MainActivity)) {
14011401
Log.w(TAG, "queryActiveSubscriptions: No active subscription found by Google Play Billing, but app was previously marked as purchased. Clearing purchase mark.")
@@ -1701,6 +1701,27 @@ class MainActivity : ComponentActivity() {
17011701
}
17021702
}
17031703

1704+
/**
1705+
* Like [evaluateWebViewJs], but re-sends the same JS a few times with a delay in between.
1706+
*
1707+
* Purchase-completion callbacks (e.g. window.onFreedomPurchaseStateChanged) were previously
1708+
* sent exactly once, right when Google Play's billing callback fires. If webViewInstance was
1709+
* null or the page/JS wasn't fully attached yet at that exact moment (e.g. because the
1710+
* Activity was still transitioning back from the Play billing sheet), the single
1711+
* evaluateJavascript call was silently dropped and the WebView kept showing the old
1712+
* (pre-purchase) state until the next full app restart, even though the native purchase
1713+
* flag was already set correctly. Retrying a few times over a couple of seconds makes
1714+
* delivery robust without needing a full native rewrite of the purchase flow.
1715+
*/
1716+
fun evaluateWebViewJsWithRetry(js: String, attempts: Int = 5, delayMs: Long = 400L) {
1717+
lifecycleScope.launch {
1718+
repeat(attempts) { attempt ->
1719+
evaluateWebViewJs(js)
1720+
if (attempt < attempts - 1) kotlinx.coroutines.delay(delayMs)
1721+
}
1722+
}
1723+
}
1724+
17041725
/**
17051726
* Escapes a string so it can be safely embedded inside a single-quoted JS string literal
17061727
* passed to [WebView.evaluateJavascript]. Delegates to [WebViewBridge.jsEscape] so both

0 commit comments

Comments
 (0)