diff --git a/api-tester/src/main/java/com/revenuecat/apitester/java/PurchasesCommonAPI.java b/api-tester/src/main/java/com/revenuecat/apitester/java/PurchasesCommonAPI.java index a7cbac6f7d..92626eb6cc 100644 --- a/api-tester/src/main/java/com/revenuecat/apitester/java/PurchasesCommonAPI.java +++ b/api-tester/src/main/java/com/revenuecat/apitester/java/PurchasesCommonAPI.java @@ -27,10 +27,10 @@ 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; +import com.revenuecat.purchases.models.StoreReplacementMode; import com.revenuecat.purchases.models.StoreTransaction; import com.revenuecat.purchases.models.SubscriptionOption; @@ -109,15 +109,15 @@ public void onError(@NonNull PurchasesError error, boolean userCancelled) { } }; String oldProductId = "old"; - GoogleReplacementMode replacementMode = GoogleReplacementMode.WITH_TIME_PRORATION; - GalaxyReplacementMode galaxyReplacementMode = GalaxyReplacementMode.INSTANT_PRORATED_CHARGE; + GoogleReplacementMode googleReplacementMode = GoogleReplacementMode.WITH_TIME_PRORATION; + StoreReplacementMode storeReplacementMode = StoreReplacementMode.WITH_TIME_PRORATION; Boolean isPersonalizedPrice = true; PurchaseParams.Builder purchaseProductBuilder = new PurchaseParams.Builder(activity, storeProduct); purchaseProductBuilder .oldProductId(oldProductId) - .googleReplacementMode(replacementMode) - .galaxyReplacementMode(galaxyReplacementMode) + .googleReplacementMode(googleReplacementMode) + .replacementMode(storeReplacementMode) .isPersonalizedPrice(isPersonalizedPrice); PurchaseParams purchaseProductParams = purchaseProductBuilder.build(); purchases.purchase(purchaseProductParams, purchaseCallback); @@ -125,8 +125,8 @@ public void onError(@NonNull PurchasesError error, boolean userCancelled) { PurchaseParams.Builder purchaseOptionBuilder = new PurchaseParams.Builder(activity, subscriptionOption); purchaseOptionBuilder .oldProductId(oldProductId) - .googleReplacementMode(replacementMode) - .galaxyReplacementMode(galaxyReplacementMode) + .googleReplacementMode(googleReplacementMode) + .replacementMode(storeReplacementMode) .isPersonalizedPrice(isPersonalizedPrice); PurchaseParams purchaseOptionParams = purchaseOptionBuilder.build(); purchases.purchase(purchaseOptionParams, purchaseCallback); @@ -134,8 +134,8 @@ public void onError(@NonNull PurchasesError error, boolean userCancelled) { PurchaseParams.Builder purchasePackageBuilder = new PurchaseParams.Builder(activity, packageToPurchase); purchasePackageBuilder .oldProductId(oldProductId) - .googleReplacementMode(replacementMode) - .galaxyReplacementMode(galaxyReplacementMode) + .googleReplacementMode(googleReplacementMode) + .replacementMode(storeReplacementMode) .isPersonalizedPrice(isPersonalizedPrice); PurchaseParams purchasePackageParams = purchasePackageBuilder.build(); purchases.purchase(purchasePackageParams, purchaseCallback); diff --git a/api-tester/src/main/java/com/revenuecat/apitester/kotlin/GalaxyReplacementModeAPI.kt b/api-tester/src/main/java/com/revenuecat/apitester/kotlin/GalaxyReplacementModeAPI.kt deleted file mode 100644 index 47843fd714..0000000000 --- a/api-tester/src/main/java/com/revenuecat/apitester/kotlin/GalaxyReplacementModeAPI.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.revenuecat.apitester.kotlin - -import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI -import com.revenuecat.purchases.InternalRevenueCatAPI -import com.revenuecat.purchases.models.GalaxyReplacementMode - -@Suppress("unused", "UNUSED_VARIABLE") -private class GalaxyReplacementModeAPI { - @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class, InternalRevenueCatAPI::class) - fun check(mode: GalaxyReplacementMode) { - when (mode) { - GalaxyReplacementMode.INSTANT_PRORATED_DATE, - GalaxyReplacementMode.INSTANT_PRORATED_CHARGE, - GalaxyReplacementMode.INSTANT_NO_PRORATION, - GalaxyReplacementMode.DEFERRED, - -> {} - }.exhaustive - - val defaultMode: GalaxyReplacementMode = GalaxyReplacementMode.default - } -} diff --git a/api-tester/src/main/java/com/revenuecat/apitester/kotlin/PurchasesCommonAPI.kt b/api-tester/src/main/java/com/revenuecat/apitester/kotlin/PurchasesCommonAPI.kt index dd8bb0e941..fca2cd9eb2 100644 --- a/api-tester/src/main/java/com/revenuecat/apitester/kotlin/PurchasesCommonAPI.kt +++ b/api-tester/src/main/java/com/revenuecat/apitester/kotlin/PurchasesCommonAPI.kt @@ -35,10 +35,10 @@ 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 +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.models.SubscriptionOption import com.revenuecat.purchases.purchaseWith @@ -110,15 +110,15 @@ private class PurchasesCommonAPI { } val oldProductId = "old" - val replacementMode = GoogleReplacementMode.WITH_TIME_PRORATION - val galaxyReplacementMode = GalaxyReplacementMode.INSTANT_PRORATED_CHARGE + val googleReplacementMode = GoogleReplacementMode.WITH_TIME_PRORATION + val storeReplacementMode = StoreReplacementMode.WITH_TIME_PRORATION val isPersonalizedPrice = true val purchasePackageBuilder: PurchaseParams.Builder = PurchaseParams.Builder(activity, packageToPurchase) purchasePackageBuilder .oldProductId(oldProductId) - .googleReplacementMode(replacementMode) - .galaxyReplacementMode(galaxyReplacementMode) + .googleReplacementMode(googleReplacementMode) + .replacementMode(storeReplacementMode) .isPersonalizedPrice(isPersonalizedPrice) val purchasePackageParams: PurchaseParams = purchasePackageBuilder.build() purchases.purchase(purchasePackageParams, purchaseCallback) @@ -126,8 +126,8 @@ private class PurchasesCommonAPI { val purchaseProductBuilder: PurchaseParams.Builder = PurchaseParams.Builder(activity, storeProduct) purchaseProductBuilder .oldProductId(oldProductId) - .googleReplacementMode(replacementMode) - .galaxyReplacementMode(galaxyReplacementMode) + .googleReplacementMode(googleReplacementMode) + .replacementMode(storeReplacementMode) .isPersonalizedPrice(isPersonalizedPrice) val purchaseProductParams: PurchaseParams = purchaseProductBuilder.build() purchases.purchase(purchaseProductParams, purchaseCallback) @@ -135,8 +135,8 @@ private class PurchasesCommonAPI { val purchaseOptionBuilder: PurchaseParams.Builder = PurchaseParams.Builder(activity, subscriptionOption) purchaseOptionBuilder .oldProductId(oldProductId) - .googleReplacementMode(replacementMode) - .galaxyReplacementMode(galaxyReplacementMode) + .googleReplacementMode(googleReplacementMode) + .replacementMode(storeReplacementMode) .isPersonalizedPrice(isPersonalizedPrice) val purchaseOptionsParams: PurchaseParams = purchaseOptionBuilder.build() purchases.purchase(purchaseOptionsParams, purchaseCallback) diff --git a/api-tester/src/main/java/com/revenuecat/apitester/kotlin/StoreReplacementModeAPI.kt b/api-tester/src/main/java/com/revenuecat/apitester/kotlin/StoreReplacementModeAPI.kt new file mode 100644 index 0000000000..908c209026 --- /dev/null +++ b/api-tester/src/main/java/com/revenuecat/apitester/kotlin/StoreReplacementModeAPI.kt @@ -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 + } +} diff --git a/examples/purchase-tester/src/main/java/com/revenuecat/purchasetester/OfferingFragment.kt b/examples/purchase-tester/src/main/java/com/revenuecat/purchasetester/OfferingFragment.kt index 32e31800c5..b245c777cc 100644 --- a/examples/purchase-tester/src/main/java/com/revenuecat/purchasetester/OfferingFragment.kt +++ b/examples/purchase-tester/src/main/java/com/revenuecat/purchasetester/OfferingFragment.kt @@ -29,9 +29,9 @@ import com.revenuecat.purchases.getCustomerInfoWith import com.revenuecat.purchases.getOfferingsWith import com.revenuecat.purchases.interfaces.PurchaseCallback import com.revenuecat.purchases.models.GooglePurchasingData -import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.PurchasingData import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.models.SubscriptionOption import com.revenuecat.purchases_sample.R @@ -51,7 +51,7 @@ class OfferingFragment : Fragment(), PackageCardAdapter.PackageCardAdapterListen private var activeSubscriptions: Set = setOf() private lateinit var dataStoreUtils: DataStoreUtils - private var isPlayStore: Boolean = true + private var store: Store = Store.GOOGLE private var packageCardAdapter: PackageCardAdapter? = null private var isAddOnPurchaseUpgrade: Boolean = false @@ -74,7 +74,7 @@ class OfferingFragment : Fragment(), PackageCardAdapter.PackageCardAdapterListen lifecycleScope.launch { dataStoreUtils.getSdkConfig().onEach { sdkConfiguration -> - isPlayStore = sdkConfiguration.store == Store.GOOGLE + store = sdkConfiguration.store }.collect() } @@ -141,7 +141,7 @@ class OfferingFragment : Fragment(), PackageCardAdapter.PackageCardAdapterListen offering.availablePackages, activeSubscriptions, this, - isPlayStore, + store, ) binding.offeringDetailsPackagesRecycler.adapter = packageCardAdapter @@ -258,7 +258,7 @@ class OfferingFragment : Fragment(), PackageCardAdapter.PackageCardAdapterListen purchaseParamsBuilder.oldProductId(it) replacementMode?.let { - purchaseParamsBuilder.googleReplacementMode(replacementMode) + purchaseParamsBuilder.replacementMode(replacementMode) } val purchaseParams = purchaseParamsBuilder.build() @@ -311,7 +311,7 @@ class OfferingFragment : Fragment(), PackageCardAdapter.PackageCardAdapterListen purchaseParamsBuilder.oldProductId(it) replacementMode?.let { - purchaseParamsBuilder.googleReplacementMode(replacementMode) + purchaseParamsBuilder.replacementMode(replacementMode) } if (isPersonalizedPrice) { @@ -372,7 +372,7 @@ class OfferingFragment : Fragment(), PackageCardAdapter.PackageCardAdapterListen } } - private fun promptForProductChangeInfo(callback: (String?, GoogleReplacementMode?) -> Unit) { + private fun promptForProductChangeInfo(callback: (String?, StoreReplacementMode?) -> Unit) { showOldSubIdPicker { subId -> subId?.let { showReplacementModePicker { replacementMode, error -> @@ -439,9 +439,9 @@ class OfferingFragment : Fragment(), PackageCardAdapter.PackageCardAdapterListen .show() } - private fun showReplacementModePicker(callback: (GoogleReplacementMode?, Error?) -> Unit) { - val replacementModeOptions = GoogleReplacementMode.values() - var selectedReplacementMode: GoogleReplacementMode? = null + private fun showReplacementModePicker(callback: (StoreReplacementMode?, Error?) -> Unit) { + val replacementModeOptions = StoreReplacementMode.values() + var selectedReplacementMode: StoreReplacementMode? = null val replacementModeNames = replacementModeOptions.map { it.name }.toTypedArray() MaterialAlertDialogBuilder(requireContext()) diff --git a/examples/purchase-tester/src/main/java/com/revenuecat/purchasetester/PackageCardAdapter.kt b/examples/purchase-tester/src/main/java/com/revenuecat/purchasetester/PackageCardAdapter.kt index 49c4828331..fb8ab8c9ad 100644 --- a/examples/purchase-tester/src/main/java/com/revenuecat/purchasetester/PackageCardAdapter.kt +++ b/examples/purchase-tester/src/main/java/com/revenuecat/purchasetester/PackageCardAdapter.kt @@ -23,7 +23,7 @@ class PackageCardAdapter( private val packages: List, private val activeSubscriptions: Set, private val listener: PackageCardAdapterListener, - private val isPlayStore: Boolean, + private val store: Store, ) : RecyclerView.Adapter() { @@ -40,7 +40,7 @@ class PackageCardAdapter( override fun getItemCount(): Int = packages.size override fun onBindViewHolder(holder: PackageViewHolder, position: Int) { - holder.bind(packages[position], isPlayStore, isAddOnMode, selectedPackages.contains(packages[position])) + holder.bind(packages[position], store, isAddOnMode, selectedPackages.contains(packages[position])) } fun setAddOnMode(enabled: Boolean) { @@ -82,7 +82,12 @@ class PackageCardAdapter( private val nothingCheckedIndex = -1 @Suppress("CyclomaticComplexMethod", "LongMethod") - fun bind(currentPackage: Package, isPlayStore: Boolean, isAddOnMode: Boolean, isSelected: Boolean) { + fun bind(currentPackage: Package, store: Store, isAddOnMode: Boolean, isSelected: Boolean) { + val isPlayStore = store == Store.GOOGLE + val supportsProductChange = when (store) { + Store.GOOGLE, Store.GALAXY -> true + Store.AMAZON -> false + } val product = currentPackage.product val isSubscription = product.type == ProductType.SUBS val isActive = activeSubscriptions.contains(product.id) @@ -115,7 +120,7 @@ class PackageCardAdapter( binding.isUpgradeCheckbox.visibility = if (isAddOnMode) View.GONE else View.VISIBLE binding.isPersonalizedCheckbox.visibility = if (isAddOnMode) View.GONE else View.VISIBLE - binding.isUpgradeCheckbox.isEnabled = isPlayStore + binding.isUpgradeCheckbox.isEnabled = supportsProductChange binding.isPersonalizedCheckbox.isEnabled = isPlayStore binding.packageBuyButton.visibility = if (isAddOnMode) View.GONE else View.VISIBLE diff --git a/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/GalaxyBillingWrapper.kt b/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/GalaxyBillingWrapper.kt index c4e922ae13..547f29aed5 100644 --- a/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/GalaxyBillingWrapper.kt +++ b/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/GalaxyBillingWrapper.kt @@ -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 @@ -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() }, @@ -361,7 +363,7 @@ internal class GalaxyBillingWrapper( }, ) } - return + return // Exits makePurchaseAsync } serialRequestExecutor.executeSerially { finish -> @@ -390,7 +392,7 @@ internal class GalaxyBillingWrapper( receipt: PurchaseVo, productId: String, presentedOfferingContext: PresentedOfferingContext?, - replacementMode: GalaxyReplacementMode?, + replacementMode: StoreReplacementMode?, ) { try { val storeTransaction = receipt.toStoreTransaction( diff --git a/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/GalaxyStrings.kt b/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/GalaxyStrings.kt index d2f45f3ab9..307cbd4662 100644 --- a/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/GalaxyStrings.kt +++ b/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/GalaxyStrings.kt @@ -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 " + diff --git a/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/conversions/GalaxyReplacementModeConversions.kt b/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/conversions/GalaxyReplacementModeConversions.kt deleted file mode 100644 index 89bc68e017..0000000000 --- a/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/conversions/GalaxyReplacementModeConversions.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.revenuecat.purchases.galaxy.conversions - -import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI -import com.revenuecat.purchases.models.GalaxyReplacementMode -import com.samsung.android.sdk.iap.lib.constants.HelperDefine - -@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) -internal fun GalaxyReplacementMode.toSamsungProrationMode(): HelperDefine.ProrationMode { - return when (this) { - GalaxyReplacementMode.INSTANT_PRORATED_DATE -> HelperDefine.ProrationMode.INSTANT_PRORATED_DATE - GalaxyReplacementMode.INSTANT_PRORATED_CHARGE -> HelperDefine.ProrationMode.INSTANT_PRORATED_CHARGE - GalaxyReplacementMode.INSTANT_NO_PRORATION -> HelperDefine.ProrationMode.INSTANT_NO_PRORATION - GalaxyReplacementMode.DEFERRED -> HelperDefine.ProrationMode.DEFERRED - } -} diff --git a/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/conversions/StoreReplacementModeConversions.kt b/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/conversions/StoreReplacementModeConversions.kt new file mode 100644 index 0000000000..6fec32c165 --- /dev/null +++ b/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/conversions/StoreReplacementModeConversions.kt @@ -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 + log(LogIntent.GALAXY_ERROR) { message } + throw PurchasesException( + PurchasesError( + PurchasesErrorCode.UnsupportedError, + message, + ), + ) + } + } +} diff --git a/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/handler/ChangeSubscriptionPlanHandler.kt b/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/handler/ChangeSubscriptionPlanHandler.kt index 4c85b55de7..9013952034 100644 --- a/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/handler/ChangeSubscriptionPlanHandler.kt +++ b/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/handler/ChangeSubscriptionPlanHandler.kt @@ -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 @@ -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, ) { @@ -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, @@ -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, diff --git a/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/listener/ChangeSubscriptionPlanResponseListener.kt b/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/listener/ChangeSubscriptionPlanResponseListener.kt index 699d975adf..75b82a064f 100644 --- a/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/listener/ChangeSubscriptionPlanResponseListener.kt +++ b/feature/galaxy/src/main/kotlin/com/revenuecat/purchases/galaxy/listener/ChangeSubscriptionPlanResponseListener.kt @@ -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 @@ -22,7 +22,7 @@ internal interface ChangeSubscriptionPlanResponseListener : OnChangeSubscription appUserID: String, oldPurchase: StoreTransaction, newProductId: String, - prorationMode: GalaxyReplacementMode, + replacementMode: StoreReplacementMode, onSuccess: (PurchaseVo) -> Unit, onError: (PurchasesError) -> Unit, ) diff --git a/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/GalaxyBillingWrapperTest.kt b/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/GalaxyBillingWrapperTest.kt index 330355ab3e..18f7fe8be0 100644 --- a/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/GalaxyBillingWrapperTest.kt +++ b/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/GalaxyBillingWrapperTest.kt @@ -29,7 +29,6 @@ import com.revenuecat.purchases.galaxy.listener.ProductDataResponseListener import com.revenuecat.purchases.galaxy.listener.PurchaseResponseListener import com.revenuecat.purchases.galaxy.logging.currentLogHandler import com.revenuecat.purchases.galaxy.utils.GalaxySerialOperation -import com.revenuecat.purchases.models.GalaxyReplacementMode import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.Period import com.revenuecat.purchases.models.Price @@ -37,6 +36,7 @@ import com.revenuecat.purchases.models.PurchaseState import com.revenuecat.purchases.models.PurchaseType import com.revenuecat.purchases.models.PurchasingData import com.revenuecat.purchases.models.StoreProduct +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 @@ -707,7 +707,7 @@ class GalaxyBillingWrapperTest : GalaxyStoreTest() { type = ProductType.SUBS, productId = "old-product", ) - val replacementMode = GalaxyReplacementMode.INSTANT_PRORATED_CHARGE + val replacementMode = StoreReplacementMode.CHARGE_PRORATED_PRICE val onSuccessSlot = slot<(PurchaseVo) -> Unit>() val onErrorSlot = slot<(PurchasesError) -> Unit>() @@ -742,7 +742,7 @@ class GalaxyBillingWrapperTest : GalaxyStoreTest() { appUserID = "user", oldPurchase = oldPurchase, newProductId = storeProduct.id, - prorationMode = replacementMode, + replacementMode = replacementMode, onSuccess = any(), onError = any(), ) @@ -770,10 +770,11 @@ class GalaxyBillingWrapperTest : GalaxyStoreTest() { @OptIn(GalaxySerialOperation::class) @Test - fun `makePurchaseAsync defaults to GalaxyReplacementMode default when non-Galaxy replacement mode provided`() { + fun `makePurchaseAsync defaults to WITHOUT_PRORATION when replacement mode is not a StoreReplacementMode`() { val changeSubscriptionPlanHandler = mockk(relaxed = true) val wrapper = createWrapper(changeSubscriptionPlanHandler = changeSubscriptionPlanHandler) - wrapper.purchasesUpdatedListener = mockk(relaxed = true) + val purchasesUpdatedListener = mockk(relaxed = true) + wrapper.purchasesUpdatedListener = purchasesUpdatedListener val storeProduct = createStoreProduct() val oldPurchase = storeTransaction( @@ -781,15 +782,15 @@ class GalaxyBillingWrapperTest : GalaxyStoreTest() { type = ProductType.SUBS, productId = "old-product", ) - val prorationModeSlot = slot() + val onSuccessSlot = slot<(PurchaseVo) -> Unit>() every { changeSubscriptionPlanHandler.changeSubscriptionPlan( any(), any(), any(), - capture(prorationModeSlot), any(), + capture(onSuccessSlot), any(), ) } answers { } @@ -809,12 +810,113 @@ class GalaxyBillingWrapperTest : GalaxyStoreTest() { isPersonalizedPrice = null, ) - assertThat(prorationModeSlot.captured).isEqualTo(GalaxyReplacementMode.default) + verify(exactly = 1) { + changeSubscriptionPlanHandler.changeSubscriptionPlan( + appUserID = "user", + oldPurchase = oldPurchase, + newProductId = storeProduct.id, + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, + onSuccess = any(), + onError = any(), + ) + } + verify(exactly = 0) { + purchaseHandlerMock.purchase(any(), any(), any(), any()) + } + + val purchaseVo = createPurchaseVo( + paymentId = "paymentId", + purchaseId = "purchaseId", + orderId = "orderId", + purchaseDate = "2024-01-15 13:45:20", + type = "subscription", + itemId = storeProduct.id, + ) + onSuccessSlot.captured(purchaseVo) + + val transactionsSlot = slot>() + verify(exactly = 1) { purchasesUpdatedListener.onPurchasesUpdated(capture(transactionsSlot)) } + val transaction = transactionsSlot.captured.single() + assertThat(transaction.productIds).containsExactly(storeProduct.id) + assertThat(transaction.replacementMode).isEqualTo(StoreReplacementMode.WITHOUT_PRORATION) } @OptIn(GalaxySerialOperation::class) @Test - fun `makePurchaseAsync forwards errors from change subscription plan handler`() { + fun `makePurchaseAsync defaults to WITHOUT_PRORATION when replacement mode is null`() { + val changeSubscriptionPlanHandler = mockk(relaxed = true) + val wrapper = createWrapper(changeSubscriptionPlanHandler = changeSubscriptionPlanHandler) + val purchasesUpdatedListener = mockk(relaxed = true) + wrapper.purchasesUpdatedListener = purchasesUpdatedListener + + val storeProduct = createStoreProduct() + val oldPurchase = storeTransaction( + token = "old-token", + type = ProductType.SUBS, + productId = "old-product", + ) + val onSuccessSlot = slot<(PurchaseVo) -> Unit>() + + every { + changeSubscriptionPlanHandler.changeSubscriptionPlan( + any(), + any(), + any(), + any(), + capture(onSuccessSlot), + any(), + ) + } answers { } + + wrapper.makePurchaseAsync( + activity = mockk(), + appUserID = "user", + purchasingData = GalaxyPurchasingData.Product( + productId = storeProduct.id, + productType = storeProduct.type, + ), + replaceProductInfo = ReplaceProductInfo( + oldPurchase = oldPurchase, + replacementMode = null, + ), + presentedOfferingContext = null, + isPersonalizedPrice = null, + ) + + verify(exactly = 1) { + changeSubscriptionPlanHandler.changeSubscriptionPlan( + appUserID = "user", + oldPurchase = oldPurchase, + newProductId = storeProduct.id, + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, + onSuccess = any(), + onError = any(), + ) + } + verify(exactly = 0) { + purchaseHandlerMock.purchase(any(), any(), any(), any()) + } + + val purchaseVo = createPurchaseVo( + paymentId = "paymentId", + purchaseId = "purchaseId", + orderId = "orderId", + purchaseDate = "2024-01-15 13:45:20", + type = "subscription", + itemId = storeProduct.id, + ) + onSuccessSlot.captured(purchaseVo) + + val transactionsSlot = slot>() + verify(exactly = 1) { purchasesUpdatedListener.onPurchasesUpdated(capture(transactionsSlot)) } + val transaction = transactionsSlot.captured.single() + assertThat(transaction.productIds).containsExactly(storeProduct.id) + assertThat(transaction.replacementMode).isEqualTo(StoreReplacementMode.WITHOUT_PRORATION) + } + + @OptIn(GalaxySerialOperation::class) + @Test + fun `makePurchaseAsync forwards errors from change subscription plan handler for StoreReplacementMode`() { val purchasesUpdatedListener = mockk(relaxed = true) val changeSubscriptionPlanHandler = mockk(relaxed = true) val wrapper = createWrapper(changeSubscriptionPlanHandler = changeSubscriptionPlanHandler) @@ -846,7 +948,10 @@ class GalaxyBillingWrapperTest : GalaxyStoreTest() { productId = storeProduct.id, productType = storeProduct.type, ), - replaceProductInfo = ReplaceProductInfo(oldPurchase = oldPurchase), + replaceProductInfo = ReplaceProductInfo( + oldPurchase = oldPurchase, + replacementMode = StoreReplacementMode.CHARGE_FULL_PRICE, + ), presentedOfferingContext = null, isPersonalizedPrice = null, ) diff --git a/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/conversions/GalaxyReplacementModeConversionsTest.kt b/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/conversions/GalaxyReplacementModeConversionsTest.kt deleted file mode 100644 index 01c764d805..0000000000 --- a/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/conversions/GalaxyReplacementModeConversionsTest.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.revenuecat.purchases.galaxy.conversions - -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI -import com.revenuecat.purchases.InternalRevenueCatAPI -import com.revenuecat.purchases.models.GalaxyReplacementMode -import com.samsung.android.sdk.iap.lib.constants.HelperDefine -import kotlin.test.Test -import org.assertj.core.api.Assertions.assertThat -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -class GalaxyReplacementModeConversionsTest { - - @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) - @Test - fun `all replacement modes map to Samsung proration modes`() { - val expectations = mapOf( - GalaxyReplacementMode.INSTANT_PRORATED_DATE to HelperDefine.ProrationMode.INSTANT_PRORATED_DATE, - GalaxyReplacementMode.INSTANT_PRORATED_CHARGE to HelperDefine.ProrationMode.INSTANT_PRORATED_CHARGE, - GalaxyReplacementMode.INSTANT_NO_PRORATION to HelperDefine.ProrationMode.INSTANT_NO_PRORATION, - GalaxyReplacementMode.DEFERRED to HelperDefine.ProrationMode.DEFERRED, - ) - - for (mode in GalaxyReplacementMode.values()) { - val expected = expectations[mode] ?: error("Missing expected mapping for $mode") - assertThat(mode.toSamsungProrationMode()).isEqualTo(expected) - } - - assertThat(expectations.size).isEqualTo(GalaxyReplacementMode.values().size) - } - - @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class, InternalRevenueCatAPI::class) - @Test - fun `default replacement mode is instant no proration`() { - assertThat(GalaxyReplacementMode.default).isEqualTo(GalaxyReplacementMode.INSTANT_NO_PRORATION) - } -} diff --git a/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/conversions/StoreReplacementModeConversionsTest.kt b/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/conversions/StoreReplacementModeConversionsTest.kt new file mode 100644 index 0000000000..f00821daf6 --- /dev/null +++ b/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/conversions/StoreReplacementModeConversionsTest.kt @@ -0,0 +1,57 @@ +package com.revenuecat.purchases.galaxy.conversions + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.revenuecat.purchases.PurchasesErrorCode +import com.revenuecat.purchases.PurchasesException +import com.revenuecat.purchases.galaxy.GalaxyStrings +import com.revenuecat.purchases.models.StoreReplacementMode +import com.samsung.android.sdk.iap.lib.constants.HelperDefine +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.catchThrowableOfType +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class StoreReplacementModeConversionsTest { + + @Test + fun `supported store replacement modes map to Galaxy proration modes`() { + val expectations = mapOf( + StoreReplacementMode.WITHOUT_PRORATION to HelperDefine.ProrationMode.INSTANT_NO_PRORATION, + StoreReplacementMode.WITH_TIME_PRORATION to HelperDefine.ProrationMode.INSTANT_PRORATED_DATE, + StoreReplacementMode.CHARGE_PRORATED_PRICE to HelperDefine.ProrationMode.INSTANT_PRORATED_CHARGE, + StoreReplacementMode.DEFERRED to HelperDefine.ProrationMode.DEFERRED, + ) + + StoreReplacementMode.values() + .filter { it != StoreReplacementMode.CHARGE_FULL_PRICE } + .forEach { mode -> + val expected = expectations[mode] ?: error("Missing expected mapping for $mode") + assertThat(mode.toGalaxyReplacementMode()).isEqualTo(expected) + } + + assertThat(expectations.size).isEqualTo(StoreReplacementMode.values().size - 1) + } + + @Test + fun `only charge full price is excluded from supported Galaxy mappings`() { + val unsupportedModes = StoreReplacementMode.values() + .filter { mode -> + runCatching { mode.toGalaxyReplacementMode() }.isFailure + } + + assertThat(unsupportedModes).containsExactly(StoreReplacementMode.CHARGE_FULL_PRICE) + } + + @Test + fun `charge full price throws unsupported error for Galaxy`() { + val exception = catchThrowableOfType( + { StoreReplacementMode.CHARGE_FULL_PRICE.toGalaxyReplacementMode() }, + PurchasesException::class.java, + ) + + assertThat(exception).isNotNull + assertThat(exception.error.code).isEqualTo(PurchasesErrorCode.UnsupportedError) + assertThat(exception.error.underlyingErrorMessage).isEqualTo(GalaxyStrings.CHARGE_FULL_PRICE_NOT_SUPPORTED) + } +} diff --git a/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/conversions/StoreTransactionConversionsTest.kt b/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/conversions/StoreTransactionConversionsTest.kt index 8c68be0033..af18cd9eab 100644 --- a/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/conversions/StoreTransactionConversionsTest.kt +++ b/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/conversions/StoreTransactionConversionsTest.kt @@ -5,9 +5,9 @@ import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI import com.revenuecat.purchases.PresentedOfferingContext import com.revenuecat.purchases.ProductType import com.revenuecat.purchases.galaxy.utils.parseDateFromGalaxyDateString -import com.revenuecat.purchases.models.GalaxyReplacementMode import com.revenuecat.purchases.models.PurchaseState import com.revenuecat.purchases.models.PurchaseType +import com.revenuecat.purchases.models.StoreReplacementMode import com.samsung.android.sdk.iap.lib.vo.PurchaseVo import io.mockk.every import io.mockk.mockk @@ -82,7 +82,7 @@ class StoreTransactionConversionsTest { purchaseDate = purchaseDateString, type = "subscription", ) - val replacementMode = GalaxyReplacementMode.INSTANT_PRORATED_DATE + val replacementMode = StoreReplacementMode.WITH_TIME_PRORATION val storeTransaction = purchaseVo.toStoreTransaction( productId = "product", diff --git a/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/handler/ChangeSubscriptionPlanHandlerTest.kt b/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/handler/ChangeSubscriptionPlanHandlerTest.kt index 0dbb966d6b..b10027a1ec 100644 --- a/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/handler/ChangeSubscriptionPlanHandlerTest.kt +++ b/feature/galaxy/src/test/java/com/revenuecat/purchases/galaxy/handler/ChangeSubscriptionPlanHandlerTest.kt @@ -9,9 +9,9 @@ import com.revenuecat.purchases.common.sha256 import com.revenuecat.purchases.galaxy.GalaxyStrings import com.revenuecat.purchases.galaxy.IAPHelperProvider import com.revenuecat.purchases.galaxy.constants.GalaxyErrorCode -import com.revenuecat.purchases.galaxy.conversions.toSamsungProrationMode +import com.revenuecat.purchases.galaxy.conversions.toGalaxyReplacementMode 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.constants.HelperDefine import com.samsung.android.sdk.iap.lib.vo.ErrorVo @@ -59,7 +59,7 @@ class ChangeSubscriptionPlanHandlerTest { appUserID = "user", oldPurchase = transactionWithProductId("old"), newProductId = "new", - prorationMode = GalaxyReplacementMode.INSTANT_PRORATED_DATE, + replacementMode = StoreReplacementMode.WITH_TIME_PRORATION, onSuccess = unexpectedOnSuccess, onError = unexpectedOnError, ) @@ -69,7 +69,7 @@ class ChangeSubscriptionPlanHandlerTest { appUserID = "user", oldPurchase = transactionWithProductId("other-old"), newProductId = "newer", - prorationMode = GalaxyReplacementMode.INSTANT_NO_PRORATION, + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, onSuccess = unexpectedOnSuccess, onError = { receivedError = it }, ) @@ -96,7 +96,7 @@ class ChangeSubscriptionPlanHandlerTest { appUserID = "user", oldPurchase = transactionWithProductId(null), newProductId = "new", - prorationMode = GalaxyReplacementMode.INSTANT_PRORATED_DATE, + replacementMode = StoreReplacementMode.WITH_TIME_PRORATION, onSuccess = unexpectedOnSuccess, onError = { receivedError = it }, ) @@ -136,7 +136,7 @@ class ChangeSubscriptionPlanHandlerTest { appUserID = "user", oldPurchase = transactionWithProductId("old"), newProductId = "new", - prorationMode = GalaxyReplacementMode.INSTANT_NO_PRORATION, + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, onSuccess = unexpectedOnSuccess, onError = { receivedError = it }, ) @@ -159,7 +159,7 @@ class ChangeSubscriptionPlanHandlerTest { appUserID = "user", oldPurchase = transactionWithProductId("old"), newProductId = "new", - prorationMode = GalaxyReplacementMode.INSTANT_NO_PRORATION, + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, onSuccess = mockk(relaxed = true), onError = unexpectedOnError, ) @@ -182,7 +182,7 @@ class ChangeSubscriptionPlanHandlerTest { fun `changeSubscriptionPlan dispatches request with expected args and forwards success`() { val oldProductId = "old-sku" val newProductId = "new-sku" - val prorationMode = GalaxyReplacementMode.INSTANT_PRORATED_CHARGE + val replacementMode = StoreReplacementMode.CHARGE_PRORATED_PRICE val obfuscatedAccountIdSlot = slot() val prorationModeSlot = slot() every { @@ -201,7 +201,7 @@ class ChangeSubscriptionPlanHandlerTest { appUserID = "user", oldPurchase = transactionWithProductId(oldProductId), newProductId = newProductId, - prorationMode = prorationMode, + replacementMode = replacementMode, onSuccess = onSuccess, onError = unexpectedOnError, ) @@ -223,14 +223,14 @@ class ChangeSubscriptionPlanHandlerTest { onChangeSubscriptionPlanListener = changeSubscriptionPlanHandler, ) } - assertThat(prorationModeSlot.captured).isEqualTo(prorationMode.toSamsungProrationMode()) + assertThat(prorationModeSlot.captured).isEqualTo(replacementMode.toGalaxyReplacementMode()) assertThat(obfuscatedAccountIdSlot.captured).isEqualTo("user".sha256()) changeSubscriptionPlanHandler.changeSubscriptionPlan( appUserID = "user", oldPurchase = transactionWithProductId(oldProductId), newProductId = "next", - prorationMode = GalaxyReplacementMode.INSTANT_NO_PRORATION, + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, onSuccess = onSuccess, onError = unexpectedOnError, ) @@ -265,7 +265,7 @@ class ChangeSubscriptionPlanHandlerTest { appUserID = "user", oldPurchase = transactionWithProductId("old"), newProductId = "new", - prorationMode = GalaxyReplacementMode.INSTANT_NO_PRORATION, + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, onSuccess = unexpectedOnSuccess, onError = { receivedError = it }, ) @@ -283,7 +283,7 @@ class ChangeSubscriptionPlanHandlerTest { appUserID = "user", oldPurchase = transactionWithProductId("old"), newProductId = "new", - prorationMode = GalaxyReplacementMode.INSTANT_NO_PRORATION, + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, onSuccess = mockk(relaxed = true), onError = unexpectedOnError, ) @@ -318,7 +318,7 @@ class ChangeSubscriptionPlanHandlerTest { appUserID = "user", oldPurchase = transactionWithProductId("old"), newProductId = "new", - prorationMode = GalaxyReplacementMode.INSTANT_NO_PRORATION, + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, onSuccess = unexpectedOnSuccess, onError = { receivedError = it }, ) @@ -336,7 +336,7 @@ class ChangeSubscriptionPlanHandlerTest { appUserID = "user", oldPurchase = transactionWithProductId("old"), newProductId = "new", - prorationMode = GalaxyReplacementMode.INSTANT_NO_PRORATION, + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, onSuccess = mockk(relaxed = true), onError = unexpectedOnError, ) @@ -352,6 +352,81 @@ class ChangeSubscriptionPlanHandlerTest { } } + @OptIn(GalaxySerialOperation::class, ExperimentalPreviewRevenueCatPurchasesAPI::class) + @Test + fun `changeSubscriptionPlan errors when replacement mode is unsupported`() { + var receivedError: PurchasesError? = null + + changeSubscriptionPlanHandler.changeSubscriptionPlan( + appUserID = "user", + oldPurchase = transactionWithProductId("old"), + newProductId = "new", + replacementMode = StoreReplacementMode.CHARGE_FULL_PRICE, + onSuccess = unexpectedOnSuccess, + onError = { receivedError = it }, + ) + + assertThat(receivedError?.code).isEqualTo(PurchasesErrorCode.UnsupportedError) + assertThat(receivedError?.underlyingErrorMessage) + .isEqualTo(GalaxyStrings.CHARGE_FULL_PRICE_NOT_SUPPORTED) + verify(exactly = 0) { + iapHelperProvider.changeSubscriptionPlan( + any(), + any(), + any(), + any(), + any(), + any() + ) + } + } + + @OptIn(GalaxySerialOperation::class, ExperimentalPreviewRevenueCatPurchasesAPI::class, + InternalRevenueCatAPI::class + ) + @Test + fun `changeSubscriptionPlan allows supported request after unsupported replacement mode error`() { + every { + iapHelperProvider.changeSubscriptionPlan( + any(), + any(), + any(), + any(), + any(), + any() + ) + } returns true + + changeSubscriptionPlanHandler.changeSubscriptionPlan( + appUserID = "user", + oldPurchase = transactionWithProductId("old"), + newProductId = "new", + replacementMode = StoreReplacementMode.CHARGE_FULL_PRICE, + onSuccess = unexpectedOnSuccess, + onError = mockk(relaxed = true), + ) + + changeSubscriptionPlanHandler.changeSubscriptionPlan( + appUserID = "user", + oldPurchase = transactionWithProductId("old"), + newProductId = "new", + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, + onSuccess = mockk(relaxed = true), + onError = unexpectedOnError, + ) + + verify(exactly = 1) { + iapHelperProvider.changeSubscriptionPlan( + any(), + any(), + any(), + any(), + any(), + any() + ) + } + } + private fun transactionWithProductId(productId: String?): StoreTransaction = mockk { val productIdsList = productId?.let { listOf(it) } ?: emptyList() every { productIds } returns productIdsList diff --git a/purchases/api-defaults-bc7.txt b/purchases/api-defaults-bc7.txt index 33067cd1ff..7cb4b8a4f3 100644 --- a/purchases/api-defaults-bc7.txt +++ b/purchases/api-defaults-bc7.txt @@ -338,15 +338,15 @@ package com.revenuecat.purchases { @dev.drewhamilton.poko.Poko public final class PurchaseParams { ctor public PurchaseParams(com.revenuecat.purchases.PurchaseParams.Builder builder); method public com.revenuecat.purchases.PurchaseParams.Builder getBuilder(); - method public com.revenuecat.purchases.models.GalaxyReplacementMode getGalaxyReplacementMode(); - method public com.revenuecat.purchases.models.GoogleReplacementMode getGoogleReplacementMode(); + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode getGoogleReplacementMode(); method public String? getOldProductId(); + method public com.revenuecat.purchases.models.StoreReplacementMode getReplacementMode(); method public Boolean? isPersonalizedPrice(); property public final com.revenuecat.purchases.PurchaseParams.Builder builder; - property public final com.revenuecat.purchases.models.GalaxyReplacementMode galaxyReplacementMode; - property public final com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode; + property @Deprecated public final com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode; property public final Boolean? isPersonalizedPrice; property public final String? oldProductId; + property public final com.revenuecat.purchases.models.StoreReplacementMode replacementMode; } public static class PurchaseParams.Builder { @@ -357,11 +357,11 @@ package com.revenuecat.purchases { method @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public final com.revenuecat.purchases.PurchaseParams.Builder addOnStoreProducts(java.util.List addOnStoreProducts); method @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public final com.revenuecat.purchases.PurchaseParams.Builder addOnSubscriptionOptions(java.util.List addOnSubscriptionOptions); method public com.revenuecat.purchases.PurchaseParams build(); - method @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public final com.revenuecat.purchases.PurchaseParams.Builder galaxyReplacementMode(com.revenuecat.purchases.models.GalaxyReplacementMode galaxyReplacementMode); - method public final com.revenuecat.purchases.PurchaseParams.Builder googleReplacementMode(com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode); + method @Deprecated public final com.revenuecat.purchases.PurchaseParams.Builder googleReplacementMode(com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode); method public final com.revenuecat.purchases.PurchaseParams.Builder isPersonalizedPrice(boolean isPersonalizedPrice); method public final com.revenuecat.purchases.PurchaseParams.Builder oldProductId(String oldProductId); method public final com.revenuecat.purchases.PurchaseParams.Builder presentedOfferingContext(com.revenuecat.purchases.PresentedOfferingContext presentedOfferingContext); + method public final com.revenuecat.purchases.PurchaseParams.Builder replacementMode(com.revenuecat.purchases.models.StoreReplacementMode replacementMode); } @dev.drewhamilton.poko.Poko public final class PurchaseResult { @@ -1178,19 +1178,6 @@ package com.revenuecat.purchases.models { enum_constant public static final com.revenuecat.purchases.models.BillingFeature SUBSCRIPTIONS_UPDATE; } - @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI @kotlinx.parcelize.Parcelize public enum GalaxyReplacementMode implements com.revenuecat.purchases.ReplacementMode { - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode DEFERRED; - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode INSTANT_NO_PRORATION; - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode INSTANT_PRORATED_CHARGE; - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode INSTANT_PRORATED_DATE; - field public static final com.revenuecat.purchases.models.GalaxyReplacementMode.Companion Companion; - } - - @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public static final class GalaxyReplacementMode.Companion { - method public com.revenuecat.purchases.models.GalaxyReplacementMode getDefault(); - property public final com.revenuecat.purchases.models.GalaxyReplacementMode default; - } - @dev.drewhamilton.poko.Poko public final class GoogleInstallmentsInfo implements com.revenuecat.purchases.models.InstallmentsInfo { ctor public GoogleInstallmentsInfo(int commitmentPaymentsCount, int renewalCommitmentPaymentsCount); method public int getCommitmentPaymentsCount(); @@ -1225,23 +1212,23 @@ package com.revenuecat.purchases.models { property public final String token; } - public enum GoogleReplacementMode implements com.revenuecat.purchases.ReplacementMode { - method public int describeContents(); - method public int getPlayBillingClientMode(); - method public void writeToParcel(android.os.Parcel out, int flags); - property public final int playBillingClientMode; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_FULL_PRICE; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_PRORATED_PRICE; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode DEFERRED; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode WITHOUT_PRORATION; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode WITH_TIME_PRORATION; - field public static final com.revenuecat.purchases.models.GoogleReplacementMode.CREATOR CREATOR; + @Deprecated public enum GoogleReplacementMode implements com.revenuecat.purchases.ReplacementMode { + method @Deprecated public int describeContents(); + method @Deprecated public int getPlayBillingClientMode(); + method @Deprecated public void writeToParcel(android.os.Parcel out, int flags); + property @Deprecated public final int playBillingClientMode; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_FULL_PRICE; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_PRORATED_PRICE; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode DEFERRED; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode WITHOUT_PRORATION; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode WITH_TIME_PRORATION; + field @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode.CREATOR CREATOR; } - public static final class GoogleReplacementMode.CREATOR implements android.os.Parcelable.Creator { - method public com.revenuecat.purchases.models.GoogleReplacementMode? createFromParcel(android.os.Parcel in); - method public com.revenuecat.purchases.models.GoogleReplacementMode? fromPlayBillingClientMode(@com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode Integer? playBillingClientMode); - method public com.revenuecat.purchases.models.GoogleReplacementMode?[] newArray(int size); + @Deprecated public static final class GoogleReplacementMode.CREATOR implements android.os.Parcelable.Creator { + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode? createFromParcel(android.os.Parcel in); + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode? fromPlayBillingClientMode(@com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode Integer? playBillingClientMode); + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode?[] newArray(int size); } @dev.drewhamilton.poko.Poko public final class GoogleStoreProduct implements com.revenuecat.purchases.models.StoreProduct { @@ -1473,6 +1460,14 @@ package com.revenuecat.purchases.models { property public abstract com.revenuecat.purchases.ProductType type; } + @kotlinx.parcelize.Parcelize public enum StoreReplacementMode implements com.revenuecat.purchases.ReplacementMode { + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode CHARGE_FULL_PRICE; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode CHARGE_PRORATED_PRICE; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode DEFERRED; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode WITHOUT_PRORATION; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode WITH_TIME_PRORATION; + } + @dev.drewhamilton.poko.Poko @kotlinx.parcelize.Parcelize @kotlinx.parcelize.TypeParceler public final class StoreTransaction implements android.os.Parcelable { ctor public StoreTransaction(String? orderId, java.util.List productIds, com.revenuecat.purchases.ProductType type, long purchaseTime, String purchaseToken, com.revenuecat.purchases.models.PurchaseState purchaseState, Boolean? isAutoRenewing, String? signature, org.json.JSONObject originalJson, com.revenuecat.purchases.PresentedOfferingContext? presentedOfferingContext, String? storeUserID, com.revenuecat.purchases.models.PurchaseType purchaseType, String? marketplace, String? subscriptionOptionId, com.revenuecat.purchases.ReplacementMode? replacementMode); ctor @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public StoreTransaction(String? orderId, java.util.List productIds, com.revenuecat.purchases.ProductType type, long purchaseTime, String purchaseToken, com.revenuecat.purchases.models.PurchaseState purchaseState, Boolean? isAutoRenewing, String? signature, org.json.JSONObject originalJson, com.revenuecat.purchases.PresentedOfferingContext? presentedOfferingContext, String? storeUserID, com.revenuecat.purchases.models.PurchaseType purchaseType, String? marketplace, String? subscriptionOptionId, @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI java.util.Map? subscriptionOptionIdForProductIDs, com.revenuecat.purchases.ReplacementMode? replacementMode); diff --git a/purchases/api-defauts.txt b/purchases/api-defauts.txt index 33067cd1ff..7cb4b8a4f3 100644 --- a/purchases/api-defauts.txt +++ b/purchases/api-defauts.txt @@ -338,15 +338,15 @@ package com.revenuecat.purchases { @dev.drewhamilton.poko.Poko public final class PurchaseParams { ctor public PurchaseParams(com.revenuecat.purchases.PurchaseParams.Builder builder); method public com.revenuecat.purchases.PurchaseParams.Builder getBuilder(); - method public com.revenuecat.purchases.models.GalaxyReplacementMode getGalaxyReplacementMode(); - method public com.revenuecat.purchases.models.GoogleReplacementMode getGoogleReplacementMode(); + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode getGoogleReplacementMode(); method public String? getOldProductId(); + method public com.revenuecat.purchases.models.StoreReplacementMode getReplacementMode(); method public Boolean? isPersonalizedPrice(); property public final com.revenuecat.purchases.PurchaseParams.Builder builder; - property public final com.revenuecat.purchases.models.GalaxyReplacementMode galaxyReplacementMode; - property public final com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode; + property @Deprecated public final com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode; property public final Boolean? isPersonalizedPrice; property public final String? oldProductId; + property public final com.revenuecat.purchases.models.StoreReplacementMode replacementMode; } public static class PurchaseParams.Builder { @@ -357,11 +357,11 @@ package com.revenuecat.purchases { method @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public final com.revenuecat.purchases.PurchaseParams.Builder addOnStoreProducts(java.util.List addOnStoreProducts); method @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public final com.revenuecat.purchases.PurchaseParams.Builder addOnSubscriptionOptions(java.util.List addOnSubscriptionOptions); method public com.revenuecat.purchases.PurchaseParams build(); - method @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public final com.revenuecat.purchases.PurchaseParams.Builder galaxyReplacementMode(com.revenuecat.purchases.models.GalaxyReplacementMode galaxyReplacementMode); - method public final com.revenuecat.purchases.PurchaseParams.Builder googleReplacementMode(com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode); + method @Deprecated public final com.revenuecat.purchases.PurchaseParams.Builder googleReplacementMode(com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode); method public final com.revenuecat.purchases.PurchaseParams.Builder isPersonalizedPrice(boolean isPersonalizedPrice); method public final com.revenuecat.purchases.PurchaseParams.Builder oldProductId(String oldProductId); method public final com.revenuecat.purchases.PurchaseParams.Builder presentedOfferingContext(com.revenuecat.purchases.PresentedOfferingContext presentedOfferingContext); + method public final com.revenuecat.purchases.PurchaseParams.Builder replacementMode(com.revenuecat.purchases.models.StoreReplacementMode replacementMode); } @dev.drewhamilton.poko.Poko public final class PurchaseResult { @@ -1178,19 +1178,6 @@ package com.revenuecat.purchases.models { enum_constant public static final com.revenuecat.purchases.models.BillingFeature SUBSCRIPTIONS_UPDATE; } - @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI @kotlinx.parcelize.Parcelize public enum GalaxyReplacementMode implements com.revenuecat.purchases.ReplacementMode { - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode DEFERRED; - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode INSTANT_NO_PRORATION; - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode INSTANT_PRORATED_CHARGE; - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode INSTANT_PRORATED_DATE; - field public static final com.revenuecat.purchases.models.GalaxyReplacementMode.Companion Companion; - } - - @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public static final class GalaxyReplacementMode.Companion { - method public com.revenuecat.purchases.models.GalaxyReplacementMode getDefault(); - property public final com.revenuecat.purchases.models.GalaxyReplacementMode default; - } - @dev.drewhamilton.poko.Poko public final class GoogleInstallmentsInfo implements com.revenuecat.purchases.models.InstallmentsInfo { ctor public GoogleInstallmentsInfo(int commitmentPaymentsCount, int renewalCommitmentPaymentsCount); method public int getCommitmentPaymentsCount(); @@ -1225,23 +1212,23 @@ package com.revenuecat.purchases.models { property public final String token; } - public enum GoogleReplacementMode implements com.revenuecat.purchases.ReplacementMode { - method public int describeContents(); - method public int getPlayBillingClientMode(); - method public void writeToParcel(android.os.Parcel out, int flags); - property public final int playBillingClientMode; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_FULL_PRICE; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_PRORATED_PRICE; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode DEFERRED; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode WITHOUT_PRORATION; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode WITH_TIME_PRORATION; - field public static final com.revenuecat.purchases.models.GoogleReplacementMode.CREATOR CREATOR; + @Deprecated public enum GoogleReplacementMode implements com.revenuecat.purchases.ReplacementMode { + method @Deprecated public int describeContents(); + method @Deprecated public int getPlayBillingClientMode(); + method @Deprecated public void writeToParcel(android.os.Parcel out, int flags); + property @Deprecated public final int playBillingClientMode; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_FULL_PRICE; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_PRORATED_PRICE; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode DEFERRED; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode WITHOUT_PRORATION; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode WITH_TIME_PRORATION; + field @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode.CREATOR CREATOR; } - public static final class GoogleReplacementMode.CREATOR implements android.os.Parcelable.Creator { - method public com.revenuecat.purchases.models.GoogleReplacementMode? createFromParcel(android.os.Parcel in); - method public com.revenuecat.purchases.models.GoogleReplacementMode? fromPlayBillingClientMode(@com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode Integer? playBillingClientMode); - method public com.revenuecat.purchases.models.GoogleReplacementMode?[] newArray(int size); + @Deprecated public static final class GoogleReplacementMode.CREATOR implements android.os.Parcelable.Creator { + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode? createFromParcel(android.os.Parcel in); + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode? fromPlayBillingClientMode(@com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode Integer? playBillingClientMode); + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode?[] newArray(int size); } @dev.drewhamilton.poko.Poko public final class GoogleStoreProduct implements com.revenuecat.purchases.models.StoreProduct { @@ -1473,6 +1460,14 @@ package com.revenuecat.purchases.models { property public abstract com.revenuecat.purchases.ProductType type; } + @kotlinx.parcelize.Parcelize public enum StoreReplacementMode implements com.revenuecat.purchases.ReplacementMode { + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode CHARGE_FULL_PRICE; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode CHARGE_PRORATED_PRICE; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode DEFERRED; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode WITHOUT_PRORATION; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode WITH_TIME_PRORATION; + } + @dev.drewhamilton.poko.Poko @kotlinx.parcelize.Parcelize @kotlinx.parcelize.TypeParceler public final class StoreTransaction implements android.os.Parcelable { ctor public StoreTransaction(String? orderId, java.util.List productIds, com.revenuecat.purchases.ProductType type, long purchaseTime, String purchaseToken, com.revenuecat.purchases.models.PurchaseState purchaseState, Boolean? isAutoRenewing, String? signature, org.json.JSONObject originalJson, com.revenuecat.purchases.PresentedOfferingContext? presentedOfferingContext, String? storeUserID, com.revenuecat.purchases.models.PurchaseType purchaseType, String? marketplace, String? subscriptionOptionId, com.revenuecat.purchases.ReplacementMode? replacementMode); ctor @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public StoreTransaction(String? orderId, java.util.List productIds, com.revenuecat.purchases.ProductType type, long purchaseTime, String purchaseToken, com.revenuecat.purchases.models.PurchaseState purchaseState, Boolean? isAutoRenewing, String? signature, org.json.JSONObject originalJson, com.revenuecat.purchases.PresentedOfferingContext? presentedOfferingContext, String? storeUserID, com.revenuecat.purchases.models.PurchaseType purchaseType, String? marketplace, String? subscriptionOptionId, @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI java.util.Map? subscriptionOptionIdForProductIDs, com.revenuecat.purchases.ReplacementMode? replacementMode); diff --git a/purchases/api-entitlement.txt b/purchases/api-entitlement.txt index 7508b4e97a..fb8881f251 100644 --- a/purchases/api-entitlement.txt +++ b/purchases/api-entitlement.txt @@ -306,15 +306,15 @@ package com.revenuecat.purchases { @dev.drewhamilton.poko.Poko public final class PurchaseParams { ctor public PurchaseParams(com.revenuecat.purchases.PurchaseParams.Builder builder); method public com.revenuecat.purchases.PurchaseParams.Builder getBuilder(); - method public com.revenuecat.purchases.models.GalaxyReplacementMode getGalaxyReplacementMode(); - method public com.revenuecat.purchases.models.GoogleReplacementMode getGoogleReplacementMode(); + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode getGoogleReplacementMode(); method public String? getOldProductId(); + method public com.revenuecat.purchases.models.StoreReplacementMode getReplacementMode(); method public Boolean? isPersonalizedPrice(); property public final com.revenuecat.purchases.PurchaseParams.Builder builder; - property public final com.revenuecat.purchases.models.GalaxyReplacementMode galaxyReplacementMode; - property public final com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode; + property @Deprecated public final com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode; property public final Boolean? isPersonalizedPrice; property public final String? oldProductId; + property public final com.revenuecat.purchases.models.StoreReplacementMode replacementMode; } public static class PurchaseParams.Builder { @@ -325,11 +325,11 @@ package com.revenuecat.purchases { method @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public final com.revenuecat.purchases.PurchaseParams.Builder addOnStoreProducts(java.util.List addOnStoreProducts); method @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public final com.revenuecat.purchases.PurchaseParams.Builder addOnSubscriptionOptions(java.util.List addOnSubscriptionOptions); method public com.revenuecat.purchases.PurchaseParams build(); - method @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public final com.revenuecat.purchases.PurchaseParams.Builder galaxyReplacementMode(com.revenuecat.purchases.models.GalaxyReplacementMode galaxyReplacementMode); - method public final com.revenuecat.purchases.PurchaseParams.Builder googleReplacementMode(com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode); + method @Deprecated public final com.revenuecat.purchases.PurchaseParams.Builder googleReplacementMode(com.revenuecat.purchases.models.GoogleReplacementMode googleReplacementMode); method public final com.revenuecat.purchases.PurchaseParams.Builder isPersonalizedPrice(boolean isPersonalizedPrice); method public final com.revenuecat.purchases.PurchaseParams.Builder oldProductId(String oldProductId); method public final com.revenuecat.purchases.PurchaseParams.Builder presentedOfferingContext(com.revenuecat.purchases.PresentedOfferingContext presentedOfferingContext); + method public final com.revenuecat.purchases.PurchaseParams.Builder replacementMode(com.revenuecat.purchases.models.StoreReplacementMode replacementMode); } @dev.drewhamilton.poko.Poko public final class PurchaseResult { @@ -1055,19 +1055,6 @@ package com.revenuecat.purchases.models { enum_constant public static final com.revenuecat.purchases.models.BillingFeature SUBSCRIPTIONS_UPDATE; } - @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI @kotlinx.parcelize.Parcelize public enum GalaxyReplacementMode implements com.revenuecat.purchases.ReplacementMode { - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode DEFERRED; - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode INSTANT_NO_PRORATION; - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode INSTANT_PRORATED_CHARGE; - enum_constant public static final com.revenuecat.purchases.models.GalaxyReplacementMode INSTANT_PRORATED_DATE; - field public static final com.revenuecat.purchases.models.GalaxyReplacementMode.Companion Companion; - } - - @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public static final class GalaxyReplacementMode.Companion { - method public com.revenuecat.purchases.models.GalaxyReplacementMode getDefault(); - property public final com.revenuecat.purchases.models.GalaxyReplacementMode default; - } - @dev.drewhamilton.poko.Poko public final class GoogleInstallmentsInfo implements com.revenuecat.purchases.models.InstallmentsInfo { ctor public GoogleInstallmentsInfo(int commitmentPaymentsCount, int renewalCommitmentPaymentsCount); method public int getCommitmentPaymentsCount(); @@ -1102,23 +1089,23 @@ package com.revenuecat.purchases.models { property public final String token; } - public enum GoogleReplacementMode implements com.revenuecat.purchases.ReplacementMode { - method public int describeContents(); - method public int getPlayBillingClientMode(); - method public void writeToParcel(android.os.Parcel out, int flags); - property public final int playBillingClientMode; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_FULL_PRICE; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_PRORATED_PRICE; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode DEFERRED; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode WITHOUT_PRORATION; - enum_constant public static final com.revenuecat.purchases.models.GoogleReplacementMode WITH_TIME_PRORATION; - field public static final com.revenuecat.purchases.models.GoogleReplacementMode.CREATOR CREATOR; + @Deprecated public enum GoogleReplacementMode implements com.revenuecat.purchases.ReplacementMode { + method @Deprecated public int describeContents(); + method @Deprecated public int getPlayBillingClientMode(); + method @Deprecated public void writeToParcel(android.os.Parcel out, int flags); + property @Deprecated public final int playBillingClientMode; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_FULL_PRICE; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode CHARGE_PRORATED_PRICE; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode DEFERRED; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode WITHOUT_PRORATION; + enum_constant @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode WITH_TIME_PRORATION; + field @Deprecated public static final com.revenuecat.purchases.models.GoogleReplacementMode.CREATOR CREATOR; } - public static final class GoogleReplacementMode.CREATOR implements android.os.Parcelable.Creator { - method public com.revenuecat.purchases.models.GoogleReplacementMode? createFromParcel(android.os.Parcel in); - method public com.revenuecat.purchases.models.GoogleReplacementMode? fromPlayBillingClientMode(@com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode Integer? playBillingClientMode); - method public com.revenuecat.purchases.models.GoogleReplacementMode?[] newArray(int size); + @Deprecated public static final class GoogleReplacementMode.CREATOR implements android.os.Parcelable.Creator { + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode? createFromParcel(android.os.Parcel in); + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode? fromPlayBillingClientMode(@com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams.ReplacementMode Integer? playBillingClientMode); + method @Deprecated public com.revenuecat.purchases.models.GoogleReplacementMode?[] newArray(int size); } @dev.drewhamilton.poko.Poko public final class GoogleStoreProduct implements com.revenuecat.purchases.models.StoreProduct { @@ -1350,6 +1337,14 @@ package com.revenuecat.purchases.models { property public abstract com.revenuecat.purchases.ProductType type; } + @kotlinx.parcelize.Parcelize public enum StoreReplacementMode implements com.revenuecat.purchases.ReplacementMode { + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode CHARGE_FULL_PRICE; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode CHARGE_PRORATED_PRICE; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode DEFERRED; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode WITHOUT_PRORATION; + enum_constant public static final com.revenuecat.purchases.models.StoreReplacementMode WITH_TIME_PRORATION; + } + @dev.drewhamilton.poko.Poko @kotlinx.parcelize.Parcelize @kotlinx.parcelize.TypeParceler public final class StoreTransaction implements android.os.Parcelable { ctor public StoreTransaction(String? orderId, java.util.List productIds, com.revenuecat.purchases.ProductType type, long purchaseTime, String purchaseToken, com.revenuecat.purchases.models.PurchaseState purchaseState, Boolean? isAutoRenewing, String? signature, org.json.JSONObject originalJson, com.revenuecat.purchases.PresentedOfferingContext? presentedOfferingContext, String? storeUserID, com.revenuecat.purchases.models.PurchaseType purchaseType, String? marketplace, String? subscriptionOptionId, com.revenuecat.purchases.ReplacementMode? replacementMode); ctor @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI public StoreTransaction(String? orderId, java.util.List productIds, com.revenuecat.purchases.ProductType type, long purchaseTime, String purchaseToken, com.revenuecat.purchases.models.PurchaseState purchaseState, Boolean? isAutoRenewing, String? signature, org.json.JSONObject originalJson, com.revenuecat.purchases.PresentedOfferingContext? presentedOfferingContext, String? storeUserID, com.revenuecat.purchases.models.PurchaseType purchaseType, String? marketplace, String? subscriptionOptionId, @com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI java.util.Map? subscriptionOptionIdForProductIDs, com.revenuecat.purchases.ReplacementMode? replacementMode); diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchaseParams.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchaseParams.kt index dcb7669885..8beb16076b 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchaseParams.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchaseParams.kt @@ -3,12 +3,14 @@ package com.revenuecat.purchases import android.app.Activity import com.revenuecat.purchases.common.LogIntent import com.revenuecat.purchases.common.log -import com.revenuecat.purchases.models.GalaxyReplacementMode import com.revenuecat.purchases.models.GooglePurchasingData import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.PurchasingData import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.SubscriptionOption +import com.revenuecat.purchases.models.toGoogleReplacementMode +import com.revenuecat.purchases.models.toStoreReplacementMode import com.revenuecat.purchases.strings.PurchaseStrings import dev.drewhamilton.poko.Poko @@ -17,10 +19,11 @@ public class PurchaseParams(public val builder: Builder) { public val isPersonalizedPrice: Boolean? public val oldProductId: String? + + @Deprecated("Use replacementMode instead") public val googleReplacementMode: GoogleReplacementMode - @ExperimentalPreviewRevenueCatPurchasesAPI - public val galaxyReplacementMode: GalaxyReplacementMode + public val replacementMode: StoreReplacementMode @get:JvmSynthetic internal val purchasingData: PurchasingData @@ -43,9 +46,7 @@ public class PurchaseParams(public val builder: Builder) { this.isPersonalizedPrice = builder.isPersonalizedPrice this.oldProductId = builder.oldProductId this.googleReplacementMode = builder.googleReplacementMode - - @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) - this.galaxyReplacementMode = builder.galaxyReplacementMode + this.replacementMode = builder.replacementMode this.purchasingData = builder.purchasingData this.activity = builder.activity this.presentedOfferingContext = builder.presentedOfferingContext @@ -61,6 +62,7 @@ public class PurchaseParams(public val builder: Builder) { * - Uses [SubscriptionOption] with the longest free trial or cheapest first phase * - Falls back to use base plan */ + @Suppress("TooManyFunctions") public open class Builder private constructor( @get:JvmSynthetic internal val activity: Activity, @get:JvmSynthetic internal var purchasingData: PurchasingData, @@ -96,12 +98,12 @@ public class PurchaseParams(public val builder: Builder) { @set:JvmSynthetic @get:JvmSynthetic + @Deprecated("Use replacementMode instead") internal var googleReplacementMode: GoogleReplacementMode = GoogleReplacementMode.WITHOUT_PRORATION - @OptIn(InternalRevenueCatAPI::class, ExperimentalPreviewRevenueCatPurchasesAPI::class) @set:JvmSynthetic @get:JvmSynthetic - internal var galaxyReplacementMode: GalaxyReplacementMode = GalaxyReplacementMode.default + internal var replacementMode: StoreReplacementMode = StoreReplacementMode.WITHOUT_PRORATION /* * Sets the data about the context in which an offering was presented. @@ -133,7 +135,8 @@ public class PurchaseParams(public val builder: Builder) { * Note: When using [GoogleReplacementMode.DEFERRED], the product ID is used to match the purchase callback * with the transaction returned by Google Play. * - * Product changes are only available in the Play Store. Ignored for Amazon Appstore purchases. + * Product changes are only available in the Play Store and the Galaxy Store. + * Ignored for Amazon Appstore purchases. */ public fun oldProductId(oldProductId: String): Builder = apply { this.oldProductId = oldProductId @@ -145,19 +148,24 @@ public class PurchaseParams(public val builder: Builder) { * * Only applied for Play Store product changes. Ignored for Amazon Appstore and Galaxy Store purchases. */ + @Deprecated("Use .replacementMode() instead") public fun googleReplacementMode(googleReplacementMode: GoogleReplacementMode): Builder = apply { this.googleReplacementMode = googleReplacementMode + this.replacementMode = googleReplacementMode.toStoreReplacementMode() } - /* - * The [GalaxyReplacementMode] to use when replacing the given oldProductId. Defaults to - * [GalaxyReplacementMode.IMMEDIATE_WITHOUT_PRORATION]. + /** + * The [StoreReplacementMode] to use when replacing the given oldProductId. Defaults to + * [StoreReplacementMode.WITHOUT_PRORATION]. * - * Only applied for Galaxy Store product changes. Ignored for Google Play and Amazon Appstore purchases. + * Refer to the [StoreReplacementMode] docs for a list of + * supported replacement modes for each store. */ - @ExperimentalPreviewRevenueCatPurchasesAPI - public fun galaxyReplacementMode(galaxyReplacementMode: GalaxyReplacementMode): Builder = apply { - this.galaxyReplacementMode = galaxyReplacementMode + public fun replacementMode(replacementMode: StoreReplacementMode): Builder = apply { + this.replacementMode = replacementMode + + // We can remove this once we fully remove PurchaseParams.googleReplacementMode + this.googleReplacementMode = replacementMode.toGoogleReplacementMode() } /* diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesOrchestrator.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesOrchestrator.kt index 33f4939fb9..65133223b4 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesOrchestrator.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/PurchasesOrchestrator.kt @@ -73,13 +73,12 @@ import com.revenuecat.purchases.interfaces.SyncAttributesAndOfferingsCallback import com.revenuecat.purchases.interfaces.SyncPurchasesCallback import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener import com.revenuecat.purchases.models.BillingFeature -import com.revenuecat.purchases.models.GalaxyReplacementMode import com.revenuecat.purchases.models.GooglePurchasingData -import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.GoogleStoreProduct import com.revenuecat.purchases.models.InAppMessageType import com.revenuecat.purchases.models.PurchasingData import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.paywalls.DownloadedFontFamily import com.revenuecat.purchases.paywalls.FontLoader @@ -630,8 +629,7 @@ internal class PurchasesOrchestrator( purchasingData, presentedOfferingContext, productId, - googleReplacementMode, - galaxyReplacementMode, + replacementMode, isPersonalizedPrice, callback, ) @@ -1524,8 +1522,7 @@ internal class PurchasesOrchestrator( purchasingData: PurchasingData, presentedOfferingContext: PresentedOfferingContext?, oldProductId: String, - googleReplacementMode: GoogleReplacementMode, - galaxyReplacementMode: GalaxyReplacementMode, + replacementMode: StoreReplacementMode, isPersonalizedPrice: Boolean?, purchaseCallback: PurchaseCallback, ) { @@ -1560,7 +1557,7 @@ internal class PurchasesOrchestrator( presentedOfferingContext?.offeringIdentifier?.let { PurchaseStrings.OFFERING + "$it" } - } oldProductId: $oldProductId googleReplacementMode $googleReplacementMode", + } oldProductId: $oldProductId replacementMode $replacementMode", ) } var userPurchasing: String? = null // Avoids race condition for userid being modified before purchase is made @@ -1575,7 +1572,7 @@ internal class PurchasesOrchestrator( // We also need to normalize oldProductId by stripping any basePlanId suffix // (e.g., "productId:basePlanId" becomes "productId") to ensure the callback key matches the productId // in the transaction returned by Google Play, which only contains the product ID without the base plan. - val productId = if (googleReplacementMode == GoogleReplacementMode.DEFERRED) { + val productId = if (replacementMode == StoreReplacementMode.DEFERRED && store == Store.PLAY_STORE) { if (oldProductId.contains(Constants.SUBS_ID_BASE_PLAN_ID_SEPARATOR)) { warnLog { PurchaseStrings.DEFERRED_PRODUCT_CHANGE_WITH_BASE_PLAN_ID.format(oldProductId) @@ -1593,11 +1590,6 @@ internal class PurchasesOrchestrator( } } userPurchasing?.let { appUserID -> - val replacementMode: ReplacementMode? = when (store) { - Store.PLAY_STORE -> googleReplacementMode - Store.GALAXY -> galaxyReplacementMode - else -> null - } replaceOldPurchaseWithNewProduct( purchasingData, oldProductId, @@ -1619,7 +1611,7 @@ internal class PurchasesOrchestrator( private fun replaceOldPurchaseWithNewProduct( purchasingData: PurchasingData, oldProductId: String, - replacementMode: ReplacementMode?, + replacementMode: StoreReplacementMode?, activity: Activity, appUserID: String, presentedOfferingContext: PresentedOfferingContext?, diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/ReplacementMode.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/ReplacementMode.kt index 6eb51a1e47..b9b12a11b1 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/ReplacementMode.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/ReplacementMode.kt @@ -1,8 +1,12 @@ package com.revenuecat.purchases import android.os.Parcelable -import com.revenuecat.purchases.models.GalaxyReplacementMode import com.revenuecat.purchases.models.GoogleReplacementMode +import com.revenuecat.purchases.models.StoreReplacementMode +import com.revenuecat.purchases.models.legacyPlayBackendName +import com.revenuecat.purchases.models.storeBackendName +import com.revenuecat.purchases.models.toStoreReplacementMode +import com.revenuecat.purchases.models.toStoreReplacementModeOrNull import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.serialization.KSerializer import kotlinx.serialization.SerializationException @@ -24,39 +28,24 @@ public interface ReplacementMode : Parcelable { public val name: String } -/** - * [GoogleReplacementMode] used to be `GoogleProrationMode`. The backend still expects these values, hence this enum. - */ -private enum class LegacyProrationMode { - IMMEDIATE_WITHOUT_PRORATION, - IMMEDIATE_WITH_TIME_PRORATION, - IMMEDIATE_AND_CHARGE_FULL_PRICE, - IMMEDIATE_AND_CHARGE_PRORATED_PRICE, - DEFERRED, -} - -private val GoogleReplacementMode.asLegacyProrationMode: LegacyProrationMode - get() = when (this) { - GoogleReplacementMode.WITHOUT_PRORATION -> LegacyProrationMode.IMMEDIATE_WITHOUT_PRORATION - GoogleReplacementMode.WITH_TIME_PRORATION -> LegacyProrationMode.IMMEDIATE_WITH_TIME_PRORATION - GoogleReplacementMode.CHARGE_FULL_PRICE -> LegacyProrationMode.IMMEDIATE_AND_CHARGE_FULL_PRICE - GoogleReplacementMode.CHARGE_PRORATED_PRICE -> LegacyProrationMode.IMMEDIATE_AND_CHARGE_PRORATED_PRICE - GoogleReplacementMode.DEFERRED -> LegacyProrationMode.DEFERRED - } - /** * Returns the backend name for this [ReplacementMode]. * For [GoogleReplacementMode], this returns the legacy proration mode name. - * For [GalaxyReplacementMode], this returns the enum name directly. */ @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) +@Deprecated("Use ReplacementMode.backendName(store: Store) instead") internal val ReplacementMode.backendName: String get() = when (this) { - is GoogleReplacementMode -> this.asLegacyProrationMode.name - is GalaxyReplacementMode -> this.name + is GoogleReplacementMode -> this.toStoreReplacementMode().legacyPlayBackendName() + is StoreReplacementMode -> this.legacyPlayBackendName() else -> this.name } +@OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) +internal fun ReplacementMode.backendName(store: Store): String? { + return this.toStoreReplacementModeOrNull()?.storeBackendName(store) +} + internal object ReplacementModeSerializer : KSerializer { override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ReplacementMode") { element("type", String.serializer().descriptor) @@ -64,13 +53,15 @@ internal object ReplacementModeSerializer : KSerializer { } override fun serialize(encoder: Encoder, value: ReplacementMode) { + // Always encode to StoreReplacementMode since GoogleReplacementMode is deprecated encoder.encodeStructure(descriptor) { - val type = when (value) { - is GoogleReplacementMode -> "GoogleReplacementMode" + val normalizedValue = when (value) { + is GoogleReplacementMode -> value.toStoreReplacementMode() + is StoreReplacementMode -> value else -> throw SerializationException("Unknown ReplacementMode type: ${value::class.simpleName}") } - encodeStringElement(descriptor, 0, type) - encodeStringElement(descriptor, 1, value.name) + encodeStringElement(descriptor, 0, "StoreReplacementMode") + encodeStringElement(descriptor, 1, normalizedValue.name) } } @@ -89,9 +80,17 @@ internal object ReplacementModeSerializer : KSerializer { } when (type) { + "StoreReplacementMode" -> { + try { + StoreReplacementMode.valueOf(name) + } catch (e: IllegalArgumentException) { + throw SerializationException("Invalid StoreReplacementMode name: $name", e) + } + } "GoogleReplacementMode" -> { try { - GoogleReplacementMode.valueOf(name) + // Parse GoogleReplacementMode to StoreReplacementMode + GoogleReplacementMode.valueOf(name).toStoreReplacementMode() } catch (e: IllegalArgumentException) { throw SerializationException("Invalid GoogleReplacementMode name: $name", e) } diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/common/Backend.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/common/Backend.kt index b406df69b2..c225a11d6d 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/common/Backend.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/common/Backend.kt @@ -292,7 +292,7 @@ internal class Backend( "normal_duration" to receiptInfo.duration, "store_user_id" to receiptInfo.storeUserID, "pricing_phases" to receiptInfo.pricingPhases?.map { it.toMap() }, - "proration_mode" to receiptInfo.replacementMode?.backendName, + "proration_mode" to receiptInfo.replacementMode?.backendName(store = appConfig.store), "initiation_source" to initiationSource.postReceiptFieldValue, "paywall" to paywallPostReceiptData?.toMap(), "sdk_originated" to receiptInfo.sdkOriginated, diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/google/BillingFlowParamsExtensions.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/google/BillingFlowParamsExtensions.kt index 71ab666a8c..4d436b37f5 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/google/BillingFlowParamsExtensions.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/google/BillingFlowParamsExtensions.kt @@ -4,19 +4,18 @@ import com.android.billingclient.api.BillingFlowParams import com.revenuecat.purchases.InternalRevenueCatAPI import com.revenuecat.purchases.common.ReplaceProductInfo import com.revenuecat.purchases.common.errorLog -import com.revenuecat.purchases.models.GoogleReplacementMode +import com.revenuecat.purchases.models.toPlayBillingClientMode +import com.revenuecat.purchases.models.toStoreReplacementModeOrNull @OptIn(InternalRevenueCatAPI::class) internal fun BillingFlowParams.Builder.setUpgradeInfo(replaceProductInfo: ReplaceProductInfo) { val subscriptionUpdateParams = BillingFlowParams.SubscriptionUpdateParams.newBuilder().apply { setOldPurchaseToken(replaceProductInfo.oldPurchase.purchaseToken) - replaceProductInfo.replacementMode?.let { - val googleReplacementMode = it as? GoogleReplacementMode - if (googleReplacementMode == null) { - errorLog { "Got non-Google replacement mode" } - } else { - setSubscriptionReplacementMode(googleReplacementMode.playBillingClientMode) - } + val replacementMode = replaceProductInfo.replacementMode.toStoreReplacementModeOrNull() + if (replacementMode != null) { + setSubscriptionReplacementMode(replacementMode.toPlayBillingClientMode()) + } else if (replaceProductInfo.replacementMode != null) { + errorLog { "Got unidentified replacement mode" } } } setSubscriptionUpdateParams(subscriptionUpdateParams.build()) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/google/BillingWrapper.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/google/BillingWrapper.kt index 08d0dc1918..6721bc9d29 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/google/BillingWrapper.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/google/BillingWrapper.kt @@ -67,6 +67,7 @@ import com.revenuecat.purchases.models.GoogleReplacementMode 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.BillingStrings import com.revenuecat.purchases.strings.OfferingStrings @@ -296,7 +297,9 @@ internal class BillingWrapper( // When using DEFERRED proration mode, callback needs to be associated with the *old* product we are // switching from, because the transaction we receive on successful purchase is for the old product. val productId = - if (replaceProductInfo?.replacementMode == GoogleReplacementMode.DEFERRED) { + if (replaceProductInfo?.replacementMode == StoreReplacementMode.DEFERRED || + replaceProductInfo?.replacementMode == GoogleReplacementMode.DEFERRED + ) { replaceProductInfo.oldPurchase.productIds.first() } else { googlePurchasingData.productId @@ -320,7 +323,7 @@ internal class BillingWrapper( googlePurchasingData.productType, presentedOfferingContext, subscriptionOptionId, - replaceProductInfo?.replacementMode as? GoogleReplacementMode?, + replaceProductInfo?.replacementMode as? StoreReplacementMode?, subscriptionOptionIdForProductIDs, ) } diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/google/PurchaseContext.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/google/PurchaseContext.kt index e2c72aac14..a19c919390 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/google/PurchaseContext.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/google/PurchaseContext.kt @@ -2,12 +2,12 @@ package com.revenuecat.purchases.google import com.revenuecat.purchases.PresentedOfferingContext import com.revenuecat.purchases.ProductType -import com.revenuecat.purchases.models.GoogleReplacementMode +import com.revenuecat.purchases.models.StoreReplacementMode internal class PurchaseContext( val productType: ProductType, val presentedOfferingContext: PresentedOfferingContext?, val selectedSubscriptionOptionId: String?, - val replacementMode: GoogleReplacementMode?, + val replacementMode: StoreReplacementMode?, val subscriptionOptionIdForProductIDs: Map?, ) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/google/storeTransactionConversions.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/google/storeTransactionConversions.kt index 8a0b8751b7..e91e0482fb 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/google/storeTransactionConversions.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/google/storeTransactionConversions.kt @@ -4,8 +4,8 @@ import com.android.billingclient.api.Purchase import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI import com.revenuecat.purchases.PresentedOfferingContext import com.revenuecat.purchases.ProductType -import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.PurchaseType +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import org.json.JSONObject @@ -15,7 +15,7 @@ internal fun Purchase.toStoreTransaction( presentedOfferingContext: PresentedOfferingContext? = null, subscriptionOptionId: String? = null, subscriptionOptionIdForProductIDs: Map? = null, - replacementMode: GoogleReplacementMode? = null, + replacementMode: StoreReplacementMode? = null, ): StoreTransaction = StoreTransaction( orderId = this.orderId, productIds = this.products, diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/models/GalaxyReplacementMode.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/models/GalaxyReplacementMode.kt deleted file mode 100644 index 97d4177935..0000000000 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/models/GalaxyReplacementMode.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.revenuecat.purchases.models - -import com.revenuecat.purchases.ExperimentalPreviewRevenueCatPurchasesAPI -import com.revenuecat.purchases.InternalRevenueCatAPI -import com.revenuecat.purchases.ReplacementMode -import kotlinx.parcelize.Parcelize - -/** - * Enum of possible replacement modes to be passed to a Samsung Galaxy Store subscription change. - * Not used for Google Play and Amazon purchases. - * - * See https://developer.samsung.com/iap/subscription-guide/manage-subscription-plan/proration-modes.html - * for more details. - */ -@ExperimentalPreviewRevenueCatPurchasesAPI -@Parcelize -public enum class GalaxyReplacementMode : ReplacementMode { - /** - * The current subscription is instantly changed and the customer can start using the new subscription - * immediately. The remaining payment of the original subscription is prorated to the cost of the new - * subscription (based on the daily price). The payment (renewal) date and starting day of the subscription - * period are changed based on this calculation. - * - * This mode can be used for both upgrades and downgrades. - */ - INSTANT_PRORATED_DATE, - - /** - * For upgraded subscriptions only. The current subscription is instantly changed and the customer can - * start using the new subscription immediately. While the starting day of the subscription period and - * payment (renewal) date remain the same, the prorated cost of the upgraded subscription for the remainder - * of the subscription period (minus the remaining payment of the original subscription) is immediately - * charged to the customer. - */ - INSTANT_PRORATED_CHARGE, - - /** - * For upgraded subscriptions only. The current subscription is instantly changed and the customer can - * start using the new subscription immediately. The new subscription rate is not applied until the current - * subscription period ends. The payment (renewal) date remains the same. There are no extra charges to use - * the upgraded subscription during the current subscription period. - * - * This is the default behavior. - */ - INSTANT_NO_PRORATION, - - /** - * The current subscription continues and the features of the new subscription are not available until - * the current subscription period ends. The new subscription price is charged and the features of the - * new subscription are available when the subscription is renewed. When the customer changes their - * subscription, they cannot change the subscription again during the remaining time of the current - * subscription period. - */ - DEFERRED, - ; - - @ExperimentalPreviewRevenueCatPurchasesAPI - public companion object { - /** - * The default replacement mode for Galaxy Store subscription changes. - */ - @InternalRevenueCatAPI - public val default: GalaxyReplacementMode = INSTANT_NO_PRORATION - } -} diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/models/GoogleReplacementMode.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/models/GoogleReplacementMode.kt index a657fd61da..748ee8ecb6 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/models/GoogleReplacementMode.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/models/GoogleReplacementMode.kt @@ -11,6 +11,7 @@ import com.revenuecat.purchases.ReplacementMode * * See https://developer.android.com/google/play/billing/subscriptions#proration for examples */ +@Deprecated("Use StoreReplacementMode instead") public enum class GoogleReplacementMode( @BillingFlowParams.SubscriptionUpdateParams.ReplacementMode public val playBillingClientMode: Int, ) : ReplacementMode { diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/models/StoreReplacementMode.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/models/StoreReplacementMode.kt new file mode 100644 index 0000000000..5e5c2c2993 --- /dev/null +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/models/StoreReplacementMode.kt @@ -0,0 +1,46 @@ +package com.revenuecat.purchases.models + +import com.revenuecat.purchases.ReplacementMode +import kotlinx.parcelize.Parcelize + +/** + * Enum of possible replacement modes to be used when performing a subscription product change. + */ +@Parcelize +public enum class StoreReplacementMode : ReplacementMode { + /** + * Old subscription is cancelled, and new subscription takes effect immediately. + * User is charged for the full price of the new subscription on the old subscription's expiration date. + * + * This is the default behavior for the Galaxy and Play stores. + */ + WITHOUT_PRORATION, + + /** + * Old subscription is cancelled, and new subscription takes effect immediately. + * Any time remaining on the old subscription is used to push out the first payment date for the new subscription. + * User is charged the full price of new subscription once that prorated time has passed. + * + * For the Play Store, the purchase will fail if this mode is used when switching between [SubscriptionOption]s + * of the same [StoreProduct]. + */ + WITH_TIME_PRORATION, + + /** + * Replacement takes effect immediately, and the user is charged full price of new plan and is + * given a full billing cycle of subscription, plus remaining prorated time from the old plan. + * + * This mode is not supported by the Galaxy Store. If it is passed to the Galaxy Store, an error will be thrown. + */ + CHARGE_FULL_PRICE, + + /** + * Replacement takes effect immediately, and the billing cycle remains the same. + */ + CHARGE_PRORATED_PRICE, + + /** + * Replacement takes effect when the old plan expires, and the new price will be charged at the same time. + */ + DEFERRED, +} diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/models/StoreReplacementModeConversions.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/models/StoreReplacementModeConversions.kt new file mode 100644 index 0000000000..10c8f50399 --- /dev/null +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/models/StoreReplacementModeConversions.kt @@ -0,0 +1,90 @@ +package com.revenuecat.purchases.models + +import com.android.billingclient.api.BillingFlowParams +import com.revenuecat.purchases.ReplacementMode +import com.revenuecat.purchases.Store + +private data class StoreReplacementModeMapping( + @get:BillingFlowParams.SubscriptionUpdateParams.ReplacementMode + val playBillingClientMode: Int, + val legacyPlayBackendName: String, + val galaxyBackendName: String?, + val googleReplacementMode: GoogleReplacementMode, +) + +// This mapping table doesn't include the Galaxy mappings from StoreReplacementMode -> HelperDefine.ProrationMode +// because the Galaxy SDK isn't available in the main purchases module. Those conversions are located in the galaxy +// module's StoreReplacementModeConversions.kt file. +private val StoreReplacementMode.mapping: StoreReplacementModeMapping + get() = when (this) { + StoreReplacementMode.WITHOUT_PRORATION -> StoreReplacementModeMapping( + playBillingClientMode = BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION, + legacyPlayBackendName = "IMMEDIATE_WITHOUT_PRORATION", + galaxyBackendName = "INSTANT_NO_PRORATION", + googleReplacementMode = GoogleReplacementMode.WITHOUT_PRORATION, + ) + StoreReplacementMode.WITH_TIME_PRORATION -> StoreReplacementModeMapping( + playBillingClientMode = BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION, + legacyPlayBackendName = "IMMEDIATE_WITH_TIME_PRORATION", + galaxyBackendName = "INSTANT_PRORATED_DATE", + googleReplacementMode = GoogleReplacementMode.WITH_TIME_PRORATION, + ) + StoreReplacementMode.CHARGE_FULL_PRICE -> StoreReplacementModeMapping( + playBillingClientMode = BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE, + legacyPlayBackendName = "IMMEDIATE_AND_CHARGE_FULL_PRICE", + galaxyBackendName = null, + googleReplacementMode = GoogleReplacementMode.CHARGE_FULL_PRICE, + ) + StoreReplacementMode.CHARGE_PRORATED_PRICE -> StoreReplacementModeMapping( + playBillingClientMode = BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE, + legacyPlayBackendName = "IMMEDIATE_AND_CHARGE_PRORATED_PRICE", + galaxyBackendName = "INSTANT_PRORATED_CHARGE", + googleReplacementMode = GoogleReplacementMode.CHARGE_PRORATED_PRICE, + ) + StoreReplacementMode.DEFERRED -> StoreReplacementModeMapping( + playBillingClientMode = BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.DEFERRED, + legacyPlayBackendName = "DEFERRED", + galaxyBackendName = "DEFERRED", + googleReplacementMode = GoogleReplacementMode.DEFERRED, + ) + } + +@BillingFlowParams.SubscriptionUpdateParams.ReplacementMode +internal fun StoreReplacementMode.toPlayBillingClientMode(): Int { + return mapping.playBillingClientMode +} + +internal fun StoreReplacementMode.legacyPlayBackendName(): String { + return mapping.legacyPlayBackendName +} + +internal fun StoreReplacementMode.storeBackendName(store: Store): String? { + return when (store) { + Store.PLAY_STORE -> legacyPlayBackendName() + Store.GALAXY -> mapping.galaxyBackendName + else -> null + } +} + +internal fun StoreReplacementMode.toGoogleReplacementMode(): GoogleReplacementMode { + return mapping.googleReplacementMode +} + +internal fun GoogleReplacementMode.toStoreReplacementMode(): StoreReplacementMode { + return when (this) { + GoogleReplacementMode.WITHOUT_PRORATION -> StoreReplacementMode.WITHOUT_PRORATION + GoogleReplacementMode.WITH_TIME_PRORATION -> StoreReplacementMode.WITH_TIME_PRORATION + GoogleReplacementMode.CHARGE_FULL_PRICE -> StoreReplacementMode.CHARGE_FULL_PRICE + GoogleReplacementMode.CHARGE_PRORATED_PRICE -> StoreReplacementMode.CHARGE_PRORATED_PRICE + GoogleReplacementMode.DEFERRED -> StoreReplacementMode.DEFERRED + } +} + +internal fun ReplacementMode?.toStoreReplacementModeOrNull(): StoreReplacementMode? { + return when (this) { + null -> null + is StoreReplacementMode -> this + is GoogleReplacementMode -> this.toStoreReplacementMode() + else -> null + } +} diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/ProductChangeConfig.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/ProductChangeConfig.kt index 20e07c6cfd..7fdb8a97e4 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/ProductChangeConfig.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/ProductChangeConfig.kt @@ -1,7 +1,7 @@ package com.revenuecat.purchases.paywalls.components.common import com.revenuecat.purchases.InternalRevenueCatAPI -import com.revenuecat.purchases.models.GoogleReplacementMode +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.paywalls.components.common.serializers.DowngradeReplacementModeDeserializer import com.revenuecat.purchases.paywalls.components.common.serializers.UpgradeReplacementModeDeserializer import dev.drewhamilton.poko.Poko @@ -26,7 +26,7 @@ public class ProductChangeConfig( @get:JvmSynthetic @Serializable(with = UpgradeReplacementModeDeserializer::class) @SerialName("upgrade_replacement_mode") - public val upgradeReplacementMode: GoogleReplacementMode = GoogleReplacementMode.CHARGE_PRORATED_PRICE, + public val upgradeReplacementMode: StoreReplacementMode = StoreReplacementMode.CHARGE_PRORATED_PRICE, /** * Replacement mode to use for downgrades (moving to a lower price per unit time). @@ -35,5 +35,5 @@ public class ProductChangeConfig( @get:JvmSynthetic @Serializable(with = DowngradeReplacementModeDeserializer::class) @SerialName("downgrade_replacement_mode") - public val downgradeReplacementMode: GoogleReplacementMode = GoogleReplacementMode.DEFERRED, + public val downgradeReplacementMode: StoreReplacementMode = StoreReplacementMode.DEFERRED, ) diff --git a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/serializers/ReplacementModeDeserializers.kt b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/serializers/ReplacementModeDeserializers.kt index 088f62d678..346692fab1 100644 --- a/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/serializers/ReplacementModeDeserializers.kt +++ b/purchases/src/main/kotlin/com/revenuecat/purchases/paywalls/components/common/serializers/ReplacementModeDeserializers.kt @@ -1,14 +1,14 @@ package com.revenuecat.purchases.paywalls.components.common.serializers -import com.revenuecat.purchases.models.GoogleReplacementMode +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.utils.serializers.EnumDeserializerWithDefault -internal object UpgradeReplacementModeDeserializer : EnumDeserializerWithDefault( - defaultValue = GoogleReplacementMode.CHARGE_PRORATED_PRICE, +internal object UpgradeReplacementModeDeserializer : EnumDeserializerWithDefault( + defaultValue = StoreReplacementMode.CHARGE_PRORATED_PRICE, typeForValue = { value -> value.name.lowercase() }, ) -internal object DowngradeReplacementModeDeserializer : EnumDeserializerWithDefault( - defaultValue = GoogleReplacementMode.DEFERRED, +internal object DowngradeReplacementModeDeserializer : EnumDeserializerWithDefault( + defaultValue = StoreReplacementMode.DEFERRED, typeForValue = { value -> value.name.lowercase() }, ) diff --git a/purchases/src/test/java/com/revenuecat/purchases/BasePurchasesTest.kt b/purchases/src/test/java/com/revenuecat/purchases/BasePurchasesTest.kt index c543919b38..dca7ff3468 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/BasePurchasesTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/BasePurchasesTest.kt @@ -35,6 +35,7 @@ import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback import com.revenuecat.purchases.interfaces.UpdatedCustomerInfoListener import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.models.SubscriptionOption import com.revenuecat.purchases.paywalls.FontLoader @@ -549,7 +550,8 @@ internal open class BasePurchasesTest { purchaseable: Any, oldProductId: String? = null, isPersonalizedPrice: Boolean? = null, - googleReplacementMode: GoogleReplacementMode? = null + googleReplacementMode: GoogleReplacementMode? = null, + storeReplacementMode: StoreReplacementMode? = null, ): PurchaseParams { val builder = when (purchaseable) { is SubscriptionOption -> PurchaseParams.Builder(mockActivity, purchaseable) @@ -569,6 +571,9 @@ internal open class BasePurchasesTest { googleReplacementMode?.let { builder!!.googleReplacementMode(googleReplacementMode) } + storeReplacementMode?.let { + builder!!.replacementMode(storeReplacementMode) + } return builder!!.build() } diff --git a/purchases/src/test/java/com/revenuecat/purchases/ParcelableTests.kt b/purchases/src/test/java/com/revenuecat/purchases/ParcelableTests.kt index 88ac5dd207..181f4a8eab 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/ParcelableTests.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/ParcelableTests.kt @@ -6,6 +6,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.PurchaseState import com.revenuecat.purchases.models.PurchaseType +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.utils.JSONObjectParceler import com.revenuecat.purchases.utils.JSONObjectParceler.write @@ -95,6 +96,13 @@ class ParcelableTests { testParcelization(nullMode, true) } + @Test + fun `StoreReplacementMode is Parcelable`() { + StoreReplacementMode.values().forEach { testParcelization(it, true) } + val nullMode: StoreReplacementMode? = null + testParcelization(nullMode, true) + } + @Test fun `VirtualCurrency is Parcelable`() { testParcelization( diff --git a/purchases/src/test/java/com/revenuecat/purchases/PostReceiptHelperTest.kt b/purchases/src/test/java/com/revenuecat/purchases/PostReceiptHelperTest.kt index eef5b27d5a..c687e492d1 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/PostReceiptHelperTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/PostReceiptHelperTest.kt @@ -20,8 +20,8 @@ import com.revenuecat.purchases.common.networking.PostReceiptProductInfo import com.revenuecat.purchases.common.networking.PostReceiptResponse import com.revenuecat.purchases.common.offlineentitlements.OfflineEntitlementsManager import com.revenuecat.purchases.google.toStoreTransaction -import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.Period +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.paywalls.PaywallPresentedCache import com.revenuecat.purchases.paywalls.events.PaywallEvent @@ -76,13 +76,13 @@ class PostReceiptHelperTest { ProductType.SUBS, null, subscriptionOptionId, - replacementMode = GoogleReplacementMode.CHARGE_FULL_PRICE + replacementMode = StoreReplacementMode.CHARGE_FULL_PRICE ) private val mockPendingStoreTransaction = mockPendingPurchase.toStoreTransaction( ProductType.SUBS, null, subscriptionOptionId, - replacementMode = GoogleReplacementMode.CHARGE_FULL_PRICE + replacementMode = StoreReplacementMode.CHARGE_FULL_PRICE ) private val testReceiptInfo = ReceiptInfo( productIDs = listOf("test-product-id-1", "test-product-id-2"), @@ -757,7 +757,7 @@ class PostReceiptHelperTest { onError = { _, _ -> fail("Should succeed") } ) assertThat(postedReceiptInfoSlot.isCaptured).isTrue - assertThat(postedReceiptInfoSlot.captured.replacementMode).isEqualTo(GoogleReplacementMode.CHARGE_FULL_PRICE) + assertThat(postedReceiptInfoSlot.captured.replacementMode).isEqualTo(StoreReplacementMode.CHARGE_FULL_PRICE) } @Test @@ -1475,7 +1475,7 @@ class PostReceiptHelperTest { ProductType.SUBS, null, subscriptionOptionId, - replacementMode = GoogleReplacementMode.CHARGE_FULL_PRICE, + replacementMode = StoreReplacementMode.CHARGE_FULL_PRICE, ) every { billing.consumeAndSave( @@ -1528,7 +1528,7 @@ class PostReceiptHelperTest { ProductType.SUBS, null, subscriptionOptionId, - replacementMode = GoogleReplacementMode.CHARGE_FULL_PRICE, + replacementMode = StoreReplacementMode.CHARGE_FULL_PRICE, ) every { billing.consumeAndSave( @@ -1734,7 +1734,7 @@ class PostReceiptHelperTest { ProductType.SUBS, PresentedOfferingContext("offering_id"), subscriptionOptionId, - replacementMode = GoogleReplacementMode.CHARGE_FULL_PRICE, + replacementMode = StoreReplacementMode.CHARGE_FULL_PRICE, ) paywallPresentedCache.receiveEvent(event) @@ -2077,7 +2077,7 @@ class PostReceiptHelperTest { currency = "USD", period = Period(1, Period.Unit.YEAR, "P1Y"), pricingPhases = null, - replacementMode = GoogleReplacementMode.DEFERRED, + replacementMode = StoreReplacementMode.DEFERRED, platformProductIds = listOf(mapOf("product_id" to "cached-product")), ) val cachedMetadata = LocalTransactionMetadata( @@ -2165,7 +2165,7 @@ class PostReceiptHelperTest { ProductType.SUBS, null, // No presentedOfferingContext subscriptionOptionId, - replacementMode = GoogleReplacementMode.CHARGE_FULL_PRICE, + replacementMode = StoreReplacementMode.CHARGE_FULL_PRICE, ) // Create a paywall event with presentedOfferingContext @@ -2219,7 +2219,7 @@ class PostReceiptHelperTest { ProductType.SUBS, null, // No presentedOfferingContext subscriptionOptionId, - replacementMode = GoogleReplacementMode.CHARGE_FULL_PRICE, + replacementMode = StoreReplacementMode.CHARGE_FULL_PRICE, ) // Don't add any paywall event to the cache (no paywall context available) @@ -2255,7 +2255,7 @@ class PostReceiptHelperTest { ProductType.SUBS, transactionContext, subscriptionOptionId, - replacementMode = GoogleReplacementMode.CHARGE_FULL_PRICE, + replacementMode = StoreReplacementMode.CHARGE_FULL_PRICE, ) // Create a paywall event with a different presentedOfferingContext diff --git a/purchases/src/test/java/com/revenuecat/purchases/PurchaseParamsTest.kt b/purchases/src/test/java/com/revenuecat/purchases/PurchaseParamsTest.kt index 4a9ab63e45..6c2d78ffa6 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/PurchaseParamsTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/PurchaseParamsTest.kt @@ -7,10 +7,11 @@ package com.revenuecat.purchases import androidx.test.ext.junit.runners.AndroidJUnit4 import com.revenuecat.purchases.models.GooglePurchasingData +import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.GoogleSubscriptionOption -import com.revenuecat.purchases.models.GalaxyReplacementMode import com.revenuecat.purchases.models.PurchasingData import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.SubscriptionOption import com.revenuecat.purchases.utils.STUB_OFFERING_IDENTIFIER import com.revenuecat.purchases.utils.stubINAPPStoreProduct @@ -108,30 +109,84 @@ class PurchaseParamsTest { assertThat(purchasePackageParams.purchasingData).isEqualTo(expectedPurchasingData) } - @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) @Test - fun `initializing defaults galaxyReplacementMode to default`() { + fun `initializing defaults replacementMode to WITHOUT_PRORATION`() { val storeProduct = stubStoreProduct("abc") val purchaseParams = PurchaseParams.Builder( mockk(), storeProduct ).build() - assertThat(purchaseParams.galaxyReplacementMode).isEqualTo(GalaxyReplacementMode.default) + assertThat(purchaseParams.replacementMode).isEqualTo(StoreReplacementMode.WITHOUT_PRORATION) } - @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) @Test - fun `galaxyReplacementMode set on builder is reflected in PurchaseParams`() { + fun `replacementMode set on builder is reflected in PurchaseParams`() { val storeProduct = stubStoreProduct("abc") - val purchaseParams = PurchaseParams.Builder( - mockk(), - storeProduct + + StoreReplacementMode.values().forEach { replacementMode -> + val purchaseParams = PurchaseParams.Builder( + mockk(), + storeProduct + ) + .replacementMode(replacementMode) + .build() + + assertThat(purchaseParams.replacementMode).isEqualTo(replacementMode) + } + } + + // We can remove this test once we fully remove PurchaseParams.googleReplacementMode + @Test + fun `replacementMode set on builder updates googleReplacementMode in PurchaseParams`() { + val storeProduct = stubStoreProduct("abc") + val expectations = mapOf( + StoreReplacementMode.WITHOUT_PRORATION to GoogleReplacementMode.WITHOUT_PRORATION, + StoreReplacementMode.WITH_TIME_PRORATION to GoogleReplacementMode.WITH_TIME_PRORATION, + StoreReplacementMode.CHARGE_FULL_PRICE to GoogleReplacementMode.CHARGE_FULL_PRICE, + StoreReplacementMode.CHARGE_PRORATED_PRICE to GoogleReplacementMode.CHARGE_PRORATED_PRICE, + StoreReplacementMode.DEFERRED to GoogleReplacementMode.DEFERRED, ) - .galaxyReplacementMode(GalaxyReplacementMode.INSTANT_PRORATED_DATE) - .build() - assertThat(purchaseParams.galaxyReplacementMode).isEqualTo(GalaxyReplacementMode.INSTANT_PRORATED_DATE) + StoreReplacementMode.values().forEach { replacementMode -> + val purchaseParams = PurchaseParams.Builder( + mockk(), + storeProduct + ) + .replacementMode(replacementMode) + .build() + + val expectedGoogleReplacementMode = + expectations[replacementMode] ?: error("Missing expected Google replacement mode for $replacementMode") + assertThat(purchaseParams.googleReplacementMode).isEqualTo(expectedGoogleReplacementMode) + } + } + + // We can remove this test once we fully remove PurchaseParams.googleReplacementMode + @Test + fun `googleReplacementMode set on builder updates replacementMode in PurchaseParams`() { + val storeProduct = stubStoreProduct("abc") + val expectations = mapOf( + GoogleReplacementMode.WITHOUT_PRORATION to StoreReplacementMode.WITHOUT_PRORATION, + GoogleReplacementMode.WITH_TIME_PRORATION to StoreReplacementMode.WITH_TIME_PRORATION, + GoogleReplacementMode.CHARGE_FULL_PRICE to StoreReplacementMode.CHARGE_FULL_PRICE, + GoogleReplacementMode.CHARGE_PRORATED_PRICE to StoreReplacementMode.CHARGE_PRORATED_PRICE, + GoogleReplacementMode.DEFERRED to StoreReplacementMode.DEFERRED, + ) + + GoogleReplacementMode.values().forEach { googleReplacementMode -> + val purchaseParams = PurchaseParams.Builder( + mockk(), + storeProduct + ) + .googleReplacementMode(googleReplacementMode) + .build() + + val expectedStoreReplacementMode = + expectations[googleReplacementMode] + ?: error("Missing expected store replacement mode for $googleReplacementMode") + assertThat(purchaseParams.replacementMode).isEqualTo(expectedStoreReplacementMode) + } } // region Add-Ons diff --git a/purchases/src/test/java/com/revenuecat/purchases/PurchasesCommonTest.kt b/purchases/src/test/java/com/revenuecat/purchases/PurchasesCommonTest.kt index 46553686ce..1c64e2c561 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/PurchasesCommonTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/PurchasesCommonTest.kt @@ -16,8 +16,6 @@ import com.revenuecat.purchases.google.toInAppStoreProduct import com.revenuecat.purchases.google.toStoreProduct import com.revenuecat.purchases.interfaces.GetStoreProductsCallback import com.revenuecat.purchases.interfaces.ReceiveCustomerInfoCallback -import com.revenuecat.purchases.models.GoogleReplacementMode -import com.revenuecat.purchases.models.GalaxyReplacementMode import com.revenuecat.purchases.models.GoogleStoreProduct import com.revenuecat.purchases.models.GoogleSubscriptionOption import com.revenuecat.purchases.models.Period @@ -25,6 +23,7 @@ import com.revenuecat.purchases.models.Price import com.revenuecat.purchases.models.PricingPhase import com.revenuecat.purchases.models.RecurrenceMode import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.models.SubscriptionOptions import com.revenuecat.purchases.strings.PurchaseStrings @@ -812,7 +811,7 @@ internal class PurchasesCommonTest: BasePurchasesTest() { val productChangeParams = getPurchaseParams( storeProduct.first().subscriptionOptions!!.first(), oldPurchase.productIds.first(), - googleReplacementMode = GoogleReplacementMode.DEFERRED, + storeReplacementMode = StoreReplacementMode.DEFERRED, ) var callCount = 0 purchases.purchaseWith( @@ -882,7 +881,7 @@ internal class PurchasesCommonTest: BasePurchasesTest() { val productChangeParams = getPurchaseParams( storeProduct.first().subscriptionOptions!!.first(), oldProductIdWithBasePlan, - googleReplacementMode = GoogleReplacementMode.DEFERRED, + storeReplacementMode = StoreReplacementMode.DEFERRED, ) var callCount = 0 purchases.purchaseWith( @@ -902,7 +901,7 @@ internal class PurchasesCommonTest: BasePurchasesTest() { } @Test - fun `upgrade defaults to ProrationMode IMMEDIATE_WITHOUT_PRORATION`() { + fun `upgrade defaults to replacement mode WITHOUT_PRORATION`() { val productId = "gold" val oldSubId = "oldSubID" val storeProduct = mockQueryingProductDetails(productId, ProductType.SUBS) @@ -931,7 +930,7 @@ internal class PurchasesCommonTest: BasePurchasesTest() { val expectedReplaceProductInfo = ReplaceProductInfo( oldTransaction, - GoogleReplacementMode.WITHOUT_PRORATION + StoreReplacementMode.WITHOUT_PRORATION ) verify { mockBillingAbstract.makePurchaseAsync( @@ -947,7 +946,7 @@ internal class PurchasesCommonTest: BasePurchasesTest() { @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) @Test - fun `upgrade uses galaxyReplacementMode when store is Galaxy`() { + fun `upgrade uses replacementMode when store is Galaxy`() { buildPurchases(anonymous = false, store = Store.GALAXY) val productId = "galaxy_gold" @@ -966,13 +965,13 @@ internal class PurchasesCommonTest: BasePurchasesTest() { lambda<(StoreTransaction) -> Unit>().captured.invoke(oldTransaction) } - val replacementMode = GalaxyReplacementMode.INSTANT_PRORATED_DATE + val replacementMode = StoreReplacementMode.WITH_TIME_PRORATION val upgradePurchaseParams = PurchaseParams.Builder( mockActivity, storeProduct ) .oldProductId(oldSubId) - .galaxyReplacementMode(replacementMode) + .replacementMode(replacementMode) .build() purchases.purchaseWith( @@ -997,6 +996,73 @@ internal class PurchasesCommonTest: BasePurchasesTest() { } } + @Test + fun `when making a deferred product change on Galaxy, completion is called with the new transaction`() { + buildPurchases(anonymous = false, store = Store.GALAXY) + + val newProductId = "newproduct" + val storeProduct = stubStoreProduct(newProductId) + val oldProductId = "oldProductId" + val oldPurchase = getMockedStoreTransaction( + productId = oldProductId, + purchaseToken = "old_purchase_token", + productType = ProductType.SUBS, + ) + val newPurchase = getMockedStoreTransaction( + productId = newProductId, + purchaseToken = "new_purchase_token", + productType = ProductType.SUBS, + ) + + every { + mockBillingAbstract.findPurchaseInPurchaseHistory( + appUserID = appUserId, + productType = ProductType.SUBS, + productId = oldProductId, + onCompletion = captureLambda(), + onError = any(), + ) + } answers { + lambda<(StoreTransaction) -> Unit>().captured.invoke(oldPurchase) + } + + mockQueryingProductDetails(newProductId, ProductType.SUBS) + every { + mockPostReceiptHelper.postTransactionAndConsumeIfNeeded( + newPurchase, + any(), + any(), + isRestore = false, + appUserId, + initiationSource, + sdkOriginated = true, + captureLambda(), + any(), + ) + } answers { + lambda().captured.invoke(newPurchase, mockk(relaxed = true)) + } + + val productChangeParams = getPurchaseParams( + storeProduct, + oldProductId, + storeReplacementMode = StoreReplacementMode.DEFERRED, + ) + + var receivedPurchase: StoreTransaction? = null + purchases.purchaseWith( + productChangeParams, + onError = { _, _ -> fail("should be successful") }, + onSuccess = { purchase, _ -> + receivedPurchase = purchase + }, + ) + + capturedPurchasesUpdatedListener.captured.onPurchasesUpdated(listOf(newPurchase)) + + assertThat(receivedPurchase).isEqualTo(newPurchase) + } + @Test fun canMakePurchase() { val storeProduct = stubStoreProduct("abc") @@ -1211,7 +1277,7 @@ internal class PurchasesCommonTest: BasePurchasesTest() { eq(mockActivity), eq(appUserId), storeProduct.defaultOption!!.purchasingData, - ReplaceProductInfo(oldPurchase, GoogleReplacementMode.WITHOUT_PRORATION), + ReplaceProductInfo(oldPurchase, StoreReplacementMode.WITHOUT_PRORATION), PresentedOfferingContext(STUB_OFFERING_IDENTIFIER), any() ) @@ -1859,7 +1925,7 @@ internal class PurchasesCommonTest: BasePurchasesTest() { eq(mockActivity), eq(appUserId), storeProduct.subscriptionOptions!!.first().purchasingData, - ReplaceProductInfo(oldPurchase, GoogleReplacementMode.WITHOUT_PRORATION), + ReplaceProductInfo(oldPurchase, StoreReplacementMode.WITHOUT_PRORATION), PresentedOfferingContext(STUB_OFFERING_IDENTIFIER), any() ) @@ -1918,7 +1984,7 @@ internal class PurchasesCommonTest: BasePurchasesTest() { eq(mockActivity), eq(appUserId), storeProduct.subscriptionOptions!!.first().purchasingData, - ReplaceProductInfo(oldPurchase, GoogleReplacementMode.WITHOUT_PRORATION), + ReplaceProductInfo(oldPurchase, StoreReplacementMode.WITHOUT_PRORATION), PresentedOfferingContext(STUB_OFFERING_IDENTIFIER), any() ) @@ -2008,7 +2074,7 @@ internal class PurchasesCommonTest: BasePurchasesTest() { val expectedReplaceProductInfo = ReplaceProductInfo( oldTransaction, - GoogleReplacementMode.WITHOUT_PRORATION + StoreReplacementMode.WITHOUT_PRORATION ) verify { @@ -2072,7 +2138,7 @@ internal class PurchasesCommonTest: BasePurchasesTest() { val expectedReplaceProductInfo = ReplaceProductInfo( oldTransaction, - GoogleReplacementMode.WITHOUT_PRORATION + StoreReplacementMode.WITHOUT_PRORATION ) verify { diff --git a/purchases/src/test/java/com/revenuecat/purchases/ReplacementModeTest.kt b/purchases/src/test/java/com/revenuecat/purchases/ReplacementModeTest.kt index f20183700a..8504e6bd17 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/ReplacementModeTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/ReplacementModeTest.kt @@ -1,8 +1,8 @@ package com.revenuecat.purchases import android.os.Parcel -import com.revenuecat.purchases.models.GalaxyReplacementMode import com.revenuecat.purchases.models.GoogleReplacementMode +import com.revenuecat.purchases.models.StoreReplacementMode import org.assertj.core.api.Assertions.assertThat import org.junit.Test @@ -25,28 +25,79 @@ class ReplacementModeTest { assertThat(expectations.size).isEqualTo(GoogleReplacementMode.values().size) } - @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) @Test - fun `galaxy replacement modes use enum name for backend`() { + fun `backend name falls back to replacement mode name`() { + val mode = TestReplacementMode("CUSTOM_MODE") + + assertThat(mode.backendName).isEqualTo("CUSTOM_MODE") + } + + @Test + fun `store replacement modes map to legacy Play Store replacement mode backend names`() { val expectations = mapOf( - GalaxyReplacementMode.INSTANT_PRORATED_DATE to "INSTANT_PRORATED_DATE", - GalaxyReplacementMode.INSTANT_PRORATED_CHARGE to "INSTANT_PRORATED_CHARGE", - GalaxyReplacementMode.INSTANT_NO_PRORATION to "INSTANT_NO_PRORATION", - GalaxyReplacementMode.DEFERRED to "DEFERRED", + StoreReplacementMode.WITHOUT_PRORATION to "IMMEDIATE_WITHOUT_PRORATION", + StoreReplacementMode.WITH_TIME_PRORATION to "IMMEDIATE_WITH_TIME_PRORATION", + StoreReplacementMode.CHARGE_FULL_PRICE to "IMMEDIATE_AND_CHARGE_FULL_PRICE", + StoreReplacementMode.CHARGE_PRORATED_PRICE to "IMMEDIATE_AND_CHARGE_PRORATED_PRICE", + StoreReplacementMode.DEFERRED to "DEFERRED", ) - GalaxyReplacementMode.values().forEach { mode -> + StoreReplacementMode.values().forEach { mode -> assertThat(expectations).containsKey(mode) - assertThat(mode.backendName).isEqualTo(expectations.getValue(mode)) + assertThat(mode.backendName(Store.PLAY_STORE)).isEqualTo(expectations.getValue(mode)) } - assertThat(expectations.size).isEqualTo(GalaxyReplacementMode.values().size) + assertThat(expectations.size).isEqualTo(StoreReplacementMode.values().size) } @Test - fun `backend name falls back to replacement mode name`() { - val mode = TestReplacementMode("CUSTOM_MODE") + fun `store replacement modes map to Galaxy backend names`() { + val expectations = mapOf( + StoreReplacementMode.WITHOUT_PRORATION to "INSTANT_NO_PRORATION", + StoreReplacementMode.WITH_TIME_PRORATION to "INSTANT_PRORATED_DATE", + StoreReplacementMode.CHARGE_PRORATED_PRICE to "INSTANT_PRORATED_CHARGE", + StoreReplacementMode.DEFERRED to "DEFERRED", + ) - assertThat(mode.backendName).isEqualTo("CUSTOM_MODE") + StoreReplacementMode.values().forEach { mode -> + if (mode == StoreReplacementMode.CHARGE_FULL_PRICE) { + assertThat(mode.backendName(Store.GALAXY)).isNull() + } else { + assertThat(expectations).containsKey(mode) + assertThat(mode.backendName(Store.GALAXY)).isEqualTo(expectations.getValue(mode)) + } + } + assertThat(expectations.size).isEqualTo(StoreReplacementMode.values().size - 1) + } + + @Test + fun `google replacement modes use store specific backend names`() { + val playExpectations = mapOf( + GoogleReplacementMode.WITHOUT_PRORATION to "IMMEDIATE_WITHOUT_PRORATION", + GoogleReplacementMode.WITH_TIME_PRORATION to "IMMEDIATE_WITH_TIME_PRORATION", + GoogleReplacementMode.CHARGE_FULL_PRICE to "IMMEDIATE_AND_CHARGE_FULL_PRICE", + GoogleReplacementMode.CHARGE_PRORATED_PRICE to "IMMEDIATE_AND_CHARGE_PRORATED_PRICE", + GoogleReplacementMode.DEFERRED to "DEFERRED", + ) + val galaxyExpectations = mapOf( + GoogleReplacementMode.WITHOUT_PRORATION to "INSTANT_NO_PRORATION", + GoogleReplacementMode.WITH_TIME_PRORATION to "INSTANT_PRORATED_DATE", + GoogleReplacementMode.CHARGE_PRORATED_PRICE to "INSTANT_PRORATED_CHARGE", + GoogleReplacementMode.DEFERRED to "DEFERRED", + ) + + GoogleReplacementMode.values().forEach { mode -> + assertThat(playExpectations).containsKey(mode) + assertThat(mode.backendName(Store.PLAY_STORE)).isEqualTo(playExpectations.getValue(mode)) + + if (mode == GoogleReplacementMode.CHARGE_FULL_PRICE) { + assertThat(mode.backendName(Store.GALAXY)).isNull() + } else { + assertThat(galaxyExpectations).containsKey(mode) + assertThat(mode.backendName(Store.GALAXY)).isEqualTo(galaxyExpectations.getValue(mode)) + } + } + assertThat(playExpectations.size).isEqualTo(GoogleReplacementMode.values().size) + assertThat(galaxyExpectations.size).isEqualTo(GoogleReplacementMode.values().size - 1) } private class TestReplacementMode( diff --git a/purchases/src/test/java/com/revenuecat/purchases/common/ReceiptInfoTest.kt b/purchases/src/test/java/com/revenuecat/purchases/common/ReceiptInfoTest.kt index 9a9a518b1b..5cf7e07a6a 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/common/ReceiptInfoTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/common/ReceiptInfoTest.kt @@ -12,7 +12,6 @@ import com.revenuecat.purchases.PresentedOfferingContextSerializer import com.revenuecat.purchases.ProductType import com.revenuecat.purchases.ReplacementMode import com.revenuecat.purchases.TargetingContextSerializer -import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.Period import com.revenuecat.purchases.models.PeriodSerializer import com.revenuecat.purchases.models.Price @@ -22,6 +21,8 @@ import com.revenuecat.purchases.models.PricingPhaseSerializer import com.revenuecat.purchases.models.PurchaseState import com.revenuecat.purchases.models.PurchaseType import com.revenuecat.purchases.models.RecurrenceMode +import com.revenuecat.purchases.models.GoogleReplacementMode +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.utils.stubGooglePurchase import com.revenuecat.purchases.utils.stubStoreProduct @@ -334,7 +335,7 @@ class ReceiptInfoTest { @Test fun `ReceiptInfo with replacement mode can be serialized and deserialized`() { - val expectedReplacementMode = GoogleReplacementMode.WITH_TIME_PRORATION + val expectedReplacementMode = StoreReplacementMode.WITH_TIME_PRORATION val original = ReceiptInfo( productIDs = listOf(productIdentifier), price = 0.99, @@ -354,7 +355,7 @@ class ReceiptInfoTest { "price":0.99, "currency":"USD", "replacementMode":{ - "type":"GoogleReplacementMode", + "type":"StoreReplacementMode", "name":"WITH_TIME_PRORATION" } } @@ -366,6 +367,76 @@ class ReceiptInfoTest { assertThat(encoded).isEqualTo(expectedJson) } + @Test + fun `ReceiptInfo with StoreReplacementMode JSON deserializes to StoreReplacementMode`() { + // language=JSON + val receiptInfoJson = """ + { + "productIDs":["com.myproduct"], + "price":0.99, + "currency":"USD", + "replacementMode":{ + "type":"StoreReplacementMode", + "name":"CHARGE_PRORATED_PRICE" + } + } + """.trimIndent().lines().joinToString("") { it.trim() } + + val decoded = json.decodeFromString(receiptInfoJson) + + assertThat(decoded.replacementMode).isEqualTo(StoreReplacementMode.CHARGE_PRORATED_PRICE) + } + + @Test + fun `ReceiptInfo with GoogleReplacementMode serializes as StoreReplacementMode and deserializes to StoreReplacementMode`() { + val original = ReceiptInfo( + productIDs = listOf(productIdentifier), + price = 0.99, + currency = "USD", + period = null, + pricingPhases = null, + replacementMode = GoogleReplacementMode.WITH_TIME_PRORATION, + ) + + val encoded = json.encodeToString(original) + val decoded = json.decodeFromString(encoded) + + val expectedJson = """ + { + "productIDs":["com.myproduct"], + "price":0.99, + "currency":"USD", + "replacementMode":{ + "type":"StoreReplacementMode", + "name":"WITH_TIME_PRORATION" + } + } + """.trimIndent().lines().joinToString("") { it.trim() } + + assertThat(encoded).isEqualTo(expectedJson) + assertThat(decoded.replacementMode).isEqualTo(StoreReplacementMode.WITH_TIME_PRORATION) + } + + @Test + fun `ReceiptInfo with legacy Google replacement mode JSON deserializes to StoreReplacementMode`() { + // language=JSON + val receiptInfoJson = """ + { + "productIDs":["com.myproduct"], + "price":0.99, + "currency":"USD", + "replacementMode":{ + "type":"GoogleReplacementMode", + "name":"WITH_TIME_PRORATION" + } + } + """.trimIndent().lines().joinToString("") { it.trim() } + + val decoded = json.decodeFromString(receiptInfoJson) + + assertThat(decoded.replacementMode).isEqualTo(StoreReplacementMode.WITH_TIME_PRORATION) + } + @Test fun `ReceiptInfo with unknown replacement mode type fails serializing and deserializing`() { val unknownReplacementMode: ReplacementMode = object : ReplacementMode { @@ -406,6 +477,26 @@ class ReceiptInfoTest { assertThatExceptionOfType(SerializationException::class.java).isThrownBy { json.decodeFromString(unknownReplacementModeJson) } } + @Test + fun `ReceiptInfo with invalid StoreReplacementMode name fails deserializing`() { + // language=JSON + val invalidReplacementModeJson = """ + { + "productIDs":["com.myproduct"], + "price":0.99, + "currency":"USD", + "replacementMode":{ + "type":"StoreReplacementMode", + "name":"NOT_A_REAL_MODE" + } + } + """.trimIndent().lines().joinToString("") { it.trim() } + + assertThatExceptionOfType(SerializationException::class.java).isThrownBy { + json.decodeFromString(invalidReplacementModeJson) + } + } + @Test fun `ReceiptInfo with PresentedOfferingContext can be serialized and deserialized`() { val targetingContext = PresentedOfferingContext.TargetingContext( diff --git a/purchases/src/test/java/com/revenuecat/purchases/common/backend/BackendTest.kt b/purchases/src/test/java/com/revenuecat/purchases/common/backend/BackendTest.kt index e5ec953402..7bb4a58a36 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/common/backend/BackendTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/common/backend/BackendTest.kt @@ -7,6 +7,8 @@ import com.revenuecat.purchases.PresentedOfferingContext import com.revenuecat.purchases.PurchasesAreCompletedBy import com.revenuecat.purchases.PurchasesError import com.revenuecat.purchases.PurchasesErrorCode +import com.revenuecat.purchases.ReplacementMode +import com.revenuecat.purchases.Store import com.revenuecat.purchases.VerificationResult import com.revenuecat.purchases.common.AppConfig import com.revenuecat.purchases.common.Backend @@ -42,6 +44,7 @@ import com.revenuecat.purchases.models.Price import com.revenuecat.purchases.models.PricingPhase import com.revenuecat.purchases.models.RecurrenceMode import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.paywalls.events.PaywallPostReceiptData import com.revenuecat.purchases.utils.Responses import com.revenuecat.purchases.utils.filterNotNullValues @@ -115,6 +118,7 @@ class BackendTest { every { baseURL } returns mockBaseURL every { customEntitlementComputation } returns false every { fallbackBaseURLs } returns emptyList() + every { store } returns Store.PLAY_STORE } private val dispatcher = spyk(SyncDispatcher()) private val backendHelper = BackendHelper(API_KEY, dispatcher, mockAppConfig, mockClient) @@ -510,7 +514,7 @@ class BackendTest { val receiptInfo = createReceiptInfoFromProduct( productIDs = productIDs, storeProduct = storeProduct, - replacementMode = GoogleReplacementMode.WITHOUT_PRORATION + replacementMode = StoreReplacementMode.WITHOUT_PRORATION ) mockPostReceiptResponseAndPost( @@ -545,6 +549,50 @@ class BackendTest { assertThat(requestBodySlot.captured["proration_mode"]).isEqualTo("IMMEDIATE_WITHOUT_PRORATION") } + @Test + fun `postReceipt uses Galaxy replacement mode names in body when configured store is Galaxy`() { + every { mockAppConfig.store } returns Store.GALAXY + + val receiptInfo = createReceiptInfoFromProduct( + productIDs = productIDs, + storeProduct = storeProduct, + replacementMode = StoreReplacementMode.WITHOUT_PRORATION, + ) + + mockPostReceiptResponseAndPost( + backend, + isRestore = false, + finishTransactions = true, + receiptInfo = receiptInfo, + initiationSource = initiationSource, + ) + + assertThat(requestBodySlot.isCaptured).isTrue + assertThat(requestBodySlot.captured["proration_mode"]).isEqualTo("INSTANT_NO_PRORATION") + } + + @Test + fun `postReceipt uses Galaxy replacement mode names for deprecated Google replacement modes when configured store is Galaxy`() { + every { mockAppConfig.store } returns Store.GALAXY + + val receiptInfo = createReceiptInfoFromProduct( + productIDs = productIDs, + storeProduct = storeProduct, + replacementMode = GoogleReplacementMode.WITHOUT_PRORATION, + ) + + mockPostReceiptResponseAndPost( + backend, + isRestore = false, + finishTransactions = true, + receiptInfo = receiptInfo, + initiationSource = initiationSource, + ) + + assertThat(requestBodySlot.isCaptured).isTrue + assertThat(requestBodySlot.captured["proration_mode"]).isEqualTo("INSTANT_NO_PRORATION") + } + @Test fun `postReceipt has product_plan_id in body if receipt is GoogleStoreProduct subscription`() { val productId = "product_id" @@ -3295,7 +3343,7 @@ class BackendTest { storeProduct: StoreProduct, productIDs: List = listOf(storeProduct.id), presentedOfferingContext: PresentedOfferingContext? = null, - replacementMode: GoogleReplacementMode? = null, + replacementMode: ReplacementMode? = null, platformProductIds: List> = listOf(mapOf("product_id" to storeProduct.id)), storeUserID: String? = null, marketplace: String? = null, diff --git a/purchases/src/test/java/com/revenuecat/purchases/common/caching/LocalTransactionMetadataStoreTest.kt b/purchases/src/test/java/com/revenuecat/purchases/common/caching/LocalTransactionMetadataStoreTest.kt index 4b52f182ff..24c41252f8 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/common/caching/LocalTransactionMetadataStoreTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/common/caching/LocalTransactionMetadataStoreTest.kt @@ -8,6 +8,7 @@ import com.revenuecat.purchases.PresentedOfferingContext import com.revenuecat.purchases.PurchasesAreCompletedBy import com.revenuecat.purchases.common.ReceiptInfo import com.revenuecat.purchases.common.sha1 +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.paywalls.events.PaywallPostReceiptData import io.mockk.Runs import io.mockk.every @@ -575,5 +576,63 @@ class LocalTransactionMetadataStoreTest { assertThat(deserialized).isEqualTo(transactionMetadata) } + @Test + fun `LocalTransactionMetadata with StoreReplacementMode serializes correctly`() { + val receiptInfoWithReplacementMode = receiptInfo.copy( + replacementMode = StoreReplacementMode.DEFERRED, + ) + val transactionMetadata = LocalTransactionMetadata( + token = purchaseToken, + receiptInfo = receiptInfoWithReplacementMode, + paywallPostReceiptData = paywallData, + purchasesAreCompletedBy = PurchasesAreCompletedBy.REVENUECAT, + ) + + val jsonString = json.encodeToString(LocalTransactionMetadata.serializer(), transactionMetadata) + val deserialized = json.decodeFromString(LocalTransactionMetadata.serializer(), jsonString) + + assertThat(jsonString).contains("\"type\":\"StoreReplacementMode\"") + assertThat(deserialized.receiptInfo.replacementMode).isEqualTo(StoreReplacementMode.DEFERRED) + } + + @Test + fun `LocalTransactionMetadata with legacy GoogleReplacementMode JSON deserializes to StoreReplacementMode`() { + // language=json + val jsonString = """ + { + "token":"$purchaseToken", + "receipt_info":{ + "productIDs":["product_id"], + "presentedOfferingContext":{ + "offeringIdentifier":"offering_id", + "placementIdentifier":null, + "targetingContext":null + }, + "price":4.99, + "formattedPrice":"$4.99", + "currency":"USD", + "replacementMode":{ + "type":"GoogleReplacementMode", + "name":"DEFERRED" + } + }, + "paywall_data":{ + "paywall_id":"paywall_id", + "session_id":"session_id", + "revision":1, + "display_mode":"full_screen", + "dark_mode":false, + "locale":"en_US", + "offering_id":"offering_id" + }, + "purchases_are_completed_by":"REVENUECAT" + } + """.trimIndent().lines().joinToString("") { it.trim() } + + val deserialized = json.decodeFromString(LocalTransactionMetadata.serializer(), jsonString) + + assertThat(deserialized.receiptInfo.replacementMode).isEqualTo(StoreReplacementMode.DEFERRED) + } + // endregion } diff --git a/purchases/src/test/java/com/revenuecat/purchases/google/BillingWrapperTest.kt b/purchases/src/test/java/com/revenuecat/purchases/google/BillingWrapperTest.kt index ad2ca3e86a..e1f0e4d192 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/google/BillingWrapperTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/google/BillingWrapperTest.kt @@ -51,9 +51,11 @@ import com.revenuecat.purchases.models.PricingPhase import com.revenuecat.purchases.models.PurchasingData import com.revenuecat.purchases.models.RecurrenceMode import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.models.SubscriptionOption import com.revenuecat.purchases.models.SubscriptionOptions +import com.revenuecat.purchases.models.toPlayBillingClientMode import com.revenuecat.purchases.strings.BillingStrings import com.revenuecat.purchases.utils.createMockProductDetailsNoOffers import com.revenuecat.purchases.utils.mockInstallmentPlandetails @@ -387,7 +389,7 @@ class BillingWrapperTest { assertThat(purchaseContext?.productType).isEqualTo(ProductType.SUBS) assertThat(purchaseContext?.presentedOfferingContext).isEqualTo(PresentedOfferingContext("offering_a")) assertThat(purchaseContext?.selectedSubscriptionOptionId).isEqualTo(storeProduct.subscriptionOptions!!.first().id) - assertThat(purchaseContext?.replacementMode).isEqualTo(GoogleReplacementMode.CHARGE_FULL_PRICE) + assertThat(purchaseContext?.replacementMode).isEqualTo(StoreReplacementMode.CHARGE_FULL_PRICE) } @Test @@ -399,7 +401,7 @@ class BillingWrapperTest { val storeProduct = createStoreProductWithoutOffers() val purchasingData = storeProduct.subscriptionOptions!!.first().purchasingData val oldPurchase = mockPurchaseRecordWrapper() - val replaceInfo = ReplaceProductInfo(oldPurchase, GoogleReplacementMode.DEFERRED) + val replaceInfo = ReplaceProductInfo(oldPurchase, StoreReplacementMode.DEFERRED) billingClientStateListener!!.onBillingSetupFinished(billingClientOKResult) wrapper.makePurchaseAsync( @@ -423,7 +425,7 @@ class BillingWrapperTest { assertThat(purchaseContext?.productType).isEqualTo(ProductType.SUBS) assertThat(purchaseContext?.presentedOfferingContext).isEqualTo(PresentedOfferingContext("offering_a")) assertThat(purchaseContext?.selectedSubscriptionOptionId).isEqualTo(storeProduct.subscriptionOptions!!.first().id) - assertThat(purchaseContext?.replacementMode).isEqualTo(GoogleReplacementMode.DEFERRED) + assertThat(purchaseContext?.replacementMode).isEqualTo(StoreReplacementMode.DEFERRED) } @Test @@ -486,7 +488,8 @@ class BillingWrapperTest { assertThat(subsGoogleProductType).isEqualTo(capturedProductDetailsParams[0].zza().productType) assertThat(upgradeInfo.oldPurchase.purchaseToken).isEqualTo(oldPurchaseTokenSlot.captured) - assertThat((upgradeInfo.replacementMode as GoogleReplacementMode?)?.playBillingClientMode).isEqualTo(replacementModeSlot.captured) + assertThat((upgradeInfo.replacementMode as StoreReplacementMode?)?.toPlayBillingClientMode()) + .isEqualTo(replacementModeSlot.captured) assertThat(isPersonalizedPrice).isEqualTo(isPersonalizedPriceSlot.captured) billingClientOKResult @@ -503,6 +506,74 @@ class BillingWrapperTest { ) } + @Test + fun `properly sets billingFlowParams for subscription purchase with deprecated GoogleReplacementMode`() { + mockkStatic(BillingFlowParams::class) + mockkStatic(BillingFlowParams.SubscriptionUpdateParams::class) + + val mockBuilder = mockk(relaxed = true) + every { + BillingFlowParams.newBuilder() + } returns mockBuilder + + val productDetailsParamsSlot = slot>() + every { + mockBuilder.setProductDetailsParamsList(capture(productDetailsParamsSlot)) + } returns mockBuilder + + every { + mockBuilder.setIsOfferPersonalized(any()) + } returns mockBuilder + + val mockSubscriptionUpdateParamsBuilder = + mockk(relaxed = true) + every { + BillingFlowParams.SubscriptionUpdateParams.newBuilder() + } returns mockSubscriptionUpdateParamsBuilder + + val oldPurchaseTokenSlot = slot() + every { + mockSubscriptionUpdateParamsBuilder.setOldPurchaseToken(capture(oldPurchaseTokenSlot)) + } returns mockSubscriptionUpdateParamsBuilder + + val replacementModeSlot = slot() + every { + mockSubscriptionUpdateParamsBuilder.setSubscriptionReplacementMode(capture(replacementModeSlot)) + } returns mockSubscriptionUpdateParamsBuilder + + val productId = "product_a" + val oldPurchase = mockPurchaseRecordWrapper() + val upgradeInfo = ReplaceProductInfo(oldPurchase, GoogleReplacementMode.DEFERRED) + val productDetails = mockProductDetails(productId = productId, type = subsGoogleProductType) + val storeProduct = productDetails.toStoreProduct( + productDetails.subscriptionOfferDetails!! + )!! + + every { + mockClient.launchBillingFlow(eq(mockActivity), any()) + } answers { + val capturedProductDetailsParams = productDetailsParamsSlot.captured + + assertThat(1).isEqualTo(capturedProductDetailsParams.size) + assertThat(productId).isEqualTo(capturedProductDetailsParams[0].zza().productId) + assertThat(subsGoogleProductType).isEqualTo(capturedProductDetailsParams[0].zza().productType) + + assertThat(oldPurchase.purchaseToken).isEqualTo(oldPurchaseTokenSlot.captured) + assertThat(GoogleReplacementMode.DEFERRED.playBillingClientMode).isEqualTo(replacementModeSlot.captured) + billingClientOKResult + } + + billingClientStateListener!!.onBillingSetupFinished(billingClientOKResult) + wrapper.makePurchaseAsync( + mockActivity, + appUserId, + storeProduct.subscriptionOptions!!.first().purchasingData, + upgradeInfo, + null, + null, + ) + } + @Test fun `skips setting on BillingFlowPrams when replacementmode or personalized price null for subscription purchase`() { mockkStatic(BillingFlowParams::class) @@ -1734,7 +1805,7 @@ class BillingWrapperTest { assertThat(purchaseContext?.productType).isEqualTo(ProductType.SUBS) assertThat(purchaseContext?.presentedOfferingContext).isEqualTo(PresentedOfferingContext("offering_a")) assertThat(purchaseContext?.selectedSubscriptionOptionId).isEqualTo(optionId) - assertThat(purchaseContext?.replacementMode).isEqualTo(GoogleReplacementMode.CHARGE_FULL_PRICE) + assertThat(purchaseContext?.replacementMode).isEqualTo(StoreReplacementMode.CHARGE_FULL_PRICE) val subscriptionOptionIdForProductIDs = purchaseContext?.subscriptionOptionIdForProductIDs assertThat(subscriptionOptionIdForProductIDs).isNotNull @@ -1813,7 +1884,8 @@ class BillingWrapperTest { assertThat(capturedProductDetailsParams[1].zza().productId).isEqualTo(productId2) assertThat(capturedProductDetailsParams[1].zza().productType).isEqualTo(subsGoogleProductType) assertThat(upgradeInfo.oldPurchase.purchaseToken).isEqualTo(oldPurchaseTokenSlot.captured) - assertThat((upgradeInfo.replacementMode as GoogleReplacementMode?)?.playBillingClientMode).isEqualTo(replacementModeSlot.captured) + assertThat((upgradeInfo.replacementMode as StoreReplacementMode?)?.toPlayBillingClientMode()) + .isEqualTo(replacementModeSlot.captured) assertThat(isPersonalizedPrice).isEqualTo(isPersonalizedPriceSlot.captured) billingClientOKResult @@ -2143,7 +2215,7 @@ class BillingWrapperTest { private fun mockReplaceSkuInfo(): ReplaceProductInfo { val oldPurchase = mockPurchaseRecordWrapper() - return ReplaceProductInfo(oldPurchase, GoogleReplacementMode.CHARGE_FULL_PRICE) + return ReplaceProductInfo(oldPurchase, StoreReplacementMode.CHARGE_FULL_PRICE) } private fun setUpForObfuscatedAccountIDTests(): BillingFlowParams.Builder { diff --git a/purchases/src/test/java/com/revenuecat/purchases/models/StoreReplacementModeConversionsTest.kt b/purchases/src/test/java/com/revenuecat/purchases/models/StoreReplacementModeConversionsTest.kt new file mode 100644 index 0000000000..3608a2eb39 --- /dev/null +++ b/purchases/src/test/java/com/revenuecat/purchases/models/StoreReplacementModeConversionsTest.kt @@ -0,0 +1,105 @@ +package com.revenuecat.purchases.models + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.billingclient.api.BillingFlowParams +import com.revenuecat.purchases.Store +import org.assertj.core.api.Assertions.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class StoreReplacementModeConversionsTest { + + @Test + fun `all store replacement modes map to Google BillingClient modes`() { + val expectations = mapOf( + StoreReplacementMode.WITHOUT_PRORATION to + BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITHOUT_PRORATION, + StoreReplacementMode.WITH_TIME_PRORATION to + BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.WITH_TIME_PRORATION, + StoreReplacementMode.CHARGE_FULL_PRICE to + BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE, + StoreReplacementMode.CHARGE_PRORATED_PRICE to + BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_PRORATED_PRICE, + StoreReplacementMode.DEFERRED to + BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.DEFERRED, + ) + + StoreReplacementMode.values().forEach { mode -> + val expected = expectations[mode] ?: error("Missing expected mapping for $mode") + assertThat(mode.toPlayBillingClientMode()).isEqualTo(expected) + } + + assertThat(expectations.size).isEqualTo(StoreReplacementMode.values().size) + } + + @Test + fun `all store replacement modes map to store specific backend names`() { + val playExpectations = mapOf( + StoreReplacementMode.WITHOUT_PRORATION to "IMMEDIATE_WITHOUT_PRORATION", + StoreReplacementMode.WITH_TIME_PRORATION to "IMMEDIATE_WITH_TIME_PRORATION", + StoreReplacementMode.CHARGE_FULL_PRICE to "IMMEDIATE_AND_CHARGE_FULL_PRICE", + StoreReplacementMode.CHARGE_PRORATED_PRICE to "IMMEDIATE_AND_CHARGE_PRORATED_PRICE", + StoreReplacementMode.DEFERRED to "DEFERRED", + ) + val galaxyExpectations = mapOf( + StoreReplacementMode.WITHOUT_PRORATION to "INSTANT_NO_PRORATION", + StoreReplacementMode.WITH_TIME_PRORATION to "INSTANT_PRORATED_DATE", + StoreReplacementMode.CHARGE_PRORATED_PRICE to "INSTANT_PRORATED_CHARGE", + StoreReplacementMode.DEFERRED to "DEFERRED", + ) + + StoreReplacementMode.values().forEach { mode -> + assertThat(mode.storeBackendName(Store.PLAY_STORE)).isEqualTo(playExpectations.getValue(mode)) + + if (mode == StoreReplacementMode.CHARGE_FULL_PRICE) { + assertThat(mode.storeBackendName(Store.GALAXY)).isNull() + } else { + assertThat(mode.storeBackendName(Store.GALAXY)).isEqualTo(galaxyExpectations.getValue(mode)) + } + } + } + + @Test + fun `all store replacement modes map to deprecated Google replacement modes`() { + val expectations = mapOf( + StoreReplacementMode.WITHOUT_PRORATION to GoogleReplacementMode.WITHOUT_PRORATION, + StoreReplacementMode.WITH_TIME_PRORATION to GoogleReplacementMode.WITH_TIME_PRORATION, + StoreReplacementMode.CHARGE_FULL_PRICE to GoogleReplacementMode.CHARGE_FULL_PRICE, + StoreReplacementMode.CHARGE_PRORATED_PRICE to GoogleReplacementMode.CHARGE_PRORATED_PRICE, + StoreReplacementMode.DEFERRED to GoogleReplacementMode.DEFERRED, + ) + + StoreReplacementMode.values().forEach { mode -> + val expected = expectations[mode] ?: error("Missing expected mapping for $mode") + assertThat(mode.toGoogleReplacementMode()).isEqualTo(expected) + } + + assertThat(expectations.size).isEqualTo(StoreReplacementMode.values().size) + } + + @Test + fun `all deprecated Google replacement modes map to store replacement modes`() { + val expectations = mapOf( + GoogleReplacementMode.WITHOUT_PRORATION to StoreReplacementMode.WITHOUT_PRORATION, + GoogleReplacementMode.WITH_TIME_PRORATION to StoreReplacementMode.WITH_TIME_PRORATION, + GoogleReplacementMode.CHARGE_FULL_PRICE to StoreReplacementMode.CHARGE_FULL_PRICE, + GoogleReplacementMode.CHARGE_PRORATED_PRICE to StoreReplacementMode.CHARGE_PRORATED_PRICE, + GoogleReplacementMode.DEFERRED to StoreReplacementMode.DEFERRED, + ) + + GoogleReplacementMode.values().forEach { mode -> + val expected = expectations[mode] ?: error("Missing expected mapping for $mode") + assertThat(mode.toStoreReplacementMode()).isEqualTo(expected) + } + + assertThat(expectations.size).isEqualTo(GoogleReplacementMode.values().size) + } + + @Test + fun `ReplacementMode normalization returns canonical store replacement modes`() { + assertThat(StoreReplacementMode.DEFERRED.toStoreReplacementModeOrNull()).isEqualTo(StoreReplacementMode.DEFERRED) + assertThat(GoogleReplacementMode.DEFERRED.toStoreReplacementModeOrNull()).isEqualTo(StoreReplacementMode.DEFERRED) + assertThat((null as com.revenuecat.purchases.ReplacementMode?).toStoreReplacementModeOrNull()).isNull() + } +} diff --git a/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/common/ProductChangeConfigTest.kt b/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/common/ProductChangeConfigTest.kt index ced970bef6..91d82be621 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/common/ProductChangeConfigTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/paywalls/components/common/ProductChangeConfigTest.kt @@ -2,6 +2,7 @@ package com.revenuecat.purchases.paywalls.components.common import com.revenuecat.purchases.JsonTools import com.revenuecat.purchases.models.GoogleReplacementMode +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.utils.serializers.EmptyObjectToNullSerializer import org.intellij.lang.annotations.Language import org.junit.Test @@ -30,8 +31,8 @@ class ProductChangeConfigTest( Args( json = """{"upgrade_replacement_mode": "charge_prorated_price"}""", expected = ProductChangeConfig( - upgradeReplacementMode = GoogleReplacementMode.CHARGE_PRORATED_PRICE, - downgradeReplacementMode = GoogleReplacementMode.DEFERRED, + upgradeReplacementMode = StoreReplacementMode.CHARGE_PRORATED_PRICE, + downgradeReplacementMode = StoreReplacementMode.DEFERRED, ), ), ), @@ -45,8 +46,8 @@ class ProductChangeConfigTest( } """.trimIndent(), expected = ProductChangeConfig( - upgradeReplacementMode = GoogleReplacementMode.CHARGE_PRORATED_PRICE, - downgradeReplacementMode = GoogleReplacementMode.DEFERRED, + upgradeReplacementMode = StoreReplacementMode.CHARGE_PRORATED_PRICE, + downgradeReplacementMode = StoreReplacementMode.DEFERRED, ), ), ), @@ -60,8 +61,8 @@ class ProductChangeConfigTest( } """.trimIndent(), expected = ProductChangeConfig( - upgradeReplacementMode = GoogleReplacementMode.CHARGE_FULL_PRICE, - downgradeReplacementMode = GoogleReplacementMode.WITH_TIME_PRORATION, + upgradeReplacementMode = StoreReplacementMode.CHARGE_FULL_PRICE, + downgradeReplacementMode = StoreReplacementMode.WITH_TIME_PRORATION, ), ), ), @@ -75,8 +76,8 @@ class ProductChangeConfigTest( } """.trimIndent(), expected = ProductChangeConfig( - upgradeReplacementMode = GoogleReplacementMode.WITHOUT_PRORATION, - downgradeReplacementMode = GoogleReplacementMode.WITHOUT_PRORATION, + upgradeReplacementMode = StoreReplacementMode.WITHOUT_PRORATION, + downgradeReplacementMode = StoreReplacementMode.WITHOUT_PRORATION, ), ), ), @@ -102,8 +103,8 @@ class ProductChangeConfigTest( val json = """{"play_store_product_change_mode": {"upgrade_replacement_mode": "charge_full_price"}}""" val wrapper = JsonTools.json.decodeFromString(json) assert(wrapper.productChangeConfig != null) - assert(wrapper.productChangeConfig!!.upgradeReplacementMode == GoogleReplacementMode.CHARGE_FULL_PRICE) - assert(wrapper.productChangeConfig!!.downgradeReplacementMode == GoogleReplacementMode.DEFERRED) + assert(wrapper.productChangeConfig!!.upgradeReplacementMode == StoreReplacementMode.CHARGE_FULL_PRICE) + assert(wrapper.productChangeConfig!!.downgradeReplacementMode == StoreReplacementMode.DEFERRED) } @Test diff --git a/purchases/src/test/java/com/revenuecat/purchases/utils/PurchaseParamsValidatorTest.kt b/purchases/src/test/java/com/revenuecat/purchases/utils/PurchaseParamsValidatorTest.kt index 9a90441cae..b66228a167 100644 --- a/purchases/src/test/java/com/revenuecat/purchases/utils/PurchaseParamsValidatorTest.kt +++ b/purchases/src/test/java/com/revenuecat/purchases/utils/PurchaseParamsValidatorTest.kt @@ -9,6 +9,7 @@ import com.revenuecat.purchases.models.GooglePurchasingData import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.Period import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.models.StoreReplacementMode import io.mockk.every import io.mockk.mockk import org.assertj.core.api.Assertions.assertThat @@ -99,6 +100,35 @@ class PurchaseParamsValidatorTest { assertThat(validationResult).isInstanceOf(Result.Success::class.java) } + @Test + fun `purchaseParams with oldProductId, GoogleReplacementMode, and StoreReplacementMode passes validation`() { + val purchaseParams = PurchaseParams.Builder( + mockk(), + subscriptionOption = stubSubscriptionOption(id = "123", productId = "abc") + ) + .oldProductId("123") + .googleReplacementMode(GoogleReplacementMode.WITHOUT_PRORATION) + .replacementMode(StoreReplacementMode.WITHOUT_PRORATION) + .build() + + val validationResult = validator.validate(purchaseParams) + assertThat(validationResult).isInstanceOf(Result.Success::class.java) + } + + @Test + fun `purchaseParams with oldProductId and StoreReplacementMode passes validation`() { + val purchaseParams = PurchaseParams.Builder( + mockk(), + subscriptionOption = stubSubscriptionOption(id = "123", productId = "abc") + ) + .oldProductId("123") + .replacementMode(StoreReplacementMode.WITHOUT_PRORATION) + .build() + + val validationResult = validator.validate(purchaseParams) + assertThat(validationResult).isInstanceOf(Result.Success::class.java) + } + @Test fun `purchaseParams with personalized price passes validation`() { for (personalizedPrice in listOf(true, false)) { diff --git a/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesTest.kt b/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesTest.kt index b851d92923..2704d3a16e 100644 --- a/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesTest.kt +++ b/purchases/src/testDefaults/kotlin/com/revenuecat/purchases/PurchasesTest.kt @@ -29,6 +29,7 @@ import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.GoogleSubscriptionOption import com.revenuecat.purchases.models.PurchasingData import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.paywalls.DownloadedFontFamily import com.revenuecat.purchases.paywalls.events.PaywallEvent @@ -274,7 +275,7 @@ internal class PurchasesTest : BasePurchasesTest() { val expectedReplaceProductInfo = ReplaceProductInfo( oldTransaction, - GoogleReplacementMode.WITHOUT_PRORATION, + StoreReplacementMode.WITHOUT_PRORATION, ) verify { mockBillingAbstract.makePurchaseAsync( @@ -385,7 +386,7 @@ internal class PurchasesTest : BasePurchasesTest() { match { replaceProductInfo -> replaceProductInfo.oldPurchase.productIds.size == 1 && replaceProductInfo.oldPurchase.productIds.first() == "oldProductId" && - replaceProductInfo.replacementMode == GoogleReplacementMode.CHARGE_PRORATED_PRICE + replaceProductInfo.replacementMode == StoreReplacementMode.CHARGE_PRORATED_PRICE }, PresentedOfferingContext(STUB_OFFERING_IDENTIFIER), null, @@ -420,7 +421,7 @@ internal class PurchasesTest : BasePurchasesTest() { match { replaceProductInfo -> replaceProductInfo.oldPurchase.productIds.size == 1 && replaceProductInfo.oldPurchase.productIds.first() == "oldProductId" && - replaceProductInfo.replacementMode == GoogleReplacementMode.CHARGE_PRORATED_PRICE + replaceProductInfo.replacementMode == StoreReplacementMode.CHARGE_PRORATED_PRICE }, PresentedOfferingContext(STUB_OFFERING_IDENTIFIER), null, @@ -1932,7 +1933,7 @@ internal class PurchasesTest : BasePurchasesTest() { val capturedReplaceProductInfo = replaceProductInfoSlot.captured assertThat(capturedReplaceProductInfo.oldPurchase.productIds).isEqualTo(expectedOldPurchase.productIds) assertThat(capturedReplaceProductInfo.oldPurchase.purchaseToken).isEqualTo(expectedOldPurchase.purchaseToken) - assertThat(capturedReplaceProductInfo.replacementMode).isEqualTo(GoogleReplacementMode.WITHOUT_PRORATION) + assertThat(capturedReplaceProductInfo.replacementMode).isEqualTo(StoreReplacementMode.WITHOUT_PRORATION) } @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) @@ -1999,7 +2000,7 @@ internal class PurchasesTest : BasePurchasesTest() { val capturedReplaceProductInfo = replaceProductInfoSlot.captured assertThat(capturedReplaceProductInfo.oldPurchase.productIds).isEqualTo(expectedOldPurchase.productIds) assertThat(capturedReplaceProductInfo.oldPurchase.purchaseToken).isEqualTo(expectedOldPurchase.purchaseToken) - assertThat(capturedReplaceProductInfo.replacementMode).isEqualTo(GoogleReplacementMode.WITHOUT_PRORATION) + assertThat(capturedReplaceProductInfo.replacementMode).isEqualTo(StoreReplacementMode.WITHOUT_PRORATION) } @OptIn(ExperimentalPreviewRevenueCatPurchasesAPI::class) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/PurchaseLogic.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/PurchaseLogic.kt index c1b4f1a4c4..73970c3674 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/PurchaseLogic.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/PurchaseLogic.kt @@ -85,7 +85,7 @@ public interface PurchaseLogic : PaywallPurchaseLogic { * ```kotlin * val params = PaywallPurchaseLogicParams.Builder(rcPackage) * .oldProductId("com.example.old_product") - * .replacementMode(GoogleReplacementMode.CHARGE_PRORATED_PRICE) + * .replacementMode(StoreReplacementMode.CHARGE_PRORATED_PRICE) * .subscriptionOption(subscriptionOption) * .build() * ``` @@ -112,7 +112,7 @@ public class PaywallPurchaseLogicParams internal constructor( /** * The replacement mode to use for this product change, as configured in the paywall. - * For Google Play, this will be a [com.revenuecat.purchases.models.GoogleReplacementMode]. + * For Google Play, this will be a [com.revenuecat.purchases.models.StoreReplacementMode]. * Null if this is a new purchase or the store does not support replacement modes. */ public val replacementMode: ReplacementMode? get() = productChange?.replacementMode @@ -137,7 +137,7 @@ public class PaywallPurchaseLogicParams internal constructor( /** * Sets the replacement mode for this product change. - * For Google Play, use [com.revenuecat.purchases.models.GoogleReplacementMode]. + * Use [com.revenuecat.purchases.models.StoreReplacementMode]. */ public fun replacementMode(replacementMode: ReplacementMode?): Builder = apply { this.replacementMode = replacementMode diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt index 539214301d..05ac05aa3d 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModel.kt @@ -551,7 +551,7 @@ internal class PaywallViewModelImpl( ) purchaseParamsBuilder .oldProductId(productChangeInfo.oldProductId) - .googleReplacementMode(productChangeInfo.replacementMode) + .replacementMode(productChangeInfo.replacementMode) } val purchaseResult = purchases.awaitPurchase(purchaseParamsBuilder) diff --git a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/ProductChangeCalculator.kt b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/ProductChangeCalculator.kt index 8ef3c85f46..b9b36aea94 100644 --- a/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/ProductChangeCalculator.kt +++ b/ui/revenuecatui/src/main/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/ProductChangeCalculator.kt @@ -5,16 +5,16 @@ import com.revenuecat.purchases.ProductType import com.revenuecat.purchases.PurchasesException import com.revenuecat.purchases.Store import com.revenuecat.purchases.SubscriptionInfo -import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.GoogleStoreProduct import com.revenuecat.purchases.models.Period import com.revenuecat.purchases.models.StoreProduct +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.paywalls.components.common.ProductChangeConfig import com.revenuecat.purchases.ui.revenuecatui.helpers.Logger internal data class ProductChangeInfo( val oldProductId: String, - val replacementMode: GoogleReplacementMode, + val replacementMode: StoreReplacementMode, ) /** diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt index fba56030d2..34448a27a6 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/PaywallViewModelTest.kt @@ -16,7 +16,7 @@ import com.revenuecat.purchases.PurchasesAreCompletedBy import com.revenuecat.purchases.PurchasesError import com.revenuecat.purchases.PurchasesErrorCode import com.revenuecat.purchases.PurchasesException -import com.revenuecat.purchases.models.GoogleReplacementMode +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.StoreTransaction import com.revenuecat.purchases.paywalls.PaywallData import com.revenuecat.purchases.paywalls.components.common.ProductChangeConfig @@ -1914,7 +1914,7 @@ class PaywallViewModelTest { productChangeCalculator.calculateProductChangeInfo(any(), any()) } returns ProductChangeInfo( oldProductId = "old_product", - replacementMode = GoogleReplacementMode.CHARGE_PRORATED_PRICE, + replacementMode = StoreReplacementMode.CHARGE_PRORATED_PRICE, ) val transaction = mockk() @@ -1954,7 +1954,7 @@ class PaywallViewModelTest { withArg { builder -> val params = builder.build() assertThat(params.oldProductId).isEqualTo("old_product") - assertThat(params.googleReplacementMode).isEqualTo(GoogleReplacementMode.CHARGE_PRORATED_PRICE) + assertThat(params.replacementMode).isEqualTo(StoreReplacementMode.CHARGE_PRORATED_PRICE) }, ) } @@ -2170,7 +2170,7 @@ class PaywallViewModelTest { productChangeCalculator.calculateProductChangeInfo(any(), any()) } returns ProductChangeInfo( oldProductId = "old_product", - replacementMode = GoogleReplacementMode.CHARGE_PRORATED_PRICE, + replacementMode = StoreReplacementMode.CHARGE_PRORATED_PRICE, ) val model = PaywallViewModelImpl( @@ -2198,7 +2198,7 @@ class PaywallViewModelTest { assertThat(receivedContext).isNotNull assertThat(receivedContext!!.oldProductId).isEqualTo("old_product") assertThat(receivedContext.replacementMode) - .isEqualTo(GoogleReplacementMode.CHARGE_PRORATED_PRICE) + .isEqualTo(StoreReplacementMode.CHARGE_PRORATED_PRICE) coVerify(exactly = 1) { purchases.awaitSyncPurchases() } assertThat(dismissInvoked).isTrue } @@ -2248,7 +2248,7 @@ class PaywallViewModelTest { productChangeCalculator.calculateProductChangeInfo(any(), any()) } returns ProductChangeInfo( oldProductId = "old_product", - replacementMode = GoogleReplacementMode.DEFERRED, + replacementMode = StoreReplacementMode.DEFERRED, ) val model = PaywallViewModelImpl( @@ -2276,7 +2276,7 @@ class PaywallViewModelTest { assertThat(receivedContext).isNotNull assertThat(receivedContext!!.oldProductId).isEqualTo("old_product") assertThat(receivedContext.replacementMode) - .isEqualTo(GoogleReplacementMode.DEFERRED) + .isEqualTo(StoreReplacementMode.DEFERRED) coVerify(exactly = 1) { purchases.awaitSyncPurchases() } assertThat(dismissInvoked).isTrue } diff --git a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/ProductChangeCalculatorTest.kt b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/ProductChangeCalculatorTest.kt index b05d10d61c..0335628381 100644 --- a/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/ProductChangeCalculatorTest.kt +++ b/ui/revenuecatui/src/test/kotlin/com/revenuecat/purchases/ui/revenuecatui/data/ProductChangeCalculatorTest.kt @@ -14,9 +14,9 @@ import com.revenuecat.purchases.PurchasesErrorCode import com.revenuecat.purchases.PurchasesException import com.revenuecat.purchases.Store import com.revenuecat.purchases.SubscriptionInfo -import com.revenuecat.purchases.models.GoogleReplacementMode import com.revenuecat.purchases.models.Period import com.revenuecat.purchases.models.Price +import com.revenuecat.purchases.models.StoreReplacementMode import com.revenuecat.purchases.models.TestStoreProduct import com.revenuecat.purchases.paywalls.components.common.ProductChangeConfig import io.mockk.coEvery @@ -37,8 +37,8 @@ class ProductChangeCalculatorTest { private lateinit var calculator: ProductChangeCalculator private val defaultProductChangeConfig = ProductChangeConfig( - upgradeReplacementMode = GoogleReplacementMode.CHARGE_PRORATED_PRICE, - downgradeReplacementMode = GoogleReplacementMode.DEFERRED, + upgradeReplacementMode = StoreReplacementMode.CHARGE_PRORATED_PRICE, + downgradeReplacementMode = StoreReplacementMode.DEFERRED, ) @Before @@ -170,7 +170,7 @@ class ProductChangeCalculatorTest { assertThat(result).isNotNull assertThat(result!!.oldProductId).isEqualTo("com.test.subscription.basic") - assertThat(result.replacementMode).isEqualTo(GoogleReplacementMode.CHARGE_PRORATED_PRICE) + assertThat(result.replacementMode).isEqualTo(StoreReplacementMode.CHARGE_PRORATED_PRICE) } @Test @@ -206,7 +206,7 @@ class ProductChangeCalculatorTest { assertThat(result).isNotNull assertThat(result!!.oldProductId).isEqualTo("com.test.subscription.premium") - assertThat(result.replacementMode).isEqualTo(GoogleReplacementMode.DEFERRED) + assertThat(result.replacementMode).isEqualTo(StoreReplacementMode.DEFERRED) } @Test @@ -242,7 +242,7 @@ class ProductChangeCalculatorTest { assertThat(result).isNotNull assertThat(result!!.oldProductId).isEqualTo("com.test.subscription.basic") - assertThat(result.replacementMode).isEqualTo(GoogleReplacementMode.CHARGE_PRORATED_PRICE) + assertThat(result.replacementMode).isEqualTo(StoreReplacementMode.CHARGE_PRORATED_PRICE) } @Test @@ -272,8 +272,8 @@ class ProductChangeCalculatorTest { ) val customConfig = ProductChangeConfig( - upgradeReplacementMode = GoogleReplacementMode.CHARGE_FULL_PRICE, - downgradeReplacementMode = GoogleReplacementMode.WITH_TIME_PRORATION, + upgradeReplacementMode = StoreReplacementMode.CHARGE_FULL_PRICE, + downgradeReplacementMode = StoreReplacementMode.WITH_TIME_PRORATION, ) val result = calculator.calculateProductChangeInfo( @@ -282,7 +282,7 @@ class ProductChangeCalculatorTest { ) assertThat(result).isNotNull - assertThat(result!!.replacementMode).isEqualTo(GoogleReplacementMode.CHARGE_FULL_PRICE) + assertThat(result!!.replacementMode).isEqualTo(StoreReplacementMode.CHARGE_FULL_PRICE) } @Test @@ -312,7 +312,7 @@ class ProductChangeCalculatorTest { ) assertThat(result).isNotNull - assertThat(result!!.replacementMode).isEqualTo(GoogleReplacementMode.DEFERRED) + assertThat(result!!.replacementMode).isEqualTo(StoreReplacementMode.DEFERRED) } @Test