Artifact: com.chargebee:chargebee-android:2.0.0-beta-6 (Maven Central, published 2026-08-21)
Introduced by: PR #124 "Upgrade Google billing library to 8.3.0" (merged 2026-08-14)
Play Billing: com.android.billingclient:billing:8.3.0
Regression from: 1.2.4 (does not crash)
Severity: hard crash on the main thread, inside a BroadcastReceiver, on a monetization path
Summary
BillingClientManager.purchaseProductParams is a lateinit property that is written in exactly
one place — BillingClientManager.purchase(PurchaseProductParams) — but is dereferenced
unguarded by both acknowledgePurchase(Purchase) and notifyPurchaseError(CBException), which
are reachable from onPurchasesUpdated.
PurchasesUpdatedListener.onPurchasesUpdated is an out-of-band callback: Google Play delivers
purchase updates that the SDK did not initiate. Whenever that happens before (or without) a
Chargebee-initiated purchase in the same BillingClientManager instance, the SDK throws
UninitializedPropertyAccessException from inside Play's broadcast receiver, which Android
escalates to a fatal RuntimeException.
This is made much more likely in beta-6 by the newly added enableAutoServiceReconnection(), which
keeps the SDK's BillingClient connected — and therefore subscribed to purchase broadcasts —
indefinitely.
Stack trace
FATAL EXCEPTION: main
Process: com.onestream.live.development, PID: 18946
java.lang.RuntimeException: Error receiving broadcast Intent { act=com.android.vending.billing.LOCAL_BROADCAST_PURCHASES_UPDATED flg=0x10 pkg=com.onestream.live.development (has extras) } in com.android.billingclient.api.zzaa@ba2effc
at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0(LoadedApk.java:1822)
at android.app.LoadedApk$ReceiverDispatcher$Args.$r8$lambda$mcNAAl1SQ4MyJPyDg8TJ2x2h0Rk(LoadedApk.java:0)
at android.app.LoadedApk$ReceiverDispatcher$Args$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
at android.os.Handler.handleCallback(Handler.java:959)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loopOnce(Looper.java:232)
at android.os.Looper.loop(Looper.java:317)
at android.app.ActivityThread.main(ActivityThread.java:8705)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:580)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:886)
Caused by: kotlin.UninitializedPropertyAccessException: lateinit property purchaseProductParams has not been initialized
at com.chargebee.android.billingservice.BillingClientManager.acknowledgePurchase(BillingClientManager.kt:358)
at com.chargebee.android.billingservice.BillingClientManager.onPurchasesUpdated(BillingClientManager.kt:314)
at com.android.billingclient.api.zzaa.onReceive(com.android.billingclient:billing@@8.3.0:38)
at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0(LoadedApk.java:1814)
... 11 more
Bytecode evidence
All offsets from javap -p -c on classes.jar inside the published
chargebee-android-2.0.0-beta-6.aar.
1. onPurchasesUpdated routes PURCHASED straight into acknowledgePurchase
public void onPurchasesUpdated(BillingResult, List<Purchase>);
63: invokevirtual Purchase.getPurchaseState:()I
68: tableswitch { 0: 145, 1: 96, 2: 105, default: 182 }
96: aload_0
97: aload 7
99: invokespecial acknowledgePurchase:(Lcom/android/billingclient/api/Purchase;)V
No check that this SDK initiated the purchase.
2. acknowledgePurchase dereferences the lateinit immediately
private final void acknowledgePurchase(Purchase);
0: aload_0
1: getfield purchaseProductParams:Lcom/chargebee/android/models/PurchaseProductParams;
4: dup
5: ifnonnull 16
8: pop
9: ldc_w "purchaseProductParams"
12: invokestatic Intrinsics.throwUninitializedPropertyAccessException:(Ljava/lang/String;)V // <-- crash
15: aconst_null
16: invokevirtual PurchaseProductParams.getProduct:()Lcom/chargebee/android/models/CBProduct;
19: invokevirtual CBProduct.getType:()Lcom/chargebee/android/billingservice/ProductType;
3. notifyPurchaseError has the identical unguarded deref
So the error path crashes too, meaning there is no safe way for the SDK to report this condition:
private final void notifyPurchaseError(CBException);
0: aload_0
1: getfield purchaseProductParams:...
5: ifnonnull 16
12: invokestatic Intrinsics.throwUninitializedPropertyAccessException // <-- crash
16: invokevirtual PurchaseProductParams.getProduct:()...
4. Exactly one writer, four readers
putfield purchaseProductParams -> 1 site, inside private final void purchase(PurchaseProductParams)
getfield purchaseProductParams -> 4 sites
So the field is only ever initialised by an SDK-initiated purchase, while the readers are reachable
from an out-of-band Play broadcast.
Why this is not specific to one integration
onPurchasesUpdated legitimately fires without a preceding SDK-initiated purchase in at least these
cases, all of which should crash:
- a subscription renewal delivered while the app is in the foreground
- a promo code redeemed in the Play Store app
- a PENDING purchase completing later (deferred payment methods)
- a purchase syncing from another device on the same Google account
- any purchase started by a second
BillingClient in the same process — Play broadcasts
LOCAL_BROADCAST_PURCHASES_UPDATED to every registered client
Our own reproduction is the last one: we run a second BillingClient for subscription
replacement (upgrade/downgrade proration), because the Chargebee SDK has no replacement API —
BillingFlowParams.SubscriptionUpdateParams.setOldPurchaseToken and
setSubscriptionReplacementMode have zero references in beta-6. So an app that supports plan
changes must run a second client, and then every plan change crashes.
Reproduction
- Integrate
chargebee-android:2.0.0-beta-6.
- Buy a subscription via
CBPurchase.purchaseProduct (succeeds).
- Complete a purchase that this SDK did not initiate — easiest is a second
BillingClient in
the same process performing a subscription replacement, but a promo-code redemption or a renewal
arriving while the app is open should also do it.
- Play broadcasts the update,
onPurchasesUpdated fires, and the app crashes as above.
Regression detail vs 1.2.4
Two changes combine to make this reachable where 1.2.4 was safe.
(a) 1.2.4 used a plain nullable field, not lateinit.
1.2.4: public com.chargebee.android.models.CBProduct product; // + getProduct()/setProduct()
beta-6: private com.chargebee.android.models.PurchaseProductParams purchaseProductParams; // lateinit
A null field degraded silently; the lateinit throws.
(b) isAcknowledgedPurchase no longer short-circuits on an already-acknowledged purchase.
1.2.4 isAcknowledgedPurchase @0-4:
1: invokevirtual Purchase.isAcknowledged:()Z
4: ifne 54 // already acknowledged -> jump to end, DO NOTHING
beta-6 isAcknowledgedPurchase @0-26:
1: invokevirtual Purchase.isAcknowledged:()Z
4: ifeq 27
11: ldc_w "Google Purchase - already acknowledged"
14: invokestatic Log.i
23: invokespecial onAcknowledged:(Purchase;I;Function0;Function1;)V // proceeds to validateReceipt
26: return
This looks like the intended "fixing stalled acknowledgment logic" change in PR #124, and in
isolation it is an improvement. But combined with (a) it means an externally-acknowledged purchase
now walks into onAcknowledged → validateReceipt, using whatever purchaseProductParams happens
to hold. In our app that produces a second, quieter bug: when a Chargebee purchase did occur
earlier in the session, the SDK silently validates the wrong product against Chargebee and
surfaces BillingErrorCode.SERVICE_UNAVAILABLE ("The service is currently unavailable") to the
user after a payment that actually succeeded.
Suggested fix
Guard both readers, e.g.:
private fun acknowledgePurchase(purchase: Purchase) {
if (!::purchaseProductParams.isInitialized) {
// Purchase not initiated by this SDK instance (renewal, promo code, pending
// completion, other BillingClient). Acknowledge without product-typed handling,
// or ignore.
return
}
...
}
More robustly, onPurchasesUpdated could correlate the incoming Purchase.products against the
product the SDK actually launched, and ignore updates it did not initiate — that also prevents the
wrong-product validation described above.
Making purchaseProductParams a nullable var rather than lateinit would restore 1.2.4's
failure-tolerance as a minimum.
Additional note
2.0.0-beta-6 is on Maven Central but has no git tag, no GitHub release, and no CHANGELOG
entry — the newest tag is 2.0.0-beta-5. Since it is currently the only Chargebee Android build
on Play Billing 8, and Google requires Billing ≥ 8.0.0 for app updates from 31 Aug 2026,
integrators are being pushed onto an artifact with no release notes. A tagged release (and a
follow-up with this fix) would help a lot.
Environment
|
|
| chargebee-android |
2.0.0-beta-6 |
| Play Billing |
8.3.0 |
| React Native wrapper |
@chargebee/react-native-chargebee 2.5.2 (patched for the beta-6 API) |
| App |
React Native 0.79.7, Kotlin 2.0.21, AGP 8.8.2, minSdk 24, targetSdk 35 |
| Build |
debuggable development flavor (com.onestream.live.development), R8 disabled |
| Device |
Android 15 (confirm/replace with the exact OS version and model) |
Artifact:
com.chargebee:chargebee-android:2.0.0-beta-6(Maven Central, published 2026-08-21)Introduced by: PR #124 "Upgrade Google billing library to 8.3.0" (merged 2026-08-14)
Play Billing:
com.android.billingclient:billing:8.3.0Regression from:
1.2.4(does not crash)Severity: hard crash on the main thread, inside a
BroadcastReceiver, on a monetization pathSummary
BillingClientManager.purchaseProductParamsis alateinitproperty that is written in exactlyone place —
BillingClientManager.purchase(PurchaseProductParams)— but is dereferencedunguarded by both
acknowledgePurchase(Purchase)andnotifyPurchaseError(CBException), whichare reachable from
onPurchasesUpdated.PurchasesUpdatedListener.onPurchasesUpdatedis an out-of-band callback: Google Play deliverspurchase updates that the SDK did not initiate. Whenever that happens before (or without) a
Chargebee-initiated purchase in the same
BillingClientManagerinstance, the SDK throwsUninitializedPropertyAccessExceptionfrom inside Play's broadcast receiver, which Androidescalates to a fatal
RuntimeException.This is made much more likely in beta-6 by the newly added
enableAutoServiceReconnection(), whichkeeps the SDK's
BillingClientconnected — and therefore subscribed to purchase broadcasts —indefinitely.
Stack trace
Bytecode evidence
All offsets from
javap -p -conclasses.jarinside the publishedchargebee-android-2.0.0-beta-6.aar.1.
onPurchasesUpdatedroutesPURCHASEDstraight intoacknowledgePurchaseNo check that this SDK initiated the purchase.
2.
acknowledgePurchasedereferences thelateinitimmediately3.
notifyPurchaseErrorhas the identical unguarded derefSo the error path crashes too, meaning there is no safe way for the SDK to report this condition:
4. Exactly one writer, four readers
So the field is only ever initialised by an SDK-initiated purchase, while the readers are reachable
from an out-of-band Play broadcast.
Why this is not specific to one integration
onPurchasesUpdatedlegitimately fires without a preceding SDK-initiated purchase in at least thesecases, all of which should crash:
BillingClientin the same process — Play broadcastsLOCAL_BROADCAST_PURCHASES_UPDATEDto every registered clientOur own reproduction is the last one: we run a second
BillingClientfor subscriptionreplacement (upgrade/downgrade proration), because the Chargebee SDK has no replacement API —
BillingFlowParams.SubscriptionUpdateParams.setOldPurchaseTokenandsetSubscriptionReplacementModehave zero references in beta-6. So an app that supports planchanges must run a second client, and then every plan change crashes.
Reproduction
chargebee-android:2.0.0-beta-6.CBPurchase.purchaseProduct(succeeds).BillingClientinthe same process performing a subscription replacement, but a promo-code redemption or a renewal
arriving while the app is open should also do it.
onPurchasesUpdatedfires, and the app crashes as above.Regression detail vs 1.2.4
Two changes combine to make this reachable where 1.2.4 was safe.
(a) 1.2.4 used a plain nullable field, not
lateinit.A null field degraded silently; the
lateinitthrows.(b)
isAcknowledgedPurchaseno longer short-circuits on an already-acknowledged purchase.This looks like the intended "fixing stalled acknowledgment logic" change in PR #124, and in
isolation it is an improvement. But combined with (a) it means an externally-acknowledged purchase
now walks into
onAcknowledged→validateReceipt, using whateverpurchaseProductParamshappensto hold. In our app that produces a second, quieter bug: when a Chargebee purchase did occur
earlier in the session, the SDK silently validates the wrong product against Chargebee and
surfaces
BillingErrorCode.SERVICE_UNAVAILABLE("The service is currently unavailable") to theuser after a payment that actually succeeded.
Suggested fix
Guard both readers, e.g.:
More robustly,
onPurchasesUpdatedcould correlate the incomingPurchase.productsagainst theproduct the SDK actually launched, and ignore updates it did not initiate — that also prevents the
wrong-product validation described above.
Making
purchaseProductParamsa nullablevarrather thanlateinitwould restore 1.2.4'sfailure-tolerance as a minimum.
Additional note
2.0.0-beta-6is on Maven Central but has no git tag, no GitHub release, and no CHANGELOGentry — the newest tag is
2.0.0-beta-5. Since it is currently the only Chargebee Android buildon Play Billing 8, and Google requires Billing ≥ 8.0.0 for app updates from 31 Aug 2026,
integrators are being pushed onto an artifact with no release notes. A tagged release (and a
follow-up with this fix) would help a lot.
Environment
com.onestream.live.development), R8 disabled