Summary
expo-iap does not ship any consumer ProGuard/R8 rules, and its android/build.gradle does not declare consumerProguardFiles. In a release build with R8 enabled (the Expo default), the subscription replacement flow fails at runtime with the system message "This feature is not supported on this device" — no build error, no exception, and it does not reproduce in debug.
Adding a -keep for the Billing API in the app's ProGuard file fixes it. Since this affects every consumer that ships a minified release with subscription replacement, the rule arguably belongs in the library.
Environment
|
|
| expo-iap |
5.0.1 (first hit) and 5.2.2 (still reproduces) |
| openiap-google |
3.0.1 → 3.2.2 |
| Google Play Billing |
9.1.0 |
| Expo SDK |
57 |
| React Native |
0.86 |
| Kotlin |
2.2.0 |
| Build |
Android release AAB, minifyEnabled true |
What happens
Calling requestPurchase with subscriptionProductReplacementParams (upgrade with charge-full-price, downgrade with deferred):
- debug build → replacement flow works, purchase sheet opens
- release build (R8) → the store returns "This feature is not supported on this device" before any payment sheet; no crash, no build warning
Steps to reproduce
- Expo SDK 57 app with
expo-iap, subscriptions configured with a monthly base plan
- Build a release artifact with
minifyEnabled true (Expo default for release)
- With an active subscription, trigger a tier change via
requestPurchase({ ..., subscriptionProductReplacementParams: { oldProductId, replacementMode } })
- Observe the failure above; the same code path works in debug
Workaround
This is what we currently carry in the consuming app's android/app/proguard-rules.pro:
# expo-iap/openiap-google calls newer Google Play Billing APIs through
# reflection. Keep Billing API names in release builds so R8 does not rename
# methods such as setSubscriptionProductReplacementParams.
-keep class com.android.billingclient.api.** { *; }
After that, replacement (both charge-full-price and deferred) works in release.
Suggested fix
Ship the keep rule with the library, so consumers get it automatically:
- add a
consumer-rules.pro under android/ containing the Billing keep, and
- reference it via
consumerProguardFiles 'consumer-rules.pro' in android/build.gradle
Note on the cause
I did not read the openiap-google source, so I can't confirm the mechanism — the comment in the snippet above is our own working hypothesis. The symptom is consistent with the Billing APIs being reached reflectively (R8 renames what it cannot see referenced statically), which would explain why only release builds break and why a -keep on the Billing package resolves it. If the access is in fact static, the root cause may be different — but the missing consumer rule and the reproducible release-only failure stand either way.
Happy to test a patch release against the same setup.
Summary
expo-iapdoes not ship any consumer ProGuard/R8 rules, and itsandroid/build.gradledoes not declareconsumerProguardFiles. In a release build with R8 enabled (the Expo default), the subscription replacement flow fails at runtime with the system message "This feature is not supported on this device" — no build error, no exception, and it does not reproduce in debug.Adding a
-keepfor the Billing API in the app's ProGuard file fixes it. Since this affects every consumer that ships a minified release with subscription replacement, the rule arguably belongs in the library.Environment
5.0.1(first hit) and5.2.2(still reproduces)3.0.1→3.2.29.1.0570.862.2.0minifyEnabled trueWhat happens
Calling
requestPurchasewithsubscriptionProductReplacementParams(upgrade withcharge-full-price, downgrade withdeferred):Steps to reproduce
expo-iap, subscriptions configured with amonthlybase planminifyEnabled true(Expo default for release)requestPurchase({ ..., subscriptionProductReplacementParams: { oldProductId, replacementMode } })Workaround
This is what we currently carry in the consuming app's
android/app/proguard-rules.pro:After that, replacement (both
charge-full-priceanddeferred) works in release.Suggested fix
Ship the keep rule with the library, so consumers get it automatically:
consumer-rules.prounderandroid/containing the Billing keep, andconsumerProguardFiles 'consumer-rules.pro'inandroid/build.gradleNote on the cause
I did not read the
openiap-googlesource, so I can't confirm the mechanism — the comment in the snippet above is our own working hypothesis. The symptom is consistent with the Billing APIs being reached reflectively (R8 renames what it cannot see referenced statically), which would explain why only release builds break and why a-keepon the Billing package resolves it. If the access is in fact static, the root cause may be different — but the missing consumer rule and the reproducible release-only failure stand either way.Happy to test a patch release against the same setup.