Skip to content
Open
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
26157e8
add StoreReplacementMode + API tests
fire-at-will Mar 13, 2026
fe1db6e
add StoreReplacementMode -> store specific conversions + tests
fire-at-will Mar 13, 2026
49a58cb
remove Product change
fire-at-will Mar 13, 2026
6d3b024
remove GalaxyPurchasingData.Product api.txt change
fire-at-will Mar 13, 2026
3039a06
add StoreReplacementMode to api.txts
fire-at-will Mar 13, 2026
9a42704
add .replacementMode() to PurchaseParams
fire-at-will Mar 13, 2026
e1e2017
set PurchaseParams.googleReplacementMode when setting replacementMode
fire-at-will Mar 13, 2026
d067c44
detekt
fire-at-will Mar 13, 2026
bf8c82a
Merge branch 'main' into shared-replacement-mode
fire-at-will Mar 13, 2026
c302f2d
Merge branch 'main' into shared-replacement-mode
fire-at-will Mar 16, 2026
8782fcb
galaxy-specific flow updates
fire-at-will Mar 16, 2026
503e9e0
add unit tests for galaxy flow
fire-at-will Mar 16, 2026
ac40b0e
Update PurchasesOrchestrator.kt
fire-at-will Mar 16, 2026
9666938
update PurchasesCommonTest
fire-at-will Mar 16, 2026
4ecb493
Google -> StoreReplacementMode in play store flows
fire-at-will Mar 17, 2026
3448485
detekt
fire-at-will Mar 17, 2026
93e19aa
Merge branch 'main' into shared-replacement-mode
fire-at-will Mar 17, 2026
aabb558
update backend name generation to use StoreReplacementMode
fire-at-will Mar 17, 2026
da3dd6e
set replacementMode in PurchaseParams when providing googleReplacemen…
fire-at-will Mar 17, 2026
a98959f
make ReplacementModeSerializer use StoreReplacementMode
fire-at-will Mar 17, 2026
7b76f17
update PurchaseParams public APIs
fire-at-will Mar 17, 2026
1f08ad0
remove GalaxyReplacementMode
fire-at-will Mar 17, 2026
8fd6aab
actually remove GalaxyReplacementMode
fire-at-will Mar 17, 2026
0981b79
Merge branch 'main' into shared-replacement-mode
fire-at-will Mar 17, 2026
0d1859f
deprecate GoogleReplacementMode
fire-at-will Mar 17, 2026
de85b71
migrate paywalls to use StoreReplacementMode
fire-at-will Mar 17, 2026
f0312d4
send replacement mode to handleReceipt() in galaxy flow
fire-at-will Mar 17, 2026
28c8ac1
update paywall tests
fire-at-will Mar 17, 2026
31a571f
docs updates
fire-at-will Mar 18, 2026
266a2ba
default to WITHOUT_PRORATION in galaxy when it receives unsupported r…
fire-at-will Mar 18, 2026
56356cd
update paywall docs
fire-at-will Mar 18, 2026
8fb4f13
ReplacementMode serialization tweaks
fire-at-will Mar 18, 2026
a981323
centralize ReplacementMode mapping
fire-at-will Mar 18, 2026
5948a18
detekt
fire-at-will Mar 18, 2026
9993691
Merge branch 'main' into shared-replacement-mode
fire-at-will Mar 18, 2026
a7e0ef9
update PurchaseTester to use new replacementMode APIs
fire-at-will Mar 19, 2026
987bc18
Update PurchaseParamsValidatorTest.kt
fire-at-will Mar 19, 2026
ff9863c
update api testers
fire-at-will Mar 19, 2026
7985a60
update PurchaseTester to allow product changes on galaxy store
fire-at-will Mar 19, 2026
065832a
Merge branch 'main' into shared-replacement-mode
fire-at-will Apr 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
import com.revenuecat.purchases.interfaces.ReceiveOfferingsCallback;
import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener;
import com.revenuecat.purchases.models.BillingFeature;
import com.revenuecat.purchases.models.GalaxyReplacementMode;
import com.revenuecat.purchases.models.GoogleReplacementMode;
import com.revenuecat.purchases.models.InAppMessageType;
import com.revenuecat.purchases.models.StoreProduct;
Expand Down Expand Up @@ -110,14 +109,12 @@ public void onError(@NonNull PurchasesError error, boolean userCancelled) {
};
String oldProductId = "old";
GoogleReplacementMode replacementMode = GoogleReplacementMode.WITH_TIME_PRORATION;
GalaxyReplacementMode galaxyReplacementMode = GalaxyReplacementMode.INSTANT_PRORATED_CHARGE;
Boolean isPersonalizedPrice = true;

