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 @@ -75,8 +75,7 @@ fun UpgradePlanRow(
modifier: Modifier = Modifier,
priceComparisonPlan: SubscriptionPlan? = null,
) {
// Don't show savings percent for installment plans
val calculatedSavingPercent = if (plan is SubscriptionPlan.Base && plan.isInstallment) {
val calculatedSavingPercent = if (plan.isInstallment && plan.offer == null) {
null
} else {
priceComparisonPlan?.let { plan.savingsPercent(priceComparisonPlan) }
Expand Down Expand Up @@ -202,7 +201,7 @@ private fun SubscriptionPlanRow(
verticalArrangement = Arrangement.spacedBy(rowConfig.labelSpacing),
) {
TextH30(
text = plan.name,
text = plan.displayName(),
fontSize = rowConfig.mainTextSize,
)
TextP40(
Expand Down Expand Up @@ -289,26 +288,40 @@ private fun CheckMark(

private val SubscriptionPlan.pricePerMonth: Float
get() {
val totalYearlyAmount = if (isInstallment) {
recurringPrice.amount * monthsInYear
} else {
when (billingCycle) {
BillingCycle.Monthly -> recurringPrice.amount
BillingCycle.Yearly -> recurringPrice.amount
}
}

val pricePerMonth = when (billingCycle) {
BillingCycle.Monthly -> recurringPrice.amount
BillingCycle.Yearly -> recurringPrice.amount / monthsInYear
BillingCycle.Monthly -> totalYearlyAmount
BillingCycle.Yearly -> totalYearlyAmount / monthsInYear
}
return pricePerMonth.toFloat()
}

private val SubscriptionPlan.pricePerWeek: Float
get() {
val pricePerWeek = when (billingCycle) {
BillingCycle.Monthly -> recurringPrice.amount * monthsInYear
BillingCycle.Yearly -> recurringPrice.amount
} / weeksInYear
return pricePerWeek.toFloat()
val totalYearlyAmount = if (isInstallment) {
recurringPrice.amount * monthsInYear
} else {
when (billingCycle) {
BillingCycle.Monthly -> recurringPrice.amount * monthsInYear
BillingCycle.Yearly -> recurringPrice.amount
}
}

return (totalYearlyAmount / weeksInYear).toFloat()
}

private val monthsInYear = 12.toBigDecimal()
private val weeksInYear = 52.toBigDecimal()

private fun SubscriptionPlan.Base.formattedTotalYearlyPrice(): String {
private fun SubscriptionPlan.formattedTotalYearlyPrice(): String {
val totalAmount = recurringPrice.amount * monthsInYear
val currencyCode = recurringPrice.currencyCode

Expand All @@ -327,8 +340,30 @@ private fun SubscriptionPlan.Base.formattedTotalYearlyPrice(): String {

@Composable
private fun SubscriptionPlan.pricePerPeriod(config: RowConfig): String? {
if (this is SubscriptionPlan.Base && isInstallment) {
return stringResource(LR.string.plus_per_year, formattedTotalYearlyPrice())
if (isInstallment) {
return when (this) {
is SubscriptionPlan.WithOffer -> when (config.pricePerPeriod) {
PricePerPeriod.PRICE_PER_WEEK -> {
val currencyCode = recurringPrice.currencyCode
if (currencyCode == "USD") {
stringResource(LR.string.price_per_week_usd, pricePerWeek)
} else {
stringResource(LR.string.price_per_week, pricePerWeek, currencyCode)
}
}

PricePerPeriod.PRICE_PER_MONTH -> {
val currencyCode = recurringPrice.currencyCode
if (currencyCode == "USD") {
stringResource(LR.string.price_per_month_usd, pricePerMonth)
} else {
stringResource(LR.string.price_per_month, pricePerMonth, currencyCode)
}
}
}

is SubscriptionPlan.Base -> stringResource(LR.string.plus_per_year, formattedTotalYearlyPrice())
}
}

return if (this.billingCycle == BillingCycle.Yearly) {
Expand Down Expand Up @@ -360,14 +395,24 @@ private fun SubscriptionPlan.savingsPercent(otherPlan: SubscriptionPlan) = 100 -

@Composable
@ReadOnlyComposable
private fun SubscriptionPlan.price(): String {
val formattedPrice = recurringPrice.formattedPrice
private fun SubscriptionPlan.displayName(): String {
return when {
isInstallment -> stringResource(LR.string.plus_yearly_installments)
else -> name
}
}

// For installment plans, show price per month with duration
if (this is SubscriptionPlan.Base && isInstallment) {
return stringResource(LR.string.price_per_month_for_months, formattedPrice, monthsInYear.toInt())
@Composable
@ReadOnlyComposable
private fun SubscriptionPlan.price(): String {
if (isInstallment) {
return when (this) {
is SubscriptionPlan.Base -> stringResource(LR.string.plus_per_month, recurringPrice.formattedPrice)
is SubscriptionPlan.WithOffer -> stringResource(LR.string.plus_per_year, formattedTotalYearlyPrice())
}
}

val formattedPrice = recurringPrice.formattedPrice
return when (billingCycle) {
BillingCycle.Monthly -> stringResource(LR.string.plus_per_month, formattedPrice)
BillingCycle.Yearly -> stringResource(LR.string.plus_per_year, formattedPrice)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,11 +154,16 @@ class OnboardingUpgradeFeaturesViewModel @AssistedInject constructor(
if (FeatureFlag.isEnabled(Feature.NEW_ONBOARDING_UPGRADE)) {
analyticsTracker.track(
AnalyticsEvent.PLUS_PROMOTION_UPGRADE_BUTTON_TAPPED,
analyticsProps(
flow = flow,
source = source,
variant = experimentProvider.getVariation(Experiment.NewOnboardingABTest).toNewOnboardingVariant(),
),
buildMap {
putAll(
analyticsProps(
flow = flow,
source = source,
variant = experimentProvider.getVariation(Experiment.NewOnboardingABTest).toNewOnboardingVariant(),
),
)
put("is_installment", plan.isInstallment.toString())
},
)
} else {
analyticsTracker.track(
Expand All @@ -167,6 +172,7 @@ class OnboardingUpgradeFeaturesViewModel @AssistedInject constructor(
"flow" to flow.analyticsValue,
"source" to flow.source.analyticsValue,
"product" to requireNotNull(plan.productId) { "productId shouldn't be null for plan=$plan" },
"is_installment" to plan.isInstallment.toString(),
),
)
}
Expand Down Expand Up @@ -291,18 +297,20 @@ sealed class OnboardingUpgradeFeaturesState {
}

private fun plusYearlyPlanWithOffer(): SubscriptionPlan {
// When installment plan feature is enabled, show installment plan instead of trial/intro offer
if (FeatureFlag.isEnabled(Feature.NEW_INSTALLMENT_PLAN)) {
return plusYearlyPlan()
}
val isInstallmentEnabled = FeatureFlag.isEnabled(Feature.NEW_INSTALLMENT_PLAN)

val offer = if (FeatureFlag.isEnabled(Feature.INTRO_PLUS_OFFER_ENABLED)) {
SubscriptionOffer.IntroOffer
} else {
SubscriptionOffer.Trial
}

val offerPlan = subscriptionPlans.findOfferPlan(SubscriptionTier.Plus, BillingCycle.Yearly, offer).getOrNull()
val offerPlan = subscriptionPlans.findOfferPlan(
SubscriptionTier.Plus,
BillingCycle.Yearly,
offer,
isInstallment = isInstallmentEnabled,
).getOrNull()
return if (offerPlan == null || OnboardingSubscriptionPlan.create(offerPlan).getOrNull() == null) {
plusYearlyPlan()
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import au.com.shiftyjelly.pocketcasts.localization.R as LR
@Composable
internal fun OfferClaimedPage(
billingCycle: BillingCycle,
isInstallment: Boolean,
onConfirm: () -> Unit,
modifier: Modifier = Modifier,
) {
Expand All @@ -61,9 +62,10 @@ internal fun OfferClaimedPage(
modifier = Modifier.height(20.dp),
)
Text(
text = when (billingCycle) {
BillingCycle.Monthly -> stringResource(LR.string.winback_claimed_offer_message_1)
BillingCycle.Yearly -> stringResource(LR.string.winback_claimed_offer_message_3)
text = when {
billingCycle == BillingCycle.Monthly -> stringResource(LR.string.winback_claimed_offer_message_1)
isInstallment -> stringResource(LR.string.winback_claimed_offer_message_5)
else -> stringResource(LR.string.winback_claimed_offer_message_3)
},
fontWeight = FontWeight.Bold,
fontSize = 28.sp,
Expand All @@ -76,9 +78,10 @@ internal fun OfferClaimedPage(
modifier = Modifier.height(16.dp),
)
TextP30(
text = when (billingCycle) {
BillingCycle.Monthly -> stringResource(LR.string.winback_claimed_offer_message_2)
BillingCycle.Yearly -> stringResource(LR.string.winback_claimed_offer_message_4)
text = when {
billingCycle == BillingCycle.Monthly -> stringResource(LR.string.winback_claimed_offer_message_2)
isInstallment -> stringResource(LR.string.winback_claimed_offer_message_6)
else -> stringResource(LR.string.winback_claimed_offer_message_4)
},
color = MaterialTheme.theme.colors.primaryText02,
textAlign = TextAlign.Center,
Expand All @@ -100,13 +103,14 @@ internal fun OfferClaimedPage(
@Preview(device = Devices.PORTRAIT_REGULAR)
@Composable
private fun WinbackOfferPageBillingPeriodPreview(
@PreviewParameter(BillingPeriodParameterProvider::class) billingPeriod: BillingCycle,
@PreviewParameter(OfferClaimedParameterProvider::class) params: OfferClaimedParams,
) {
AppThemeWithBackground(
themeType = ThemeType.ROSE,
) {
OfferClaimedPage(
billingCycle = billingPeriod,
billingCycle = params.billingCycle,
isInstallment = params.isInstallment,
onConfirm = {},
)
}
Expand All @@ -122,14 +126,21 @@ private fun WinbackOfferPageThemePreview(
) {
OfferClaimedPage(
billingCycle = BillingCycle.Monthly,
isInstallment = false,
onConfirm = {},
)
}
}

private class BillingPeriodParameterProvider : PreviewParameterProvider<BillingCycle> {
private data class OfferClaimedParams(
val billingCycle: BillingCycle,
val isInstallment: Boolean,
)

private class OfferClaimedParameterProvider : PreviewParameterProvider<OfferClaimedParams> {
override val values = sequenceOf(
BillingCycle.Monthly,
BillingCycle.Yearly,
OfferClaimedParams(billingCycle = BillingCycle.Monthly, isInstallment = false),
OfferClaimedParams(billingCycle = BillingCycle.Yearly, isInstallment = false),
OfferClaimedParams(billingCycle = BillingCycle.Yearly, isInstallment = true),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.core.os.BundleCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.fragment.compose.content
Expand Down Expand Up @@ -126,16 +125,21 @@ class WinbackFragment : BaseDialogFragment() {
composable(
WinbackNavRoutes.offerClaimedRoute(),
listOf(
navArgument(WinbackNavRoutes.OFER_CLAIMED_BILLING_CYCLE_ARGUMENT) {
navArgument(WinbackNavRoutes.OFFER_CLAIMED_BILLING_CYCLE_ARGUMENT) {
type = NavType.EnumType(BillingCycle::class.java)
},
navArgument(WinbackNavRoutes.OFFER_CLAIMED_IS_INSTALLMENT_ARGUMENT) {
type = NavType.BoolType
},
),
) { backStackEntry ->
val arguments = requireNotNull(backStackEntry.arguments) { "Missing back stack entry arguments" }
val billingCycle = arguments.requireSerializable<BillingCycle>(WinbackNavRoutes.OFER_CLAIMED_BILLING_CYCLE_ARGUMENT)
val billingCycle = arguments.requireSerializable<BillingCycle>(WinbackNavRoutes.OFFER_CLAIMED_BILLING_CYCLE_ARGUMENT)
val isInstallment = arguments.getBoolean(WinbackNavRoutes.OFFER_CLAIMED_IS_INSTALLMENT_ARGUMENT)

OfferClaimedPage(
billingCycle = billingCycle,
isInstallment = isInstallment,
onConfirm = {
viewModel.trackOfferClaimedConfirmationTapped()
dismiss()
Expand Down Expand Up @@ -236,7 +240,8 @@ class WinbackFragment : BaseDialogFragment() {
LaunchedEffect(Unit) {
viewModel.consumeClaimedOffer()
val billingCycle = offerState.offer.billingCycle
navController.navigate(WinbackNavRoutes.offerClaimedDestination(billingCycle)) {
val isInstallment = offerState.offer.isInstallment
navController.navigate(WinbackNavRoutes.offerClaimedDestination(billingCycle, isInstallment)) {
popUpTo(WinbackNavRoutes.MAIN) {
inclusive = true
}
Expand Down Expand Up @@ -364,11 +369,12 @@ private object WinbackNavRoutes {
const val WINBACK_OFFER = "winback_offer"
private const val OFFER_CLAIMED = "offer_claimed"

const val OFER_CLAIMED_BILLING_CYCLE_ARGUMENT = "billingCycle"
const val OFFER_CLAIMED_BILLING_CYCLE_ARGUMENT = "billingCycle"
const val OFFER_CLAIMED_IS_INSTALLMENT_ARGUMENT = "isInstallment"

fun offerClaimedRoute() = "$OFFER_CLAIMED/{$OFER_CLAIMED_BILLING_CYCLE_ARGUMENT}"
fun offerClaimedRoute() = "$OFFER_CLAIMED/{$OFFER_CLAIMED_BILLING_CYCLE_ARGUMENT}/{$OFFER_CLAIMED_IS_INSTALLMENT_ARGUMENT}"

fun offerClaimedDestination(billingCycle: BillingCycle) = "$OFFER_CLAIMED/$billingCycle"
fun offerClaimedDestination(billingCycle: BillingCycle, isInstallment: Boolean) = "$OFFER_CLAIMED/$billingCycle/$isInstallment"
}

private val intOffsetAnimationSpec = tween<IntOffset>(350)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,9 +80,10 @@ internal fun WinbackOfferPage(
modifier = Modifier.height(20.dp),
)
Text(
text = when (offer.billingCycle) {
BillingCycle.Monthly -> stringResource(LR.string.winback_offer_free_offer_title, offer.formattedPrice)
BillingCycle.Yearly -> stringResource(LR.string.winback_offer_free_offer_yearly_title, offer.formattedPrice)
text = when {
offer.billingCycle == BillingCycle.Monthly -> stringResource(LR.string.winback_offer_free_offer_title, offer.formattedPrice)
offer.isInstallment && offer.formattedTotalSavings != null -> stringResource(LR.string.winback_offer_free_offer_installment_title, offer.formattedPrice, offer.formattedTotalSavings)
else -> stringResource(LR.string.winback_offer_free_offer_yearly_title, offer.formattedPrice)
},
fontWeight = FontWeight.Bold,
fontSize = 28.sp,
Expand All @@ -95,9 +96,10 @@ internal fun WinbackOfferPage(
modifier = Modifier.height(16.dp),
)
TextP30(
text = when (offer.billingCycle) {
BillingCycle.Monthly -> stringResource(LR.string.winback_offer_free_offer_description, offer.formattedPrice)
BillingCycle.Yearly -> stringResource(LR.string.winback_offer_free_offer_yearly_description, offer.formattedPrice)
text = when {
offer.billingCycle == BillingCycle.Monthly -> stringResource(LR.string.winback_offer_free_offer_description, offer.formattedPrice)
offer.isInstallment -> stringResource(LR.string.winback_offer_free_offer_installment_description, offer.formattedPrice)
else -> stringResource(LR.string.winback_offer_free_offer_yearly_description, offer.formattedPrice)
},
color = MaterialTheme.theme.colors.primaryText02,
textAlign = TextAlign.Center,
Expand Down Expand Up @@ -260,6 +262,8 @@ private fun WinbackOfferPageThemePreview(
formattedPrice = "\$3.99",
tier = SubscriptionTier.Plus,
billingCycle = BillingCycle.Monthly,
isInstallment = false,
formattedTotalSavings = null,
),
)
}
Expand All @@ -272,12 +276,24 @@ private class WinbackOfferParameterProvider : PreviewParameterProvider<WinbackOf
formattedPrice = "\$3.99",
tier = SubscriptionTier.Plus,
billingCycle = BillingCycle.Monthly,
isInstallment = false,
formattedTotalSavings = null,
),
WinbackOffer(
redeemCode = "",
formattedPrice = "\$19.99",
tier = SubscriptionTier.Plus,
billingCycle = BillingCycle.Yearly,
isInstallment = false,
formattedTotalSavings = null,
),
WinbackOffer(
redeemCode = "",
formattedPrice = "\$2.49",
tier = SubscriptionTier.Plus,
billingCycle = BillingCycle.Yearly,
isInstallment = true,
formattedTotalSavings = "\$29.88",
),
)
}
Loading