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

ALTAPPS-1303: Fix limits are visible in GamificationToolbar after step solving #1114

Merged
merged 7 commits into from
Jul 23, 2024
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 @@ -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
Loading