PurchaseParams.Builder purchaseProductBuilder = new PurchaseParams.Builder(activity, storeProduct);
purchaseProductBuilder
.oldProductId(oldProductId)
.googleReplacementMode(replacementMode)
.galaxyReplacementMode(galaxyReplacementMode)
.isPersonalizedPrice(isPersonalizedPrice);
PurchaseParams purchaseProductParams = purchaseProductBuilder.build();
purchases.purchase(purchaseProductParams, purchaseCallback);
Expand All @@ -126,7 +123,6 @@ public void onError(@NonNull PurchasesError error, boolean userCancelled) {
purchaseOptionBuilder
.oldProductId(oldProductId)
.googleReplacementMode(replacementMode)
.galaxyReplacementMode(galaxyReplacementMode)
.isPersonalizedPrice(isPersonalizedPrice);
PurchaseParams purchaseOptionParams = purchaseOptionBuilder.build();
purchases.purchase(purchaseOptionParams, purchaseCallback);
Expand All @@ -135,7 +131,6 @@ public void onError(@NonNull PurchasesError error, boolean userCancelled) {
purchasePackageBuilder
.oldProductId(oldProductId)
.googleReplacementMode(replacementMode)
.galaxyReplacementMode(galaxyReplacementMode)
.isPersonalizedPrice(isPersonalizedPrice);
PurchaseParams purchasePackageParams = purchasePackageBuilder.build();
purchases.purchase(purchasePackageParams, purchaseCallback);
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback
import com.revenuecat.purchases.interfaces.ReceiveOfferingsCallback
import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener
import com.revenuecat.purchases.models.BillingFeature
import com.revenuecat.purchases.models.GalaxyReplacementMode
import com.revenuecat.purchases.models.GoogleReplacementMode
import com.revenuecat.purchases.models.InAppMessageType
import com.revenuecat.purchases.models.StoreProduct
Expand Down Expand Up @@ -111,14 +110,12 @@ private class PurchasesCommonAPI {

val oldProductId = "old"
val replacementMode = GoogleReplacementMode.WITH_TIME_PRORATION
val galaxyReplacementMode = GalaxyReplacementMode.INSTANT_PRORATED_CHARGE
val isPersonalizedPrice = true

val purchasePackageBuilder: PurchaseParams.Builder = PurchaseParams.Builder(activity, packageToPurchase)
purchasePackageBuilder
.oldProductId(oldProductId)
.googleReplacementMode(replacementMode)
.galaxyReplacementMode(galaxyReplacementMode)
.isPersonalizedPrice(isPersonalizedPrice)
val purchasePackageParams: PurchaseParams = purchasePackageBuilder.build()
purchases.purchase(purchasePackageParams, purchaseCallback)
Expand All @@ -127,7 +124,6 @@ private class PurchasesCommonAPI {
purchaseProductBuilder
.oldProductId(oldProductId)
.googleReplacementMode(replacementMode)
.galaxyReplacementMode(galaxyReplacementMode)
.isPersonalizedPrice(isPersonalizedPrice)
val purchaseProductParams: PurchaseParams = purchaseProductBuilder.build()
purchases.purchase(purchaseProductParams, purchaseCallback)
Expand All @@ -136,7 +132,6 @@ private class PurchasesCommonAPI {
purchaseOptionBuilder
.oldProductId(oldProductId)
.googleReplacementMode(replacementMode)
.galaxyReplacementMode(galaxyReplacementMode)
.isPersonalizedPrice(isPersonalizedPrice)
val purchaseOptionsParams: PurchaseParams = purchaseOptionBuilder.build()
purchases.purchase(purchaseOptionsParams, purchaseCallback)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.revenuecat.apitester.kotlin

import com.revenuecat.purchases.models.StoreReplacementMode

@Suppress("unused", "UNUSED_VARIABLE")
private class StoreReplacementModeAPI {
fun check(mode: StoreReplacementMode) {
when (mode) {
StoreReplacementMode.WITHOUT_PRORATION,
StoreReplacementMode.WITH_TIME_PRORATION,
StoreReplacementMode.CHARGE_FULL_PRICE,
StoreReplacementMode.CHARGE_PRORATED_PRICE,
StoreReplacementMode.DEFERRED,
-> {}
}.exhaustive
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ import com.revenuecat.purchases.galaxy.logging.log
import com.revenuecat.purchases.galaxy.utils.GalaxySerialOperation
import com.revenuecat.purchases.galaxy.utils.SerialRequestExecutor
import com.revenuecat.purchases.galaxy.utils.parseDateFromGalaxyDateString
import com.revenuecat.purchases.models.GalaxyReplacementMode
import com.revenuecat.purchases.models.InAppMessageType
import com.revenuecat.purchases.models.PurchaseState
import com.revenuecat.purchases.models.PurchasingData
import com.revenuecat.purchases.models.StoreReplacementMode
import com.revenuecat.purchases.models.StoreTransaction
import com.revenuecat.purchases.strings.PurchaseStrings
import com.revenuecat.purchases.strings.RestoreStrings
Expand Down Expand Up @@ -336,22 +336,24 @@ internal class GalaxyBillingWrapper(

val productId = galaxyPurchaseInfo.productId

if (replaceProductInfo != null) {
val galaxyReplacementMode = replaceProductInfo.replacementMode as? GalaxyReplacementMode
?: GalaxyReplacementMode.default
replaceProductInfo?.let { replaceInfo ->
val replacementMode = when (val mode = replaceInfo.replacementMode) {
is StoreReplacementMode -> mode
else -> StoreReplacementMode.WITHOUT_PRORATION
}

serialRequestExecutor.executeSerially { finish ->
changeSubscriptionPlanHandler.changeSubscriptionPlan(
appUserID = appUserID,
oldPurchase = replaceProductInfo.oldPurchase,
oldPurchase = replaceInfo.oldPurchase,
newProductId = productId,
prorationMode = galaxyReplacementMode,
replacementMode = replacementMode,
onSuccess = { receipt ->
handleReceipt(
receipt = receipt,
productId = productId,
presentedOfferingContext = presentedOfferingContext,
replacementMode = galaxyReplacementMode,
replacementMode = replacementMode,
)
finish()
},
Expand All @@ -361,7 +363,7 @@ internal class GalaxyBillingWrapper(
},
)
}
return
return // Exits makePurchaseAsync
}

serialRequestExecutor.executeSerially { finish ->
Expand Down Expand Up @@ -390,7 +392,7 @@ internal class GalaxyBillingWrapper(
receipt: PurchaseVo,
productId: String,
presentedOfferingContext: PresentedOfferingContext?,
replacementMode: GalaxyReplacementMode?,
replacementMode: StoreReplacementMode?,
) {
try {
val storeTransaction = receipt.toStoreTransaction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,17 @@ internal object GalaxyStrings {
"the Galaxy Store is already in progress. Please wait until that request completes and then try again."
const val CHANGE_SUBSCRIPTION_PLAN_NO_OLD_PRODUCT_ID = "Cannot change subscription plan: the old purchase " +
"does not have a product ID."

const val CANNOT_CHANGE_SUBSCRIPTION_PLAN_UNSUPPORTED_REPLACEMENT_MODE = "Cannot change subscription plan: the " +
"provided replacement mode is not supported by the Galaxy Store."
const val GALAXY_STORE_FAILED_TO_ACCEPT_CHANGE_SUBSCRIPTION_PLAN_REQUEST = "The Galaxy Store did not accept " +
"the subscription plan change request for processing."
const val CHANGE_SUBSCRIPTION_PLAN_RETURNED_SUCCESS_BUT_NO_RESULT = "The subscription plan change request " +
"returned no error, but also returned no result. This is likely an issue with the Galaxy Store."
const val CHANGE_SUBSCRIPTION_PLAN_REQUEST_ERRORED = "An error occurred while changing subscription from " +
"product ID %s to %s with the Galaxy Store. Error: %s"
const val CHARGE_FULL_PRICE_NOT_SUPPORTED = "StoreReplacementMode.CHARGE_FULL_PRICE is not supported for " +
"Galaxy Store subscription changes."

// Promotion Eligibility
const val EMPTY_GET_PROMOTION_ELIGIBILITY_REQUEST = "Received a promotion eligibility request for 0 " +
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.revenuecat.purchases.galaxy.conversions

import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.PurchasesErrorCode
import com.revenuecat.purchases.PurchasesException
import com.revenuecat.purchases.galaxy.GalaxyStrings
import com.revenuecat.purchases.galaxy.logging.LogIntent
import com.revenuecat.purchases.galaxy.logging.log
import com.revenuecat.purchases.models.StoreReplacementMode
import com.samsung.android.sdk.iap.lib.constants.HelperDefine

@Throws(PurchasesException::class)
internal fun StoreReplacementMode.toGalaxyReplacementMode(): HelperDefine.ProrationMode {
return when (this) {
StoreReplacementMode.WITHOUT_PRORATION -> HelperDefine.ProrationMode.INSTANT_NO_PRORATION
StoreReplacementMode.WITH_TIME_PRORATION -> HelperDefine.ProrationMode.INSTANT_PRORATED_DATE
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
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.

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?

log(LogIntent.GALAXY_ERROR) { message }
throw PurchasesException(
PurchasesError(
PurchasesErrorCode.UnsupportedError,
message,
),
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,21 @@ import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI
import com.revenuecat.purchases.InternalRevenueCatAPI
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.PurchasesErrorCode
import com.revenuecat.purchases.PurchasesException
import com.revenuecat.purchases.common.sha256
import com.revenuecat.purchases.galaxy.GalaxyStrings
import com.revenuecat.purchases.galaxy.IAPHelperProvider
import com.revenuecat.purchases.galaxy.conversions.toSamsungProrationMode
import com.revenuecat.purchases.galaxy.conversions.toGalaxyReplacementMode
import com.revenuecat.purchases.galaxy.listener.ChangeSubscriptionPlanResponseListener
import com.revenuecat.purchases.galaxy.logging.LogIntent
import com.revenuecat.purchases.galaxy.logging.log
import com.revenuecat.purchases.galaxy.utils.GalaxySerialOperation
import com.revenuecat.purchases.galaxy.utils.isError
import com.revenuecat.purchases.galaxy.utils.toPurchasesError
import com.revenuecat.purchases.models.GalaxyReplacementMode
import com.revenuecat.purchases.models.StoreReplacementMode
import com.revenuecat.purchases.models.StoreTransaction
import com.revenuecat.purchases.strings.PurchaseStrings
import com.samsung.android.sdk.iap.lib.constants.HelperDefine
import com.samsung.android.sdk.iap.lib.vo.ErrorVo
import com.samsung.android.sdk.iap.lib.vo.PurchaseVo

Expand All @@ -35,13 +37,13 @@ internal class ChangeSubscriptionPlanHandler(
)

@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class, InternalRevenueCatAPI::class)
@Suppress("ReturnCount")
@Suppress("ReturnCount", "LongMethod")
@GalaxySerialOperation
override fun changeSubscriptionPlan(
appUserID: String,
oldPurchase: StoreTransaction,
newProductId: String,
prorationMode: GalaxyReplacementMode,
replacementMode: StoreReplacementMode,
onSuccess: (PurchaseVo) -> Unit,
onError: (PurchasesError) -> Unit,
) {
Expand All @@ -66,6 +68,17 @@ internal class ChangeSubscriptionPlanHandler(
return
}

val samsungProrationMode: HelperDefine.ProrationMode
try {
samsungProrationMode = replacementMode.toGalaxyReplacementMode()
} catch (e: PurchasesException) {
log(LogIntent.GALAXY_ERROR) {
GalaxyStrings.CANNOT_CHANGE_SUBSCRIPTION_PLAN_UNSUPPORTED_REPLACEMENT_MODE
}
onError(e.error)
return
}

inFlightRequest = Request(
newProductId = newProductId,
oldProductId = oldProductId,
Expand All @@ -83,7 +96,7 @@ internal class ChangeSubscriptionPlanHandler(
val requestWasDispatched = iapHelper.changeSubscriptionPlan(
oldItemId = oldProductId,
newItemId = newProductId,
prorationMode = prorationMode.toSamsungProrationMode(),
prorationMode = samsungProrationMode,
obfuscatedAccountId = appUserID.sha256(),
obfuscatedProfileId = null,
onChangeSubscriptionPlanListener = this,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package com.revenuecat.purchases.galaxy.listener
import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI
import com.revenuecat.purchases.PurchasesError
import com.revenuecat.purchases.galaxy.utils.GalaxySerialOperation
import com.revenuecat.purchases.models.GalaxyReplacementMode
import com.revenuecat.purchases.models.StoreReplacementMode
import com.revenuecat.purchases.models.StoreTransaction
import com.samsung.android.sdk.iap.lib.listener.OnChangeSubscriptionPlanListener
import com.samsung.android.sdk.iap.lib.vo.ErrorVo
Expand All @@ -22,7 +22,7 @@ internal interface ChangeSubscriptionPlanResponseListener : OnChangeSubscription
appUserID: String,
oldPurchase: StoreTransaction,
newProductId: String,
prorationMode: GalaxyReplacementMode,
replacementMode: StoreReplacementMode,
onSuccess: (PurchaseVo) -> Unit,
onError: (PurchasesError) -> Unit,
)
Expand Down
Loading