Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #3234 +/- ##
==========================================
+ Coverage 79.39% 79.43% +0.04%
==========================================
Files 356 357 +1
Lines 14344 14387 +43
Branches 1959 1966 +7
==========================================
+ Hits 11389 11429 +40
- Misses 2151 2152 +1
- Partials 804 806 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
📸 Snapshot Test582 unchanged
🛸 Powered by Emerge Tools |
| } | ||
| } | ||
|
|
||
| internal object ReplacementModeSerializer : KSerializer<ReplacementMode> { |
There was a problem hiding this comment.
This PR updates ReplacementModeSerializer to be asymmetric to account for the migration from GoogleReplacementMode -> StoreReplacementMode.
For serialization, it always serializes to StoreReplacementMode, even if the object is a GoogleReplacementMode.
For deserialization, it supports both GoogleReplacementMode and StoreReplacementMode as inputs to maintain backwards compatibility, but always deserializes to a StoreReplacementMode.
| StoreReplacementMode.CHARGE_PRORATED_PRICE -> HelperDefine.ProrationMode.INSTANT_PRORATED_CHARGE | ||
| StoreReplacementMode.DEFERRED -> HelperDefine.ProrationMode.DEFERRED | ||
| StoreReplacementMode.CHARGE_FULL_PRICE -> { | ||
| val message = GalaxyStrings.CHARGE_FULL_PRICE_NOT_SUPPORTED |
There was a problem hiding this comment.
If StoreReplacementMode.CHARGE_FULL_PRICE is provided to the Galaxy store for a product change, we return an error, since there is no equivalent of this mode for the Galaxy Store.
We could gracefully degrade to a default here, but I thought it might be best to fail since the request can't be executed as specified. What do y'all think?
| * For Google Play, use [com.revenuecat.purchases.models.GoogleReplacementMode]. | ||
| * Use [com.revenuecat.purchases.models.StoreReplacementMode]. | ||
| */ | ||
| public fun replacementMode(replacementMode: ReplacementMode?): Builder = apply { |
There was a problem hiding this comment.
I don't believe this is technically a breaking change since .replacementMode() uses ReplacementMode
tonidero
left a comment
There was a problem hiding this comment.
Great work! My only comment is whether we should try to avoid using enums for the new type now that we have the chance. Lmk what you think!
| return mapping.legacyPlayBackendName | ||
| } | ||
|
|
||
| internal fun StoreReplacementMode.storeBackendName(store: Store): String? { |
There was a problem hiding this comment.
Could have been nice to try to consolidate these in the backend... But that should be a different PR.
| * Enum of possible replacement modes to be used when performing a subscription product change. | ||
| */ | ||
| @Parcelize | ||
| public enum class StoreReplacementMode : ReplacementMode { |
There was a problem hiding this comment.
I'm thinking... should we take the chance to make this not an enum? Since we want to avoid enums/sealed classes in public APIs when possible.... The approach we've been folllowing can be seen for example with the CustomVariableValue type. It would mean we can't have exhaustive when-else statements... but I think that's exactly the point, for public APIs 😅 . Wdyt?
There was a problem hiding this comment.
If this is the direction we're trying to take our APIs, I'm more than happy to go with the pattern! Will update the PR to use an abstract class like CustomVariableValue does :)
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
| val productId = | ||
| if (replaceProductInfo?.replacementMode == GoogleReplacementMode.DEFERRED) { | ||
| if (replaceProductInfo?.replacementMode == StoreReplacementMode.DEFERRED || | ||
| replaceProductInfo?.replacementMode == GoogleReplacementMode.DEFERRED |
There was a problem hiding this comment.
Inconsistent replacement mode handling in BillingWrapper DEFERRED check
Low Severity
The DEFERRED mode check on lines 300–301 defensively handles both StoreReplacementMode.DEFERRED and GoogleReplacementMode.DEFERRED, but the PurchaseContext assignment on line 326 uses as? StoreReplacementMode?, which silently drops GoogleReplacementMode to null. If a GoogleReplacementMode.DEFERRED were to reach this code, the callback would be correctly routed to the old product ID, but the replacement mode stored on the resulting StoreTransaction (and sent to the backend) would be null. The two locations are inconsistent in their handling of the deprecated type.


Motivation
With the introduction of the Galaxy Store, we needed to find a way to gracefully model our replacement mode APIs for product changes, now that we have two stores in the Android SDK that support product changes with replacement modes.
Our current replacement mode APIs are store-specific. This allows you to tailor the replacement modes to each store, but puts the onus on the developer to maintain different logic for each store to call the appropriate
PurchaseParams.Builderfunction to set the replacement mode for that store.Description
This PR introduces a new unified
StoreReplacementModeenum that can be used across both the Play Store and the Galaxy Store. It replaces the existingGoogleReplacementModeandGalaxyReplacementModeenums. It has the same fields as the existingGoogleReplacementMode:The following table documents how each
StoreReplacementModemaps to each store:Additionally, this PR deprecates the existing
GoogleReplacementModeenum and associated public APIs and hard-removes the experimentalGalaxyReplacementModeenum and its APIs.Under the hood, it also updates the product change flows for both the Galaxy and Play Stores to use the
StoreReplacementModeenum instead of the store-specific enums.API Changes
Additions
StoreReplacementModeenumPurchaseParams.replacementMode: StoreReplacementMode(defaults to WITHOUT_PRORATION)PurchaseParams.Builder.replacementMode(replacementMode: StoreReplacementMode)Deprecations
GoogleReplacementMode- useStoreReplacementModeinsteadPurchaseParams.googleReplacementMode- usePurchaseParams.replacementModeinsteadPurchaseParams.Builder.googleReplacementMode- usePurchaseParams.Builder.replacementModeinsteadPurchaseParams.Builder.googleReplacementMode(googleReplacementMode: GoogleReplacementMode)- usePurchaseParams.Builder.replacementMode(replacementMode: StoreReplacementMode)insteadHard Removals
These are all currently marked as
@ExperimentalPreviewRevenueCatPurchasesAPI:GalaxyReplacementMode- useStoreReplacementModeinsteadPurchaseParams.galaxyReplacementMode- usePurchaseParams.replacementModeinsteadPurchaseParams.Builder.galaxyReplacementMode- usePurchaseParams.Builder.replacementModeinsteadPurchaseParams.Builder.galaxyReplacementMode(galaxyReplacementMode: GalaxyReplacementMode)- usePurchaseParams.Builder.replacementMode(replacementMode: StoreReplacementMode)insteadPaywalls Changes
This PR includes minor changes to the paywalls product to account for this change, primarily in how product changes and custom purchase logic are handled.
Testing
StoreReplacementAPIworkStoreReplacementAPI.CHARGE_FULL_PRICEreturns an unsupported errorGoogleReplacementAPIstill workStoreReplacementAPIworkNote
Medium Risk
Introduces a new unified replacement-mode API and rewires Play/Galaxy product-change flows and backend payload mapping, which could affect upgrade/downgrade behavior and receipt posting across stores. Risk is mitigated by extensive test updates but touches core purchase paths.
Overview
Unifies subscription product-change replacement modes across stores by introducing
StoreReplacementModeand a newPurchaseParams.Builder.replacementMode(...)/PurchaseParams.replacementModefield, while hard-removing experimentalGalaxyReplacementModeAPIs and deprecatingGoogleReplacementModeand relatedPurchaseParamsaccessors.Updates Play Store and Galaxy Store purchase/product-change implementations to consume
StoreReplacementMode, including store-specific conversions (Play Billing mode, Galaxy proration mode) and store-aware backend serialization forproration_mode.Adjusts sample/test harnesses and paywall product-change config/serializers to use
StoreReplacementMode, and refreshes unit tests to cover new defaults, mapping behavior, deferred product-change handling, and Galaxy unsupported-mode errors (notablyCHARGE_FULL_PRICE).Written by Cursor Bugbot for commit 065832a. This will update automatically on new commits. Configure here.