diff --git a/android/hybridcommon/src/main/java/com/revenuecat/purchases/hybridcommon/mappers/StoreProductMapper.kt b/android/hybridcommon/src/main/java/com/revenuecat/purchases/hybridcommon/mappers/StoreProductMapper.kt index a7a24995e..c36e5f71a 100644 --- a/android/hybridcommon/src/main/java/com/revenuecat/purchases/hybridcommon/mappers/StoreProductMapper.kt +++ b/android/hybridcommon/src/main/java/com/revenuecat/purchases/hybridcommon/mappers/StoreProductMapper.kt @@ -2,6 +2,7 @@ package com.revenuecat.purchases.hybridcommon.mappers import androidx.annotation.VisibleForTesting import com.revenuecat.purchases.ProductType +import com.revenuecat.purchases.amazon.AmazonStoreProduct import com.revenuecat.purchases.models.InstallmentsInfo import com.revenuecat.purchases.models.Period import com.revenuecat.purchases.models.Price @@ -15,21 +16,22 @@ val StoreProduct.priceString: String get() = this.price.formatted val StoreProduct.priceCurrencyCode: String get() = this.price.currencyCode -val StoreProduct.freeTrialPeriod: Period? + +val StoreProduct.googleFreeTrialPeriod: Period? get() = this.defaultOption?.freePhase?.billingPeriod -val StoreProduct.freeTrialCycles: Int? +val StoreProduct.googleFreeTrialCycles: Int? get() = this.defaultOption?.freePhase?.billingCycleCount -private val StoreProduct.introductoryPhase: PricingPhase? +private val StoreProduct.googleIntroductoryPhase: PricingPhase? get() = this.defaultOption?.introPhase -val StoreProduct.introductoryPrice: String? - get() = this.introductoryPhase?.price?.formatted -val StoreProduct.introductoryPricePeriod: Period? - get() = this.introductoryPhase?.billingPeriod -val StoreProduct.introductoryPriceAmountMicros: Long - get() = this.introductoryPhase?.price?.amountMicros ?: 0 -val StoreProduct.introductoryPriceCycles: Int - get() = this.introductoryPhase?.billingCycleCount ?: 0 +val StoreProduct.googleIntroductoryPrice: String? + get() = this.googleIntroductoryPhase?.price?.formatted +val StoreProduct.googleIntroductoryPricePeriod: Period? + get() = this.googleIntroductoryPhase?.billingPeriod +val StoreProduct.googleIntroductoryPriceAmountMicros: Long + get() = this.googleIntroductoryPhase?.price?.amountMicros ?: 0 +val StoreProduct.googleIntroductoryPriceCycles: Int + get() = this.googleIntroductoryPhase?.billingCycleCount ?: 0 private const val DAYS_PER_WEEK = 7 private const val MICROS_CONVERSION_METRIC = 1_000_000.0 @@ -102,37 +104,47 @@ internal fun StoreProduct.mapProductType(): String { @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) internal fun StoreProduct.mapIntroPrice(): Map? { - return when { - freeTrialPeriod != null -> { - // Check freeTrialPeriod first to give priority to trials - // Format using device locale. iOS will format using App Store locale, but there's no way - // to figure out how the price in the SKUDetails is being formatted. - freeTrialPeriod?.mapPeriodForStoreProduct()?.let { periodFields -> - mapOf( - "price" to 0, - "priceString" to formatUsingDeviceLocale(priceCurrencyCode, 0), - "period" to freeTrialPeriod?.iso8601, - "cycles" to (freeTrialCycles ?: 1), - ) + periodFields - } + return when (this) { + is AmazonStoreProduct -> freeTrialPeriod?.mapPeriodForStoreProduct()?.let { periodFields -> + mapOf( + "price" to 0, + "priceString" to formatUsingDeviceLocale(priceCurrencyCode, 0), + "period" to freeTrialPeriod?.iso8601, + "cycles" to 1, + ) + periodFields } - introductoryPrice != null -> { - introductoryPricePeriod?.mapPeriodForStoreProduct()?.let { periodFields -> - mapOf( - "price" to introductoryPriceAmountMicros / MICROS_CONVERSION_METRIC, - "priceString" to introductoryPrice, - "period" to introductoryPricePeriod?.iso8601, - "cycles" to introductoryPriceCycles, - ) + periodFields + else -> when { + googleFreeTrialPeriod != null -> { + // Check freeTrialPeriod first to give priority to trials + // Format using device locale. iOS will format using App Store locale, but there's no way + // to figure out how the price in the SKUDetails is being formatted. + googleFreeTrialPeriod?.mapPeriodForStoreProduct()?.let { periodFields -> + mapOf( + "price" to 0, + "priceString" to formatUsingDeviceLocale(priceCurrencyCode, 0), + "period" to googleFreeTrialPeriod?.iso8601, + "cycles" to (googleFreeTrialCycles ?: 1), + ) + periodFields + } + } + googleIntroductoryPrice != null -> { + googleIntroductoryPricePeriod?.mapPeriodForStoreProduct()?.let { periodFields -> + mapOf( + "price" to googleIntroductoryPriceAmountMicros / MICROS_CONVERSION_METRIC, + "priceString" to googleIntroductoryPrice, + "period" to googleIntroductoryPricePeriod?.iso8601, + "cycles" to googleIntroductoryPriceCycles, + ) + periodFields + } + } + else -> { + null } - } - else -> { - null } } } -private fun Period.mapPeriodForStoreProduct(): Map? { +private fun Period.mapPeriodForStoreProduct(): Map { return when (this.unit) { Period.Unit.DAY -> mapOf( "periodUnit" to "DAY", diff --git a/android/hybridcommon/src/test/java/com/revenuecat/purchases/hybridcommon/mappers/StoreProductIntroPriceMapperTests.kt b/android/hybridcommon/src/test/java/com/revenuecat/purchases/hybridcommon/mappers/StoreProductIntroPriceMapperTests.kt index 9c9822799..91d16a967 100644 --- a/android/hybridcommon/src/test/java/com/revenuecat/purchases/hybridcommon/mappers/StoreProductIntroPriceMapperTests.kt +++ b/android/hybridcommon/src/test/java/com/revenuecat/purchases/hybridcommon/mappers/StoreProductIntroPriceMapperTests.kt @@ -1,5 +1,6 @@ package com.revenuecat.purchases.hybridcommon.mappers +import com.revenuecat.purchases.amazon.AmazonStoreProduct import com.revenuecat.purchases.models.Period import com.revenuecat.purchases.models.Price import com.revenuecat.purchases.models.PricingPhase @@ -93,10 +94,10 @@ internal class StoreProductIntroPriceMapperTests { inner class MappingIntroPrice { @BeforeEach fun beforeEachTest() { - every { mockStoreProduct.freeTrialPeriod?.iso8601 } returns null - every { mockStoreProduct.introductoryPriceAmountMicros } returns 10_000_000 - every { mockStoreProduct.introductoryPrice } returns "$10.00" - every { mockStoreProduct.introductoryPriceCycles } returns 2 + every { mockStoreProduct.googleFreeTrialPeriod?.iso8601 } returns null + every { mockStoreProduct.googleIntroductoryPriceAmountMicros } returns 10_000_000 + every { mockStoreProduct.googleIntroductoryPrice } returns "$10.00" + every { mockStoreProduct.googleIntroductoryPriceCycles } returns 2 } private val expectedCommon = mapOf( @@ -107,8 +108,8 @@ internal class StoreProductIntroPriceMapperTests { @Test fun `of 7 days, the map has the correct intro price values`() { - every { mockStoreProduct.freeTrialPeriod } returns null - every { mockStoreProduct.introductoryPricePeriod } returns Period(7, Period.Unit.DAY, "P7D") + every { mockStoreProduct.googleFreeTrialPeriod } returns null + every { mockStoreProduct.googleIntroductoryPricePeriod } returns Period(7, Period.Unit.DAY, "P7D") received = mockStoreProduct.mapIntroPrice() val expected = mapOf( "period" to "P7D", @@ -120,8 +121,8 @@ internal class StoreProductIntroPriceMapperTests { @Test fun `of 1 month, the map has the correct intro price values`() { - every { mockStoreProduct.freeTrialPeriod } returns null - every { mockStoreProduct.introductoryPricePeriod } returns Period(1, Period.Unit.MONTH, "P1M") + every { mockStoreProduct.googleFreeTrialPeriod } returns null + every { mockStoreProduct.googleIntroductoryPricePeriod } returns Period(1, Period.Unit.MONTH, "P1M") received = mockStoreProduct.mapIntroPrice() val expected = mapOf( @@ -134,8 +135,8 @@ internal class StoreProductIntroPriceMapperTests { @Test fun `of 0 days, the map has the correct intro price values`() { - every { mockStoreProduct.freeTrialPeriod } returns null - every { mockStoreProduct.introductoryPricePeriod } returns Period(0, Period.Unit.DAY, "P0D") + every { mockStoreProduct.googleFreeTrialPeriod } returns null + every { mockStoreProduct.googleIntroductoryPricePeriod } returns Period(0, Period.Unit.DAY, "P0D") received = mockStoreProduct.mapIntroPrice() val expected = mapOf( @@ -149,9 +150,35 @@ internal class StoreProductIntroPriceMapperTests { @Test fun `when mapping a StoreProduct with no free trial nor introductory price, intro price is null`() { - every { mockStoreProduct.freeTrialPeriod } returns null - every { mockStoreProduct.introductoryPrice } returns null + every { mockStoreProduct.googleFreeTrialPeriod } returns null + every { mockStoreProduct.googleIntroductoryPrice } returns null received = mockStoreProduct.mapIntroPrice() assertThat(received).isEqualTo(null) } + + @Test + fun `when mapping a AmazonStoreProduct with no free trial nor introductory price, intro price is null`() { + val mockAmazonStoreProduct = mockk(relaxed = true) + + every { mockAmazonStoreProduct.freeTrialPeriod } returns null + received = mockAmazonStoreProduct.mapIntroPrice() + assertThat(received).isEqualTo(null) + } + + @Test + fun `when mapping a AmazonStoreProduct with free trial nor introductory price, introPrice has the free trial`() { + val mockAmazonStoreProduct = mockk(relaxed = true) + every { mockAmazonStoreProduct.priceCurrencyCode } returns "USD" + every { mockAmazonStoreProduct.freeTrialPeriod } returns Period(1, Period.Unit.DAY, "P1D") + received = mockAmazonStoreProduct.mapIntroPrice() + val expected = mapOf( + "cycles" to 1, + "period" to "P1D", + "periodUnit" to "DAY", + "periodNumberOfUnits" to 1, + "price" to 0, + "priceString" to "$0.00", + ) + assertThat(expected).isEqualTo(received) + } }