Skip to content

Narrow consumer R8 rules#3557

Draft
tonidero wants to merge 9 commits into
mainfrom
refine-consumer-proguard-rules
Draft

Narrow consumer R8 rules#3557
tonidero wants to merge 9 commits into
mainfrom
refine-consumer-proguard-rules

Conversation

@tonidero

@tonidero tonidero commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Why

The SDK's consumer Proguard rules kept the entire SDK:

-keep class com.revenuecat.** { *; }

This means that all the code of the SDK is kept and included in our customers apps, which is not great. This PR attempts to reduce what we keep to a few strategic locations that might be more dangerous. It's possible that this can be reduced further, but we are taking a conservative approach for now.

What changes

Replace the blanket keep with targeted rules covering only what R8 can't follow:

  • Enum constant namesEnumDeserializerWithDefault matches JSON against enum constant names read reflectively and silently falls back to a default on mismatch, so an obfuscated name would be a silent wrong-value bug, not a crash.
  • Galaxy reflective constructor (feature/galaxy) — GalaxyBillingWrapper is instantiated via Class.forName + getDeclaredConstructor.
  • PaywallKt existence probe (ui/revenuecatui) — canUsePaywallUI checks for the class via Class.forName.

Additionally, for EnumDeserializerWithDefault, we had to manually pass an individual name for each serializer.

Validation

R8 only runs in a consuming app, so the Maestro e2e job now builds and installs the minified release APK (test-apps/e2etests, debug-signed to avoid release secrets) — the purchase → entitlement-verify flow exercises these rules end-to-end.

In order to use release in maestro builds, we had to add an option to bypass the test store release mode check. Currently behind an internal API.

@tonidero tonidero changed the title Narrow consumer R8 rules to keep only what R8 cannot trace Narrow consumer R8 rules Jun 8, 2026
The consumer rules kept the entire SDK via `-keep class com.revenuecat.** { *; }`,
forcing every consuming app's R8 to retain all classes and members and blocking
shrinking/obfuscation of dead code and internals.

Replace it with conservative, targeted keeps: stable public API names
(`-keepnames`), all `@Serializable` models (kotlinx + custom KSerializers), enum
constant names (the EnumDeserializerWithDefault reflective name match fails
silently), the Galaxy reflective constructor, and the PaywallKt existence probe.

Because R8 only runs in a consuming app, the Maestro e2e job now builds and
installs the minified release APK so the purchase flow exercises these rules
end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tonidero
tonidero force-pushed the refine-consumer-proguard-rules branch from b7e3392 to a5a60e9 Compare June 8, 2026 14:00
The Maestro e2e job now runs against a minified release build, which is
non-debuggable and trips the guard in PurchasesFactory that blocks Test Store
keys in release builds (it shows SimulatedStoreErrorDialogActivity, crashing
the app on launch). uiPreviewMode bypasses the guard but disables billing, so
it can't drive the real purchase flow.

Add an internal opt-in, DangerousSettings.forTestStoreInReleaseBuild()
(@InternalRevenueCatAPI, mirroring forPreviewMode()), that the e2etests app
sets so it can use the Test Store in a true release build. Extend the guard to
honor it. For RevenueCat-internal testing only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@tonidero
tonidero force-pushed the refine-consumer-proguard-rules branch from b4f50aa to 24ee300 Compare June 8, 2026 15:01

@JayShortway JayShortway left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost can't believe it 😭

Comment thread purchases/consumer-rules.pro Outdated
Comment thread purchases/consumer-rules.pro Outdated
Comment thread ui/revenuecatui/consumer-rules.pro
@codecov

codecov Bot commented Jun 8, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.11765% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.29%. Comparing base (34168d5) to head (382d4e2).

Files with missing lines Patch % Lines
...tlin/com/revenuecat/purchases/DangerousSettings.kt 80.00% 1 Missing ⚠️
...otlin/com/revenuecat/purchases/PurchasesFactory.kt 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3557      +/-   ##
==========================================
+ Coverage   80.26%   80.29%   +0.03%     
==========================================
  Files         378      378              
  Lines       15448    15477      +29     
  Branches     2143     2145       +2     
==========================================
+ Hits        12400    12428      +28     
- Misses       2189     2190       +1     
  Partials      859      859              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

}

internal object WorkflowTriggerTypeDeserializer : EnumDeserializerWithDefault<WorkflowTriggerType>(
serialName = "com.revenuecat.purchases.common.workflows.WorkflowTriggerType",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So basically we need each deserializer to have a different name, and after minifiying, the previous strategy didn't work so I'm now passing the FQN as a parameter... In theory, any different string could work, but with the FQN we make sure to avoid duplicates.

*/
@InternalRevenueCatAPI
@JvmStatic
public fun forTestStoreInReleaseBuild(): DangerousSettings = DangerousSettings(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do wonder if we would want to end up making this public... But for now, kept it internal

Comment thread ui/revenuecatui/consumer-rules.pro
tonidero added a commit to RevenueCat/purchases-unity that referenced this pull request Jun 11, 2026
We're working to narrow down our consumer rules in
RevenueCat/purchases-android#3557. However, this
makes it not find our code anymore in Unity when minifying since nothing
is directly referencing the Java types from Unity. This adds some Keep
annotations that from my tests avoid the issue. This does mean we will
need to remember to mark all java types as `@Keep` but it was easier
than trying to add proguard rules to be used by our customers when
importing our plugins.

Note this won't do anything until the purchases-android changes actually
get merged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants