Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -102,37 +104,47 @@ internal fun StoreProduct.mapProductType(): String {

@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal fun StoreProduct.mapIntroPrice(): Map<String, Any?>? {
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<String, Any?>? {
private fun Period.mapPeriodForStoreProduct(): Map<String, Any?> {
return when (this.unit) {
Period.Unit.DAY -> mapOf(
"periodUnit" to "DAY",
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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",
Expand All @@ -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(
Expand All @@ -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(
Expand All @@ -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<AmazonStoreProduct>(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<AmazonStoreProduct>(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)
}
}