Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle claiming winback offer #3551

Merged
merged 1 commit into from
Feb 6, 2025
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 @@ -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