Skip to content

Commit

Permalink
Plus promotion login (#1726)
Browse files Browse the repository at this point in the history
  • Loading branch information
ashiagr authored Jan 23, 2024
1 parent ca475be commit 70c9aa1
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import au.com.shiftyjelly.pocketcasts.account.onboarding.OnboardingActivity
import au.com.shiftyjelly.pocketcasts.account.onboarding.OnboardingFlowComposable
import au.com.shiftyjelly.pocketcasts.account.onboarding.OnboardingNavRoute
import au.com.shiftyjelly.pocketcasts.models.to.SignInState
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingExitInfo
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingFlow
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingUpgradeSource
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
Expand All @@ -33,7 +34,7 @@ class OnboardingFlowComposableTest {
fun setupAppNavHost(
flow: OnboardingFlow,
signInState: SignInState = mock(),
exitOnboarding: () -> Unit = {},
exitOnboarding: (OnboardingExitInfo) -> Unit = {},
completeOnboardingToDiscover: () -> Unit = {},
) {
composeTestRule.activity.setContent {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ import au.com.shiftyjelly.pocketcasts.servers.ServerManager
import au.com.shiftyjelly.pocketcasts.servers.discover.PodcastSearch
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingFlow
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingLauncher
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingUpgradeSource
import au.com.shiftyjelly.pocketcasts.settings.whatsnew.WhatsNewFragment
import au.com.shiftyjelly.pocketcasts.ui.MainActivityViewModel.NavigationState
import au.com.shiftyjelly.pocketcasts.ui.helper.FragmentHostListener
Expand Down Expand Up @@ -248,6 +249,10 @@ class MainActivity :
settings.setHasDoneInitialOnboarding()
openTab(VR.id.navigation_discover)
}
OnboardingFinish.DoneShowPlusPromotion -> {
settings.setHasDoneInitialOnboarding()
OnboardingLauncher.openOnboardingFlow(this, OnboardingFlow.Upsell(OnboardingUpgradeSource.LOGIN_PLUS_PROMOTION))
}
null -> {
Timber.e("Unexpected null result from onboarding activity")
}
Expand Down
1 change: 1 addition & 0 deletions modules/features/account/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ dependencies {
implementation(project(":modules:services:ui"))
implementation(project(":modules:services:utils"))
implementation(project(":modules:services:views"))
testImplementation(project(":modules:services:sharedtest"))

// android libs
implementation(libs.horologist.auth.data.phone)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.core.content.IntentCompat
import androidx.core.view.WindowCompat
import au.com.shiftyjelly.pocketcasts.account.onboarding.OnboardingActivityContract.OnboardingFinish
import au.com.shiftyjelly.pocketcasts.account.viewmodel.OnboardingActivityViewModel
import au.com.shiftyjelly.pocketcasts.repositories.user.UserManager
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingFlow
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
Expand All @@ -25,6 +27,8 @@ class OnboardingActivity : AppCompatActivity() {

@Inject lateinit var userManager: UserManager

private val viewModel: OnboardingActivityViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Make content edge-to-edge
Expand All @@ -34,6 +38,9 @@ class OnboardingActivity : AppCompatActivity() {
val signInState = userManager.getSignInState().asFlow().collectAsState(null)
val currentSignInState = signInState.value

val finishState = viewModel.finishState.collectAsState(null)
finishState.value?.let { finishWithResult(it) }

if (currentSignInState != null) {
val onboardingFlow = remember(savedInstanceState) {
IntentCompat.getParcelableExtra(intent, ANALYTICS_FLOW_KEY, OnboardingFlow::class.java)
Expand All @@ -46,7 +53,7 @@ class OnboardingActivity : AppCompatActivity() {
OnboardingFlowComposable(
theme = theme.activeTheme,
flow = onboardingFlow,
exitOnboarding = { finishWithResult(OnboardingFinish.Done) },
exitOnboarding = { viewModel.onExitOnboarding(it) },
completeOnboardingToDiscover = { finishWithResult(OnboardingFinish.DoneGoToDiscover) },
signInState = currentSignInState,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class OnboardingActivityContract : ActivityResultContract<Intent, OnboardingActi
enum class OnboardingFinish {
Done,
DoneGoToDiscover,
DoneShowPlusPromotion,
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import au.com.shiftyjelly.pocketcasts.account.onboarding.recommendations.Onboard
import au.com.shiftyjelly.pocketcasts.account.onboarding.upgrade.OnboardingUpgradeFlow
import au.com.shiftyjelly.pocketcasts.compose.AppThemeWithBackground
import au.com.shiftyjelly.pocketcasts.models.to.SignInState
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingExitInfo
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingFlow
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingUpgradeSource
import au.com.shiftyjelly.pocketcasts.ui.theme.Theme
Expand All @@ -24,7 +25,7 @@ import au.com.shiftyjelly.pocketcasts.utils.extensions.getSerializableCompat
fun OnboardingFlowComposable(
theme: Theme.ThemeType,
flow: OnboardingFlow,
exitOnboarding: () -> Unit,
exitOnboarding: (OnboardingExitInfo) -> Unit,
completeOnboardingToDiscover: () -> Unit,
signInState: SignInState,
navController: NavHostController = rememberNavController(),
Expand Down Expand Up @@ -56,7 +57,7 @@ fun OnboardingFlowComposable(
private fun Content(
theme: Theme.ThemeType,
flow: OnboardingFlow,
exitOnboarding: () -> Unit,
exitOnboarding: (OnboardingExitInfo) -> Unit,
completeOnboardingToDiscover: () -> Unit,
signInState: SignInState,
navController: NavHostController,
Expand Down Expand Up @@ -89,7 +90,7 @@ private fun Content(
onboardingRecommendationsFlowGraph(
theme = theme,
flow = flow,
onBackPressed = exitOnboarding,
onBackPressed = { exitOnboarding(OnboardingExitInfo()) },
onComplete = {
navController.navigate(
if (signInState.isSignedInAsPlusOrPatron) {
Expand Down Expand Up @@ -118,13 +119,13 @@ private fun Content(
-> {
val popped = navController.popBackStack()
if (!popped) {
exitOnboarding()
exitOnboarding(OnboardingExitInfo())
}
}

OnboardingFlow.InitialOnboarding,
OnboardingFlow.LoggedOut,
-> exitOnboarding()
-> exitOnboarding(OnboardingExitInfo())
}
},
onSignUpClicked = { navController.navigate(OnboardingNavRoute.createFreeAccount) },
Expand Down Expand Up @@ -162,7 +163,7 @@ private fun Content(
OnboardingForgotPasswordPage(
theme = theme,
onBackPressed = { navController.popBackStack() },
onCompleted = exitOnboarding,
onCompleted = { exitOnboarding(OnboardingExitInfo()) },
)
}

Expand Down Expand Up @@ -203,6 +204,7 @@ private fun Content(
OnboardingUpgradeSource.FOLDERS,
OnboardingUpgradeSource.HEADPHONE_CONTROLS_SETTINGS,
OnboardingUpgradeSource.LOGIN,
OnboardingUpgradeSource.LOGIN_PLUS_PROMOTION,
OnboardingUpgradeSource.OVERFLOW_MENU,
OnboardingUpgradeSource.PLUS_DETAILS,
OnboardingUpgradeSource.PROFILE,
Expand All @@ -221,15 +223,15 @@ private fun Content(
if (userCreatedNewAccount) {
navController.popBackStack()
} else {
exitOnboarding()
exitOnboarding(OnboardingExitInfo())
}
},
onNeedLogin = { navController.navigate(OnboardingNavRoute.logInOrSignUp) },
onProceed = {
if (userCreatedNewAccount) {
navController.navigate(OnboardingNavRoute.welcome)
} else {
exitOnboarding()
exitOnboarding(OnboardingExitInfo())
}
},
)
Expand All @@ -240,13 +242,13 @@ private fun Content(
activeTheme = theme,
flow = flow,
isSignedInAsPlusOrPatron = signInState.isSignedInAsPlusOrPatron,
onDone = exitOnboarding,
onDone = { exitOnboarding(OnboardingExitInfo()) },
onContinueToDiscover = completeOnboardingToDiscover,
onImportTapped = { navController.navigate(OnboardingImportFlow.route) },
onBackPressed = {
// Don't allow navigation back to the upgrade screen after the user upgrades
if (signInState.isSignedInAsPlusOrPatron) {
exitOnboarding()
exitOnboarding(OnboardingExitInfo())
} else {
navController.popBackStack()
}
Expand All @@ -258,13 +260,13 @@ private fun Content(

private fun onLoginToExistingAccount(
flow: OnboardingFlow,
exitOnboarding: () -> Unit,
exitOnboarding: (OnboardingExitInfo) -> Unit,
navController: NavHostController,
) {
when (flow) {
OnboardingFlow.InitialOnboarding,
OnboardingFlow.LoggedOut,
-> exitOnboarding()
-> exitOnboarding(OnboardingExitInfo(showPlusPromotionForFreeUser = true))

is OnboardingFlow.PlusAccountUpgrade,
is OnboardingFlow.PatronAccountUpgrade,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package au.com.shiftyjelly.pocketcasts.account.viewmodel

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import au.com.shiftyjelly.pocketcasts.account.onboarding.OnboardingActivityContract.OnboardingFinish
import au.com.shiftyjelly.pocketcasts.models.to.SignInState
import au.com.shiftyjelly.pocketcasts.models.to.SubscriptionStatus
import au.com.shiftyjelly.pocketcasts.repositories.user.UserManager
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingExitInfo
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.reactive.asFlow

@HiltViewModel
class OnboardingActivityViewModel @Inject constructor(
private val userManager: UserManager,
) : ViewModel() {

private var showPlusPromotionForFreeUserFlow = MutableStateFlow(false)

private val _finishState = MutableSharedFlow<OnboardingFinish>()
val finishState = _finishState.asSharedFlow()

init {
viewModelScope.launch {
combine(
showPlusPromotionForFreeUserFlow,
userManager.getSignInState().asFlow(),
) { showPlusPromotionForFreeUser, signInState ->
if (showPlusPromotionForFreeUser) {
// subscriptionStatus is null just after sign in, so we need to wait for it to be set
// before we can finish the onboarding flow to show plus promotion for a free user
(signInState as? SignInState.SignedIn)?.subscriptionStatus?.let { status ->
showPlusPromotionForFreeUserFlow.value = false
if (status is SubscriptionStatus.Free) {
_finishState.emit(OnboardingFinish.DoneShowPlusPromotion)
} else {
_finishState.emit(OnboardingFinish.Done)
}
}
}
}.stateIn(viewModelScope)
}
}

fun onExitOnboarding(exitInfo: OnboardingExitInfo) {
if (exitInfo.showPlusPromotionForFreeUser) {
showPlusPromotionForFreeUserFlow.value = true
} else {
viewModelScope.launch {
_finishState.emit(OnboardingFinish.Done)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package au.com.shiftyjelly.pocketcasts.account.viewmodel

import app.cash.turbine.test
import au.com.shiftyjelly.pocketcasts.account.onboarding.OnboardingActivityContract.OnboardingFinish
import au.com.shiftyjelly.pocketcasts.models.to.SignInState
import au.com.shiftyjelly.pocketcasts.models.to.SubscriptionStatus
import au.com.shiftyjelly.pocketcasts.repositories.user.UserManager
import au.com.shiftyjelly.pocketcasts.settings.onboarding.OnboardingExitInfo
import au.com.shiftyjelly.pocketcasts.sharedtest.MainCoroutineRule
import io.reactivex.Flowable
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.junit.MockitoJUnitRunner
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@RunWith(MockitoJUnitRunner::class)
class OnboardingActivityViewModelTest {
@OptIn(ExperimentalCoroutinesApi::class)
@get:Rule
val coroutineRule = MainCoroutineRule()

@Mock
private lateinit var userManager: UserManager

@Mock
private lateinit var paidSubscriptionStatus: SubscriptionStatus.Paid

@Mock
private lateinit var freeSubscriptionStatus: SubscriptionStatus.Free

private lateinit var viewModel: OnboardingActivityViewModel

@Test
fun `given showPlusPromotionForFreeUser is false, when exit onboarding, then finish with Done`() = runTest {
initViewModel()

viewModel.finishState.test {
viewModel.onExitOnboarding(OnboardingExitInfo(showPlusPromotionForFreeUser = false))
assert(awaitItem() == OnboardingFinish.Done)
}
}

@Test
fun `given showPlusPromotionForFreeUser is true and free user, when exit onboarding, then finish with DoneShowPlusPromotion`() = runTest {
initViewModel(freeSubscriptionStatus)

viewModel.finishState.test {
viewModel.onExitOnboarding(OnboardingExitInfo(showPlusPromotionForFreeUser = true))
assert(awaitItem() == OnboardingFinish.DoneShowPlusPromotion)
}
}

@Test
fun `given showPlusPromotionForFreeUser is true and paid user, when exit onboarding, then finish with Done`() = runTest {
initViewModel(paidSubscriptionStatus)

viewModel.finishState.test {
viewModel.onExitOnboarding(OnboardingExitInfo(showPlusPromotionForFreeUser = true))
assert(awaitItem() == OnboardingFinish.Done)
}
}

private fun initViewModel(subscriptionStatus: SubscriptionStatus = mock<SubscriptionStatus>()) {
whenever(userManager.getSignInState()).thenReturn(
Flowable.just(
SignInState.SignedIn(email = "", subscriptionStatus = subscriptionStatus),
),
)
viewModel = OnboardingActivityViewModel(
userManager = userManager,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package au.com.shiftyjelly.pocketcasts.settings.onboarding

data class OnboardingExitInfo(
val showPlusPromotionForFreeUser: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ enum class OnboardingUpgradeSource(val analyticsValue: String) {
FILES("files"),
FOLDERS("folders"),
HEADPHONE_CONTROLS_SETTINGS("headphone_controls_settings"),
LOGIN("login"),
LOGIN("login"), // for login from within upsell screen
LOGIN_PLUS_PROMOTION("login_plus_promotion"), // for login from outside upsell screen
OVERFLOW_MENU("overflow_menu"),
PLUS_DETAILS("plus_details"),
PROFILE("profile"),
Expand Down

0 comments on commit 70c9aa1

Please sign in to comment.