Skip to content

expo-iap ships no consumer ProGuard rules — subscription replacement fails silently in release builds #307

Description

@acpmarcodev

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

  1. Expo SDK 57 app with expo-iap, subscriptions configured with a monthly base plan
  2. Build a release artifact with minifyEnabled true (Expo default for release)
  3. With an active subscription, trigger a tier change via requestPurchase({ ..., subscriptionProductReplacementParams: { oldProductId, replacementMode } })
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    expo-iapexpo-iap library🐛 bugSomething isn't working🤖 androidRelated to android

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions