Skip to content

Commit

Permalink
Merge branch 'develop' into feature/ALTAPPS-1291/Android-improve-trac…
Browse files Browse the repository at this point in the history
…k-switching-flow
  • Loading branch information
XanderZhu authored Jul 23, 2024
2 parents 539d488 + 148cf16 commit 3e6cd2f
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import org.hyperskill.app.core.domain.DataSourceType
import org.hyperskill.app.core.presentation.ActionDispatcherOptions
Expand Down Expand Up @@ -40,6 +41,8 @@ internal class MainGamificationToolbarActionDispatcher(
private val sentryInteractor: SentryInteractor
) : CoroutineActionDispatcher<Action, Message>(config.createConfig()) {

private var isMobileContentTrialEnabled: Boolean = false

init {
stepCompletedFlow.observe()
.onEach { onNewMessage(InternalMessage.StepSolved) }
Expand Down Expand Up @@ -72,6 +75,12 @@ internal class MainGamificationToolbarActionDispatcher(
.launchIn(actionScope)

currentSubscriptionStateRepository.changes
.map {
it.orContentTrial(
isMobileContentTrialEnabled = isMobileContentTrialEnabled,
canMakePayments = canMakePayments()
)
}
.distinctUntilChanged()
.onEach { onNewMessage(InternalMessage.SubscriptionChanged(it)) }
.launchIn(actionScope)
Expand Down Expand Up @@ -108,7 +117,10 @@ internal class MainGamificationToolbarActionDispatcher(
val gamificationToolbarDataWithSource = toolbarDataDeferred.await().getOrThrow()
val profile = profileDeferred.await().getOrThrow()

val canMakePayments = purchaseInteractor.canMakePayments().getOrDefault(false)
this@MainGamificationToolbarActionDispatcher.isMobileContentTrialEnabled =
profile.features.isMobileContentTrialEnabled

val canMakePayments = canMakePayments()

val subscription = getSubscription(
isMobileContentTrialEnabled = profile.features.isMobileContentTrialEnabled,
Expand Down Expand Up @@ -169,4 +181,7 @@ internal class MainGamificationToolbarActionDispatcher(
subscriptionWithSource.state
}
}

private suspend fun canMakePayments(): Boolean =
purchaseInteractor.canMakePayments().getOrDefault(false)
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.hyperskill.app.core.presentation.ActionDispatcherOptions
import org.hyperskill.app.profile.domain.model.freemiumChargeLimitsStrategy
import org.hyperskill.app.profile.domain.model.isMobileContentTrialEnabled
Expand All @@ -30,21 +29,19 @@ internal class MainStepQuizToolbarActionDispatcher(
private var isMobileContentTrialEnabled = false

init {
actionScope.launch {
currentSubscriptionStateRepository
.changes
.map { subscription ->
subscription.orContentTrial(
isMobileContentTrialEnabled = isMobileContentTrialEnabled,
canMakePayments = canMakePayments()
)
}
.distinctUntilChanged()
.onEach {
onNewMessage(InternalMessage.SubscriptionChanged(it))
}
.launchIn(this)
}
currentSubscriptionStateRepository
.changes
.map { subscription ->
subscription.orContentTrial(
isMobileContentTrialEnabled = isMobileContentTrialEnabled,
canMakePayments = canMakePayments()
)
}
.distinctUntilChanged()
.onEach {
onNewMessage(InternalMessage.SubscriptionChanged(it))
}
.launchIn(actionScope)
}

override suspend fun doSuspendableAction(action: Action) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import org.hyperskill.app.subscriptions.domain.model.getSubscriptionLimitType
/**
* @return current [StudyPlanSection].
*
* `studyPlanSections` map preserves the entry iteration order, so we can use the first element as the current section.
*
* @see StudyPlanWidgetReducer.handleLearningActivitiesWithSectionsFetchSuccess
*/
internal fun StudyPlanWidgetFeature.State.getCurrentSection(): StudyPlanSection? =
/**
* [StudyPlanWidgetFeature.State.studyPlanSections] map preserves the entry iteration order,
* so we can use the first element as the current section.
*/
studyPlanSections.values.firstOrNull()?.studyPlanSection

/**
Expand All @@ -37,19 +39,45 @@ internal fun StudyPlanWidgetFeature.State.getCurrentActivity(): LearningActivity

/**
* @param sectionId target section id.
* @return list of [LearningActivity] for the given section with [sectionId].
* @return a sequence of [LearningActivity] for the given section with [sectionId]
* filtered by availability in [StudyPlanWidgetFeature.State.activities]
*/
internal fun StudyPlanWidgetFeature.State.getSectionActivities(sectionId: Long): List<LearningActivity> =
internal fun StudyPlanWidgetFeature.State.getSectionActivities(sectionId: Long): Sequence<LearningActivity> =
studyPlanSections[sectionId]
?.studyPlanSection
?.activities
?.mapNotNull { id -> activities[id] } ?: emptyList()
?.asSequence()
?.mapNotNull { id -> activities[id] }
?: emptySequence()

/**
* @param sectionId target section id.
* @param activityId target activity id in the section with id = [sectionId]
*
* @return true in case of MobileContentTrial subscription and
* this activity number is bigger then the free topics count.
* Otherwise returns false (activity with [activityId] is unlocked).
*/
internal fun StudyPlanWidgetFeature.State.isActivityLocked(
sectionId: Long,
activityId: Long,
): Boolean {
val unlockedActivitiesCount = getUnlockedActivitiesCount(sectionId)
return unlockedActivitiesCount != null &&
getSectionActivities(sectionId)
.take(unlockedActivitiesCount)
.map { it.id }
.contains(activityId)
.not()
}

/**
* Returns unlocked activities ids list in case of MobileContentTrial subscription.
* @param sectionId target sectionId.
*
* @return unlocked activities count for [sectionId] in case of MobileContentTrial subscription.
* Otherwise returns null (all the activities are unlocked).
*/
internal fun StudyPlanWidgetFeature.State.getUnlockedActivitiesIds(sectionId: Long): List<Long>? {
internal fun StudyPlanWidgetFeature.State.getUnlockedActivitiesCount(sectionId: Long): Int? {
val section = studyPlanSections[sectionId]?.studyPlanSection ?: return null
val isRootTopicsSection = section.type == StudyPlanSectionType.ROOT_TOPICS
val isTopicsLimitEnabled =
Expand All @@ -59,12 +87,18 @@ internal fun StudyPlanWidgetFeature.State.getUnlockedActivitiesIds(sectionId: Lo
) == SubscriptionLimitType.TOPICS
val unlockedActivitiesCount = profile?.feautureValues?.mobileContentTrialFreeTopics?.minus(learnedTopicsCount)
return if (isRootTopicsSection && isTopicsLimitEnabled && unlockedActivitiesCount != null) {
section.activities.take(unlockedActivitiesCount)
unlockedActivitiesCount
} else {
null
}
}

/**
* @return true in case of MobileContentTrial subscription,
* only on root topics section and
* all the free topics are solved.
* Otherwise returns false.
*/
internal fun StudyPlanWidgetFeature.State.isPaywallShown(): Boolean {
val rootTopicsSections =
studyPlanSections
Expand All @@ -75,8 +109,8 @@ internal fun StudyPlanWidgetFeature.State.isPaywallShown(): Boolean {
val hasOnlyOneRootTopicSection = rootTopicsSections.count() == 1
return if (hasOnlyOneRootTopicSection) {
val rootTopicsSectionId = rootTopicsSections.first().studyPlanSection.id
val unlockedActivitiesIds = getUnlockedActivitiesIds(rootTopicsSectionId)
unlockedActivitiesIds?.isEmpty() == true
val unlockedActivitiesCount = getUnlockedActivitiesCount(rootTopicsSectionId)
unlockedActivitiesCount == 0
} else {
false
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -368,7 +368,7 @@ class StudyPlanWidgetReducer : StateReducer<State, Message, Action> {
private fun handleActivityClicked(state: State, message: Message.ActivityClicked): StudyPlanWidgetReducerResult {
val activity = state.activities[message.activityId] ?: return state to emptySet()

val isActivityLocked = isActivityLocked(state, message.activityId, message.sectionId)
val isActivityLocked = state.isActivityLocked(message.sectionId, message.activityId)

val logAnalyticEventAction = InternalAction.LogAnalyticEvent(
StudyPlanClickedActivityHyperskillAnalyticEvent(
Expand Down Expand Up @@ -399,11 +399,6 @@ class StudyPlanWidgetReducer : StateReducer<State, Message, Action> {
return state to setOf(activityTargetAction, logAnalyticEventAction)
}

private fun isActivityLocked(state: State, activityId: Long, sectionId: Long): Boolean {
val unlockedActivitiesIds = state.getUnlockedActivitiesIds(sectionId)
return unlockedActivitiesIds != null && activityId !in unlockedActivitiesIds
}

private fun handleSubscribeClicked(state: State): StudyPlanWidgetReducerResult =
if (state.isPaywallShown()) {
state to setOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature
import org.hyperskill.app.study_plan.widget.presentation.getCurrentActivity
import org.hyperskill.app.study_plan.widget.presentation.getCurrentSection
import org.hyperskill.app.study_plan.widget.presentation.getSectionActivities
import org.hyperskill.app.study_plan.widget.presentation.getUnlockedActivitiesIds
import org.hyperskill.app.study_plan.widget.presentation.getUnlockedActivitiesCount
import org.hyperskill.app.study_plan.widget.presentation.isPaywallShown
import org.hyperskill.app.study_plan.widget.view.model.StudyPlanWidgetViewState
import org.hyperskill.app.study_plan.widget.view.model.StudyPlanWidgetViewState.SectionContent
Expand Down Expand Up @@ -73,44 +73,54 @@ class StudyPlanWidgetViewStateMapper(private val dateFormatter: SharedDateFormat
if (sectionInfo.isExpanded) {
when (sectionInfo.contentStatus) {
StudyPlanWidgetFeature.ContentStatus.IDLE -> SectionContent.Collapsed
StudyPlanWidgetFeature.ContentStatus.ERROR -> SectionContent.Error
StudyPlanWidgetFeature.ContentStatus.LOADING -> {
val activities = state.getSectionActivities(sectionInfo.studyPlanSection.id)
if (activities.isEmpty()) {
SectionContent.Loading
} else {
getContent(
activities = activities,
currentActivityId = currentActivityId,
unlockedActivitiesIds = state.getUnlockedActivitiesIds(sectionInfo.studyPlanSection.id)
)
}
getContent(
state = state,
sectionInfo = sectionInfo,
currentActivityId = currentActivityId,
emptyActivitiesState = SectionContent.Loading
)
}
StudyPlanWidgetFeature.ContentStatus.ERROR -> SectionContent.Error
StudyPlanWidgetFeature.ContentStatus.LOADED -> {
val activities = state.getSectionActivities(sectionInfo.studyPlanSection.id)
if (activities.isNotEmpty()) {
getContent(
activities = activities,
currentActivityId = currentActivityId,
unlockedActivitiesIds = state.getUnlockedActivitiesIds(sectionInfo.studyPlanSection.id)
)
} else {
SectionContent.Error
}
getContent(
state = state,
sectionInfo = sectionInfo,
currentActivityId = currentActivityId,
emptyActivitiesState = SectionContent.Error
)
}
}
} else {
SectionContent.Collapsed
}

private fun getContent(
state: StudyPlanWidgetFeature.State,
sectionInfo: StudyPlanWidgetFeature.StudyPlanSectionInfo,
currentActivityId: Long?,
emptyActivitiesState: SectionContent
): SectionContent {
val activities = state.getSectionActivities(sectionInfo.studyPlanSection.id).toList()
return if (activities.isEmpty()) {
emptyActivitiesState
} else {
getContent(
activities = activities,
currentActivityId = currentActivityId,
unlockedActivitiesCount = state.getUnlockedActivitiesCount(sectionInfo.studyPlanSection.id)
)
}
}

private fun getContent(
activities: List<LearningActivity>,
currentActivityId: Long?,
unlockedActivitiesIds: List<Long>?
unlockedActivitiesCount: Int?
): SectionContent.Content =
SectionContent.Content(
sectionItems = activities.map { activity ->
val isLocked = unlockedActivitiesIds != null && activity.id !in unlockedActivitiesIds
sectionItems = activities.mapIndexed { index, activity ->
val isLocked = unlockedActivitiesCount != null && index + 1 > unlockedActivitiesCount
StudyPlanWidgetViewState.SectionItem(
id = activity.id,
title = LearningActivityTextsMapper.mapLearningActivityToTitle(activity),
Expand Down

0 comments on commit 3e6cd2f

Please sign in to comment.