Skip to content

Commit

Permalink
Handle claiming winback offer
Browse files Browse the repository at this point in the history
  • Loading branch information
MiSikora committed Feb 5, 2025
1 parent beba17f commit dea4f16
Show file tree
Hide file tree
Showing 8 changed files with 332 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
Expand Down Expand Up @@ -35,6 +34,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.core.os.BundleCompat
Expand All @@ -48,6 +48,7 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.navArgument
import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground
import au.com.shiftyjelly.pocketcasts.compose.components.ProgressDialog
import au.com.shiftyjelly.pocketcasts.compose.components.TextH50
import au.com.shiftyjelly.pocketcasts.compose.theme
import au.com.shiftyjelly.pocketcasts.models.type.BillingPeriod
Expand Down Expand Up @@ -120,12 +121,7 @@ class WinbackFragment : BaseDialogFragment() {
WinbackOfferPage(
offer = state.winbackOfferState?.offer,
onClaimOffer = { offer ->
viewModel.trackClaimOfferTapped()
navController.navigate(WinbackNavRoutes.offerClaimedDestination(offer.details.billingPeriod)) {
popUpTo(WinbackNavRoutes.WinbackOffer) {
inclusive = true
}
}
viewModel.claimOffer(offer, requireActivity())
},
onSeeAvailablePlans = {
viewModel.trackAvailablePlansTapped()
Expand Down Expand Up @@ -164,7 +160,7 @@ class WinbackFragment : BaseDialogFragment() {
composable(WinbackNavRoutes.AvailablePlans) {
AvailablePlansPage(
plansState = state.subscriptionPlansState,
onSelectPlan = { plan -> viewModel.changePlan(requireActivity() as AppCompatActivity, plan) },
onSelectPlan = { plan -> viewModel.changePlan(plan, requireActivity()) },
onGoToSubscriptions = {
if (!goToPlayStoreSubscriptions()) {
scope.launch {
Expand Down Expand Up @@ -223,6 +219,33 @@ class WinbackFragment : BaseDialogFragment() {
}
}

val offerState = state.winbackOfferState
if (offerState?.isClaimingOffer == true) {
ProgressDialog(
text = stringResource(LR.string.winback_claiming_offer),
onDismiss = {},
)
}

if (offerState?.isOfferClaimed == true) {
LaunchedEffect(Unit) {
viewModel.consumeClaimedOffer()
val billingPeriod = offerState.offer.details.billingPeriod
navController.navigate(WinbackNavRoutes.offerClaimedDestination(billingPeriod)) {
popUpTo(WinbackNavRoutes.WinbackOffer) {
inclusive = true
}
}
}
}

val hasClaimOfferFailed = offerState?.hasOfferClaimFailed == true
if (hasClaimOfferFailed) {
LaunchedEffect(Unit) {
snackbarHostState.showSnackbar(getString(LR.string.error_generic_message))
}
}

val hasPlanChangeFailed = (state.subscriptionPlansState as? SubscriptionPlansState.Loaded)?.hasPlanChangeFailed == true
if (hasPlanChangeFailed) {
LaunchedEffect(Unit) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package au.com.shiftyjelly.pocketcasts.profile.winback

import androidx.appcompat.app.AppCompatActivity
import android.app.Activity
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import au.com.shiftyjelly.pocketcasts.analytics.AnalyticsEvent
Expand Down Expand Up @@ -93,8 +93,8 @@ class WinbackViewModel @Inject constructor(
private var changePlanJob: Job? = null

internal fun changePlan(
activity: AppCompatActivity,
newPlan: SubscriptionPlan,
activity: Activity,
) {
trackPlanSelected(newPlan.productId)
if (changePlanJob?.isActive == true) {
Expand Down Expand Up @@ -166,6 +166,71 @@ class WinbackViewModel @Inject constructor(
}
}

private var claimOfferjob: Job? = null

internal fun claimOffer(
offer: WinbackOffer,
activity: Activity,
) {
trackClaimOfferTapped()
if (claimOfferjob?.isActive == true) {
return
}

val loadedState = (_uiState.value.subscriptionPlansState as? SubscriptionPlansState.Loaded) ?: run {
logWarning("Failed to start winback offer flow. Subscriptions are not loaded.")
return
}
val currentPurchase = _uiState.value.purchases.find { it.orderId == loadedState.activePurchase.orderId } ?: run {
logWarning("Failed to start winback offer flow. No matching current purchase.")
return
}
val winbackProduct = _uiState.value.productsDetails.find { product ->
product.subscriptionOfferDetails.orEmpty().any { playOffer -> playOffer.offerToken == offer.offerToken }
} ?: run {
logWarning("Failed to start winback offer flow. No matching product for a winback offer.")
return
}
claimOfferjob = viewModelScope.launch {
_uiState.value = _uiState.value.withOfferState { state ->
state.copy(isClaimingOffer = true)
}
val purchaseEvent = winbackManager.claimWinbackOffer(
currentPurchase = currentPurchase,
winbackProduct = winbackProduct,
winbackOfferToken = offer.offerToken,
winbackClaimCode = offer.redeemCode,
activity = activity,
)
when (purchaseEvent) {
is PurchaseEvent.Cancelled -> {
_uiState.value = _uiState.value.withOfferState { state ->
state.copy(isClaimingOffer = false)
}
}

is PurchaseEvent.Failure -> {
logWarning("Winback offer failure: ${purchaseEvent.responseCode}, ${purchaseEvent.errorMessage}")
_uiState.value = _uiState.value.withOfferState { state ->
state.copy(isClaimingOffer = false, hasOfferClaimFailed = true)
}
}

is PurchaseEvent.Success -> {
_uiState.value = _uiState.value.withOfferState { state ->
state.copy(isClaimingOffer = false, isOfferClaimed = true)
}
}
}
}
}

internal fun consumeClaimedOffer() {
_uiState.value = _uiState.value.withOfferState { state ->
state.copy(isOfferClaimed = false)
}
}

private suspend fun loadPlans() = when (val state = winbackManager.loadProducts()) {
is ProductDetailsState.Loaded -> state.productDetails to state.productDetails.toSubscriptionPlans()
is ProductDetailsState.Failure -> emptyList<ProductDetails>() to null
Expand Down Expand Up @@ -243,6 +308,14 @@ class WinbackViewModel @Inject constructor(
}
}

private fun UiState.withOfferState(block: (WinbackOfferState) -> WinbackOfferState): UiState {
return if (winbackOfferState != null) {
copy(winbackOfferState = block(winbackOfferState))
} else {
this
}
}

private fun logWarning(message: String) {
Timber.tag(LogBuffer.TAG_SUBSCRIPTIONS).w(message)
LogBuffer.w(LogBuffer.TAG_SUBSCRIPTIONS, message)
Expand All @@ -268,7 +341,7 @@ class WinbackViewModel @Inject constructor(
)
}

internal fun trackClaimOfferTapped() {
private fun trackClaimOfferTapped() {
val activePurchase = (uiState.value.subscriptionPlansState as? SubscriptionPlansState.Loaded)?.activePurchase
tracker.track(
event = AnalyticsEvent.WINBACK_MAIN_SCREEN_ROW_TAP,
Expand Down Expand Up @@ -374,6 +447,7 @@ class WinbackViewModel @Inject constructor(
internal data class WinbackOfferState(
val offer: WinbackOffer,
val isClaimingOffer: Boolean = false,
val isOfferClaimed: Boolean = false,
val hasOfferClaimFailed: Boolean = false,
)

Expand Down
Loading

0 comments on commit dea4f16

Please sign in to comment.