diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt index b8f216b70b..b39afcfc1d 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/home/view/ui/fragment/HomeFragment.kt @@ -207,8 +207,8 @@ class HomeFragment : val homeState = state.homeState if (homeState is HomeFeature.HomeState.Content) { - renderProblemOfDay(viewBinding, homeState.problemOfDayState, homeState.areProblemsLimited) - renderTopicsRepetition(homeState.repetitionsState, homeState.areProblemsLimited) + renderProblemOfDay(viewBinding, homeState.problemOfDayState, homeState.isProblemsLimitEnabled) + renderTopicsRepetition(homeState.repetitionsState, homeState.isProblemsLimitEnabled) } renderChallengeCard(state.challengeWidgetViewState) @@ -242,19 +242,19 @@ class HomeFragment : private fun renderProblemOfDay( viewBinding: FragmentHomeBinding, state: HomeFeature.ProblemOfDayState, - areProblemsLimited: Boolean + isProblemsLimitEnabled: Boolean ) { problemOfDayCardFormDelegate.render( dateFormatter = dateFormatter, binding = viewBinding.homeScreenProblemOfDayCard, state = state, - areProblemsLimited = areProblemsLimited + areProblemsLimited = isProblemsLimitEnabled ) } private fun renderTopicsRepetition( repetitionsState: HomeFeature.RepetitionsState, - areProblemsLimited: Boolean + isProblemsLimitEnabled: Boolean ) { viewBinding.homeScreenTopicsRepetitionCard.root.isVisible = repetitionsState is HomeFeature.RepetitionsState.Available @@ -263,7 +263,7 @@ class HomeFragment : context = requireContext(), binding = viewBinding.homeScreenTopicsRepetitionCard, recommendedRepetitionsCount = repetitionsState.recommendedRepetitionsCount, - areProblemsLimited = areProblemsLimited + isProblemsLimitEnabled = isProblemsLimitEnabled ) } } diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topics_repetitions/view/delegate/TopicsRepetitionCardFormDelegate.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topics_repetitions/view/delegate/TopicsRepetitionCardFormDelegate.kt index 267c2eb413..09a5a82485 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topics_repetitions/view/delegate/TopicsRepetitionCardFormDelegate.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topics_repetitions/view/delegate/TopicsRepetitionCardFormDelegate.kt @@ -10,7 +10,7 @@ class TopicsRepetitionCardFormDelegate { context: Context, binding: LayoutTopicsRepetitionCardBinding, recommendedRepetitionsCount: Int, - areProblemsLimited: Boolean + isProblemsLimitEnabled: Boolean ) { with(binding) { topicsRepetitionBackgroundImageView.setImageResource( @@ -49,7 +49,7 @@ class TopicsRepetitionCardFormDelegate { } ) topicsRepetitionUnlimitedBadge.isVisible = - areProblemsLimited && recommendedRepetitionsCount > 0 + isProblemsLimitEnabled && recommendedRepetitionsCount > 0 } } } \ No newline at end of file diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index b22a201a28..e6d290dfee 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -242,7 +242,6 @@ ReturnCount:SearchReducer.kt$SearchReducer$private fun handleSearchResultsItemClickedMessage( state: State, message: Message.SearchResultsItemClicked ): SearchReducerResult? ReturnCount:SharedDateFormatter.kt$SharedDateFormatter$fun formatTimeDistance(millis: Long): String ReturnCount:StateExtentions.kt$internal fun ChallengeWidgetFeature.State.Content.setCurrentChallengeIntervalProgressAsCompleted(): Challenge? - ReturnCount:StepQuizActionDispatcher.kt$StepQuizActionDispatcher$private suspend fun handleUpdateProblemsLimitAction( action: InternalAction.UpdateProblemsLimit, onNewMessage: (Message) -> Unit ) ReturnCount:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleDeleteButtonClicked( state: State ): StepQuizCodeBlanksReducerResult? ReturnCount:StepQuizCodeBlanksReducer.kt$StepQuizCodeBlanksReducer$private fun handleSuggestionClicked( state: State, message: Message.SuggestionClicked ): StepQuizCodeBlanksReducerResult? ReturnCount:StepQuizHintsInteractor.kt$StepQuizHintsInteractor$suspend fun getLastSeenHint(stepId: Long): Comment? @@ -282,6 +281,7 @@ TooManyFunctions:SharedDateFormatter.kt$SharedDateFormatter TooManyFunctions:StepActionDispatcher.kt$StepActionDispatcher : CoroutineActionDispatcher TooManyFunctions:StudyPlanWidgetDelegate.kt$StudyPlanWidgetDelegate + TooManyFunctions:SubscriptionsInteractor.kt$SubscriptionsInteractor TopLevelPropertyNaming:HyperskillNotificationChannel.kt$private const val dailyReminderId = "dailyReminderChannel" TopLevelPropertyNaming:HyperskillNotificationChannel.kt$private const val otherId = "otherChannel" TopLevelPropertyNaming:HyperskillNotificationChannel.kt$private const val regularLearningRemindersId = "regularLearningRemindersChannel" diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Home/Views/HomeView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Home/Views/HomeView.swift index a628d46288..99edbd1d75 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Home/Views/HomeView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/Home/Views/HomeView.swift @@ -87,7 +87,7 @@ struct HomeView: View { ProblemOfDayAssembly( problemOfDayState: data.problemOfDayState, - isFreemiumEnabled: data.areProblemsLimited, + isFreemiumEnabled: data.isProblemsLimitEnabled, output: viewModel ) .makeModule() @@ -96,7 +96,7 @@ struct HomeView: View { TopicsRepetitionsCardView( topicsToRepeatCount: Int(availableRepetitionsState.recommendedRepetitionsCount), onTap: viewModel.doTopicsRepetitionsPresentation, - isFreemiumEnabled: data.areProblemsLimited + isFreemiumEnabled: data.isProblemsLimitEnabled ) } } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/interactor/AnalyticInteractor.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/interactor/AnalyticInteractor.kt index a5e03338db..89c276a15b 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/interactor/AnalyticInteractor.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/interactor/AnalyticInteractor.kt @@ -19,14 +19,20 @@ import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionSta import ru.nobird.app.core.model.mapOfNotNull class AnalyticInteractor( + // Provide CurrentSubscriptionStateRepository lazily + // because it depends on AnalyticInteractor + currentSubscriptionStateRepositoryProvider: () -> CurrentSubscriptionStateRepository, private val currentProfileStateRepository: CurrentProfileStateRepository, - private val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, private val notificationInteractor: NotificationInteractor, private val platform: Platform, private val analyticEngines: List, override val eventMonitor: AnalyticEventMonitor? ) : Analytic { + private val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository by lazy { + currentSubscriptionStateRepositoryProvider.invoke() + } + private var userProperties: MutableMap = mutableMapOf() override fun reportEvent(event: AnalyticEvent, forceReportEvent: Boolean) { diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/injection/AnalyticComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/injection/AnalyticComponentImpl.kt index ba12b70a8c..e34aec5902 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/injection/AnalyticComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/injection/AnalyticComponentImpl.kt @@ -37,7 +37,9 @@ class AnalyticComponentImpl( AnalyticInteractor( analyticEngines = analyticEngines, currentProfileStateRepository = appGraph.profileDataComponent.currentProfileStateRepository, - currentSubscriptionStateRepository = appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository, + currentSubscriptionStateRepositoryProvider = { + appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository + }, notificationInteractor = appGraph.buildNotificationComponent().notificationInteractor, eventMonitor = BatchAnalyticEventMonitor(listOfNotNull(sentryInteractor, loggableAnalyticEventMonitor)), platform = appGraph.commonComponent.platform diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/data/repository/BaseStateRepository.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/data/repository/BaseStateRepository.kt index e19db08830..12c29e8cfc 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/data/repository/BaseStateRepository.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/data/repository/BaseStateRepository.kt @@ -4,8 +4,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import org.hyperskill.app.core.domain.DataSourceType @@ -43,7 +43,7 @@ abstract class BaseStateRepository : StateRepository { * * @return shared flow */ - override val changes: SharedFlow + override val changes: Flow get() = mutableSharedFlow /** diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/StateRepository.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/StateRepository.kt index 50c4e1ee4f..b91564b2f5 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/StateRepository.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/StateRepository.kt @@ -1,6 +1,6 @@ package org.hyperskill.app.core.domain.repository -import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.first interface StateRepository { @@ -26,7 +26,7 @@ interface StateRepository { * * @return shared flow */ - val changes: SharedFlow + val changes: Flow /** * Update state locally in app. diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponentImpl.kt index f5293cc2c1..69f0ee84cb 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponentImpl.kt @@ -6,6 +6,7 @@ import org.hyperskill.app.gamification_toolbar.remote.GamificationToolbarRemoteD import org.hyperskill.app.learning_activities.data.repository.NextLearningActivityStateRepositoryImpl import org.hyperskill.app.learning_activities.domain.repository.NextLearningActivityStateRepository import org.hyperskill.app.learning_activities.remote.LearningActivitiesRemoteDataSourceImpl +import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.study_plan.data.repository.CurrentStudyPlanStateRepositoryImpl import org.hyperskill.app.study_plan.domain.repository.CurrentStudyPlanStateRepository import org.hyperskill.app.study_plan.remote.StudyPlanRemoteDataSourceImpl @@ -20,6 +21,10 @@ class StateRepositoriesComponentImpl(appGraph: AppGraph) : StateRepositoriesComp private val authorizedHttpClient = appGraph.networkComponent.authorizedHttpClient + private val featuresDataSource = appGraph.profileDataComponent.featuresDataSource + + private val purchaseInteractor: PurchaseInteractor = appGraph.buildPurchaseComponent().purchaseInteractor + /** * Current subscription */ @@ -34,7 +39,12 @@ class StateRepositoriesComponentImpl(appGraph: AppGraph) : StateRepositoriesComp } override val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository = - CurrentSubscriptionStateRepositoryImpl(subscriptionsRemoteDataSource, currentSubscriptionStateHolder) + CurrentSubscriptionStateRepositoryImpl( + subscriptionsRemoteDataSource = subscriptionsRemoteDataSource, + stateHolder = currentSubscriptionStateHolder, + featuresDataSource = featuresDataSource, + purchaseInteractor = purchaseInteractor + ) /** * Study plan diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/features/data/source/FeaturesDataSource.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/features/data/source/FeaturesDataSource.kt new file mode 100644 index 0000000000..b0317c7436 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/features/data/source/FeaturesDataSource.kt @@ -0,0 +1,9 @@ +package org.hyperskill.app.features.data.source + +import org.hyperskill.app.profile.domain.model.FeatureValues +import org.hyperskill.app.profile.domain.model.FeaturesMap + +interface FeaturesDataSource { + fun getFeaturesMap(): FeaturesMap + fun getFeatureValues(): FeatureValues +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/injection/GamificationToolbarComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/injection/GamificationToolbarComponentImpl.kt index 437511325e..395b95d6e3 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/injection/GamificationToolbarComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/injection/GamificationToolbarComponentImpl.kt @@ -24,9 +24,8 @@ internal class GamificationToolbarComponentImpl( topicCompletedFlow = appGraph.stepCompletionFlowDataComponent.topicCompletedFlow, currentGamificationToolbarDataStateRepository = appGraph.stateRepositoriesComponent .currentGamificationToolbarDataStateRepository, - currentSubscriptionStateRepository = appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository, + subscriptionInteractor = appGraph.subscriptionDataComponent.subscriptionsInteractor, currentProfileStateRepository = appGraph.profileDataComponent.currentProfileStateRepository, - purchaseInteractor = appGraph.buildPurchaseComponent().purchaseInteractor, sentryInteractor = appGraph.sentryComponent.sentryInteractor ) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarFeature.kt index f417290476..33ef64e81b 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarFeature.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarFeature.kt @@ -10,6 +10,8 @@ import org.hyperskill.app.streaks.domain.model.Streak import org.hyperskill.app.study_plan.domain.model.StudyPlan import org.hyperskill.app.subscriptions.domain.model.FreemiumChargeLimitsStrategy import org.hyperskill.app.subscriptions.domain.model.Subscription +import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType +import org.hyperskill.app.subscriptions.domain.model.SubscriptionWithLimitType object GamificationToolbarFeature { sealed interface State { @@ -22,8 +24,7 @@ object GamificationToolbarFeature { val historicalStreak: HistoricalStreak, val subscription: Subscription, val chargeLimitsStrategy: FreemiumChargeLimitsStrategy, - internal val isMobileContentTrialEnabled: Boolean, - internal val canMakePayments: Boolean = false, + internal val subscriptionLimitType: SubscriptionLimitType, internal val isRefreshing: Boolean = false ) : State } @@ -72,9 +73,8 @@ object GamificationToolbarFeature { data class FetchGamificationToolbarDataSuccess( val gamificationToolbarData: GamificationToolbarData, val subscription: Subscription, - val chargeLimitsStrategy: FreemiumChargeLimitsStrategy, - val isMobileContentTrialEnabled: Boolean, - val canMakePayments: Boolean + val subscriptionLimitType: SubscriptionLimitType, + val chargeLimitsStrategy: FreemiumChargeLimitsStrategy ) : InternalMessage object PullToRefresh : InternalMessage @@ -89,7 +89,9 @@ object GamificationToolbarFeature { data class GamificationToolbarDataChanged( val gamificationToolbarData: GamificationToolbarData ) : InternalMessage - data class SubscriptionChanged(val subscription: Subscription) : InternalMessage + data class SubscriptionChanged( + val subscriptionWithLimitType: SubscriptionWithLimitType + ) : InternalMessage } sealed interface Action { diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarReducer.kt index 9aaa4ef373..94e203ab28 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/GamificationToolbarReducer.kt @@ -16,6 +16,7 @@ import org.hyperskill.app.streaks.domain.model.HistoricalStreak import org.hyperskill.app.streaks.domain.model.StreakState import org.hyperskill.app.subscriptions.domain.model.FreemiumChargeLimitsStrategy import org.hyperskill.app.subscriptions.domain.model.Subscription +import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType import ru.nobird.app.presentation.redux.reducer.StateReducer private typealias GamificationToolbarReducerResult = Pair> @@ -33,9 +34,8 @@ class GamificationToolbarReducer( createContentState( gamificationToolbarData = message.gamificationToolbarData, subscription = message.subscription, - chargeLimitsStrategy = message.chargeLimitsStrategy, - isMobileContentTrialEnabled = message.isMobileContentTrialEnabled, - canMakePayments = message.canMakePayments + subscriptionLimitType = message.subscriptionLimitType, + chargeLimitsStrategy = message.chargeLimitsStrategy ) to emptySet() is InternalMessage.PullToRefresh -> handlePullToRefreshMessage(state) @@ -99,7 +99,10 @@ class GamificationToolbarReducer( if (state.isRefreshing) { state to emptySet() } else { - state.copy(subscription = message.subscription) to emptySet() + state.copy( + subscription = message.subscriptionWithLimitType.subscription, + subscriptionLimitType = message.subscriptionWithLimitType.subscriptionLimitType + ) to emptySet() } else -> state to emptySet() } @@ -226,9 +229,8 @@ class GamificationToolbarReducer( private fun createContentState( gamificationToolbarData: GamificationToolbarData, subscription: Subscription, - chargeLimitsStrategy: FreemiumChargeLimitsStrategy, - isMobileContentTrialEnabled: Boolean, - canMakePayments: Boolean + subscriptionLimitType: SubscriptionLimitType, + chargeLimitsStrategy: FreemiumChargeLimitsStrategy ): State.Content = State.Content( trackProgress = gamificationToolbarData.trackProgress, @@ -236,7 +238,6 @@ class GamificationToolbarReducer( historicalStreak = HistoricalStreak(gamificationToolbarData.streakState), subscription = subscription, chargeLimitsStrategy = chargeLimitsStrategy, - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments + subscriptionLimitType = subscriptionLimitType ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/MainGamificationToolbarActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/MainGamificationToolbarActionDispatcher.kt index 3ac1e38eb0..255adbe5e2 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/MainGamificationToolbarActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/presentation/MainGamificationToolbarActionDispatcher.kt @@ -4,7 +4,6 @@ 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 @@ -14,18 +13,15 @@ import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarF import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.InternalMessage import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarFeature.Message import org.hyperskill.app.profile.domain.model.freemiumChargeLimitsStrategy -import org.hyperskill.app.profile.domain.model.isMobileContentTrialEnabled import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository -import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.sentry.domain.withTransaction import org.hyperskill.app.step_completion.domain.flow.StepCompletedFlow import org.hyperskill.app.step_completion.domain.flow.TopicCompletedFlow import org.hyperskill.app.streaks.domain.flow.StreakFlow import org.hyperskill.app.study_plan.domain.repository.CurrentStudyPlanStateRepository -import org.hyperskill.app.subscriptions.domain.model.Subscription -import org.hyperskill.app.subscriptions.domain.model.orContentTrial -import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository +import org.hyperskill.app.subscriptions.domain.interactor.SubscriptionsInteractor +import org.hyperskill.app.subscriptions.domain.model.SubscriptionWithLimitType import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher internal class MainGamificationToolbarActionDispatcher( @@ -35,14 +31,11 @@ internal class MainGamificationToolbarActionDispatcher( currentStudyPlanStateRepository: CurrentStudyPlanStateRepository, topicCompletedFlow: TopicCompletedFlow, private val currentGamificationToolbarDataStateRepository: CurrentGamificationToolbarDataStateRepository, - private val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, private val currentProfileStateRepository: CurrentProfileStateRepository, - private val purchaseInteractor: PurchaseInteractor, + private val subscriptionInteractor: SubscriptionsInteractor, private val sentryInteractor: SentryInteractor ) : CoroutineActionDispatcher(config.createConfig()) { - private var isMobileContentTrialEnabled: Boolean = false - init { stepCompletedFlow.observe() .onEach { onNewMessage(InternalMessage.StepSolved) } @@ -74,13 +67,8 @@ internal class MainGamificationToolbarActionDispatcher( .onEach { onNewMessage(InternalMessage.GamificationToolbarDataChanged(it)) } .launchIn(actionScope) - currentSubscriptionStateRepository.changes - .map { - it.orContentTrial( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments() - ) - } + subscriptionInteractor + .subscribeOnSubscriptionWithLimitType() .distinctUntilChanged() .onEach { onNewMessage(InternalMessage.SubscriptionChanged(it)) } .launchIn(actionScope) @@ -117,46 +105,28 @@ internal class MainGamificationToolbarActionDispatcher( val gamificationToolbarDataWithSource = toolbarDataDeferred.await().getOrThrow() val profile = profileDeferred.await().getOrThrow() - this@MainGamificationToolbarActionDispatcher.isMobileContentTrialEnabled = - profile.features.isMobileContentTrialEnabled - - val canMakePayments = canMakePayments() - - val subscription = getSubscription( - isMobileContentTrialEnabled = profile.features.isMobileContentTrialEnabled, - canMakePayments = canMakePayments, + val subscriptionWithLimitType = getSubscription( forceUpdate = action.forceUpdate, gamificationToolbarDataSourceType = gamificationToolbarDataWithSource.usedDataSourceType ) InternalMessage.FetchGamificationToolbarDataSuccess( gamificationToolbarData = gamificationToolbarDataWithSource.state, - subscription = subscription, + subscription = subscriptionWithLimitType.subscription, + subscriptionLimitType = subscriptionWithLimitType.subscriptionLimitType, chargeLimitsStrategy = profile.freemiumChargeLimitsStrategy, - isMobileContentTrialEnabled = profile.features.isMobileContentTrialEnabled, - canMakePayments = canMakePayments ) } }.let(onNewMessage) } private suspend fun getSubscription( - isMobileContentTrialEnabled: Boolean, - canMakePayments: Boolean, forceUpdate: Boolean, gamificationToolbarDataSourceType: DataSourceType - ): Subscription { + ): SubscriptionWithLimitType { val subscriptionWithSource = - currentSubscriptionStateRepository - .getStateWithSource(forceUpdate = forceUpdate) - .map { - it.copy( - state = it.state.orContentTrial( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) - ) - } + subscriptionInteractor + .getSubscriptionWithLimitTypeWithSource(forceUpdate = forceUpdate) .getOrThrow() // Fetch subscription from remote @@ -168,20 +138,11 @@ internal class MainGamificationToolbarActionDispatcher( subscriptionWithSource.usedDataSourceType == DataSourceType.CACHE return if (shouldFetchSubscriptionFromRemote) { - currentSubscriptionStateRepository - .getState(forceUpdate = true) - .map { subscription -> - subscription.orContentTrial( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) - } + subscriptionInteractor + .getSubscriptionWithLimitType(forceUpdate = true) .getOrThrow() } else { subscriptionWithSource.state } } - - private suspend fun canMakePayments(): Boolean = - purchaseInteractor.canMakePayments().getOrDefault(false) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/view/mapper/GamificationToolbarViewStateMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/view/mapper/GamificationToolbarViewStateMapper.kt index fa6c327d5d..f81d53d4bc 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/view/mapper/GamificationToolbarViewStateMapper.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/gamification_toolbar/view/mapper/GamificationToolbarViewStateMapper.kt @@ -5,7 +5,6 @@ import org.hyperskill.app.gamification_toolbar.presentation.GamificationToolbarF import org.hyperskill.app.streaks.domain.model.StreakState import org.hyperskill.app.subscriptions.domain.model.Subscription import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType -import org.hyperskill.app.subscriptions.domain.model.getSubscriptionLimitType internal object GamificationToolbarViewStateMapper { fun map(state: GamificationToolbarFeature.State): GamificationToolbarFeature.ViewState = @@ -28,8 +27,7 @@ internal object GamificationToolbarViewStateMapper { ), problemsLimit = getProblemsLimitState( subscription = state.subscription, - isMobileContentTrialEnabled = state.isMobileContentTrialEnabled, - canMakePayments = state.canMakePayments + subscriptionLimitType = state.subscriptionLimitType ) ) @@ -48,14 +46,9 @@ internal object GamificationToolbarViewStateMapper { private fun getProblemsLimitState( subscription: Subscription, - isMobileContentTrialEnabled: Boolean, - canMakePayments: Boolean + subscriptionLimitType: SubscriptionLimitType ): GamificationToolbarFeature.ViewState.Content.ProblemsLimit? { val stepsLimitLeft = subscription.stepsLimitLeft - val subscriptionLimitType = subscription.getSubscriptionLimitType( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) return if (subscriptionLimitType == SubscriptionLimitType.PROBLEMS && stepsLimitLeft != null) { GamificationToolbarFeature.ViewState.Content.ProblemsLimit(limitLabel = stepsLimitLeft.toString()) } else { diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/home/injection/HomeComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/home/injection/HomeComponentImpl.kt index f13cba0810..a58c6ed3df 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/home/injection/HomeComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/home/injection/HomeComponentImpl.kt @@ -19,14 +19,13 @@ internal class HomeComponentImpl(private val appGraph: AppGraph) : HomeComponent currentProfileStateRepository = appGraph.profileDataComponent.currentProfileStateRepository, topicsRepetitionsInteractor = appGraph.buildTopicsRepetitionsDataComponent().topicsRepetitionsInteractor, stepInteractor = appGraph.buildStepDataComponent().stepInteractor, - currentSubscriptionStateRepository = appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository, + subscriptionsInteractor = appGraph.subscriptionDataComponent.subscriptionsInteractor, analyticInteractor = appGraph.analyticComponent.analyticInteractor, sentryInteractor = appGraph.sentryComponent.sentryInteractor, dateFormatter = appGraph.commonComponent.dateFormatter, topicRepeatedFlow = appGraph.topicsRepetitionsFlowDataComponent.topicRepeatedFlow, topicCompletedFlow = appGraph.stepCompletionFlowDataComponent.topicCompletedFlow, stepCompletedFlow = appGraph.stepCompletionFlowDataComponent.stepCompletedFlow, - purchaseInteractor = appGraph.buildPurchaseComponent().purchaseInteractor, gamificationToolbarReducer = gamificationToolbarComponent.gamificationToolbarReducer, gamificationToolbarActionDispatcher = gamificationToolbarComponent.gamificationToolbarActionDispatcher, challengeWidgetReducer = challengeWidgetComponent.challengeWidgetReducer, diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/home/injection/HomeFeatureBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/home/injection/HomeFeatureBuilder.kt index 92dce97f53..53d4069ea4 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/home/injection/HomeFeatureBuilder.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/home/injection/HomeFeatureBuilder.kt @@ -20,12 +20,11 @@ import org.hyperskill.app.home.presentation.HomeReducer import org.hyperskill.app.home.view.mapper.HomeViewStateMapper import org.hyperskill.app.logging.presentation.wrapWithLogger import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository -import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.step.domain.interactor.StepInteractor import org.hyperskill.app.step_completion.domain.flow.StepCompletedFlow import org.hyperskill.app.step_completion.domain.flow.TopicCompletedFlow -import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository +import org.hyperskill.app.subscriptions.domain.interactor.SubscriptionsInteractor import org.hyperskill.app.topics_repetitions.domain.flow.TopicRepeatedFlow import org.hyperskill.app.topics_repetitions.domain.interactor.TopicsRepetitionsInteractor import ru.nobird.app.core.model.safeCast @@ -41,14 +40,13 @@ internal object HomeFeatureBuilder { currentProfileStateRepository: CurrentProfileStateRepository, topicsRepetitionsInteractor: TopicsRepetitionsInteractor, stepInteractor: StepInteractor, - currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, + subscriptionsInteractor: SubscriptionsInteractor, analyticInteractor: AnalyticInteractor, sentryInteractor: SentryInteractor, dateFormatter: SharedDateFormatter, topicRepeatedFlow: TopicRepeatedFlow, topicCompletedFlow: TopicCompletedFlow, stepCompletedFlow: StepCompletedFlow, - purchaseInteractor: PurchaseInteractor, gamificationToolbarReducer: GamificationToolbarReducer, gamificationToolbarActionDispatcher: GamificationToolbarActionDispatcher, challengeWidgetReducer: ChallengeWidgetReducer, @@ -66,13 +64,12 @@ internal object HomeFeatureBuilder { currentProfileStateRepository = currentProfileStateRepository, topicsRepetitionsInteractor = topicsRepetitionsInteractor, stepInteractor = stepInteractor, - currentSubscriptionStateRepository = currentSubscriptionStateRepository, sentryInteractor = sentryInteractor, - purchaseInteractor = purchaseInteractor, dateFormatter = dateFormatter, topicRepeatedFlow = topicRepeatedFlow, topicCompletedFlow = topicCompletedFlow, - stepCompletedFlow = stepCompletedFlow + stepCompletedFlow = stepCompletedFlow, + subscriptionInteractor = subscriptionsInteractor ) val homeViewStateMapper = HomeViewStateMapper( challengeWidgetViewStateMapper = challengeWidgetViewStateMapper diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt index 18968819a5..e1a5a2bcaa 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeActionDispatcher.kt @@ -16,17 +16,14 @@ import org.hyperskill.app.home.presentation.HomeFeature.Action import org.hyperskill.app.home.presentation.HomeFeature.InternalAction import org.hyperskill.app.home.presentation.HomeFeature.InternalMessage import org.hyperskill.app.home.presentation.HomeFeature.Message -import org.hyperskill.app.profile.domain.model.isMobileContentTrialEnabled import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository -import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder import org.hyperskill.app.sentry.domain.withTransaction import org.hyperskill.app.step.domain.interactor.StepInteractor import org.hyperskill.app.step_completion.domain.flow.StepCompletedFlow import org.hyperskill.app.step_completion.domain.flow.TopicCompletedFlow -import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository -import org.hyperskill.app.subscriptions.domain.repository.areProblemsLimited +import org.hyperskill.app.subscriptions.domain.interactor.SubscriptionsInteractor import org.hyperskill.app.topics_repetitions.domain.flow.TopicRepeatedFlow import org.hyperskill.app.topics_repetitions.domain.interactor.TopicsRepetitionsInteractor import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher @@ -36,9 +33,8 @@ internal class HomeActionDispatcher( private val currentProfileStateRepository: CurrentProfileStateRepository, private val topicsRepetitionsInteractor: TopicsRepetitionsInteractor, private val stepInteractor: StepInteractor, - private val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, + private val subscriptionInteractor: SubscriptionsInteractor, private val sentryInteractor: SentryInteractor, - private val purchaseInteractor: PurchaseInteractor, private val dateFormatter: SharedDateFormatter, topicRepeatedFlow: TopicRepeatedFlow, topicCompletedFlow: TopicCompletedFlow, @@ -115,18 +111,15 @@ internal class HomeActionDispatcher( val problemOfDayStateResult = async { getProblemOfDayState(currentProfile.dailyStep) } val repetitionsStateResult = async { getRepetitionsState() } - val areProblemsLimited = async { - currentSubscriptionStateRepository.areProblemsLimited( - isMobileContentTrialEnabled = currentProfile.features.isMobileContentTrialEnabled, - canMakePayments = purchaseInteractor.canMakePayments().getOrDefault(false) - ) + val isProblemsLimitEnabled = async { + subscriptionInteractor.isProblemsLimitEnabled() } setOf( Message.HomeSuccess( problemOfDayState = problemOfDayStateResult.await().getOrThrow(), repetitionsState = repetitionsStateResult.await().getOrThrow(), - areProblemsLimited = areProblemsLimited.await() + isProblemsLimitEnabled = isProblemsLimitEnabled.await() ), Message.ReadyToLaunchNextProblemInTimer ) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeFeature.kt index ee33d6bb0a..80598355cc 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeFeature.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeFeature.kt @@ -45,7 +45,7 @@ object HomeFeature { * * @property problemOfDayState Problem of the day state. * @property repetitionsState Topics repetitions state. - * @property areProblemsLimited A boolean flag that indicates that problem limits are enabled. + * @property isProblemsLimitEnabled A boolean flag that indicates that problem limits are enabled. * @property isRefreshing A boolean flag that indicates about is pull-to-refresh is ongoing. * * @see Streak @@ -54,7 +54,7 @@ object HomeFeature { data class Content( val problemOfDayState: ProblemOfDayState, val repetitionsState: RepetitionsState, - val areProblemsLimited: Boolean, + val isProblemsLimitEnabled: Boolean, internal val isRefreshing: Boolean = false ) : HomeState @@ -105,7 +105,7 @@ object HomeFeature { data class HomeSuccess( val problemOfDayState: ProblemOfDayState, val repetitionsState: RepetitionsState, - val areProblemsLimited: Boolean + val isProblemsLimitEnabled: Boolean ) : Message object HomeFailure : Message diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeReducer.kt index 2b811c148c..be19adf5d7 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/home/presentation/HomeReducer.kt @@ -34,7 +34,7 @@ internal class HomeReducer( homeState = HomeState.Content( problemOfDayState = message.problemOfDayState, repetitionsState = message.repetitionsState, - areProblemsLimited = message.areProblemsLimited + isProblemsLimitEnabled = message.isProblemsLimitEnabled ) ) to emptySet() } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppActionDispatcher.kt index eaca07e549..96eac4f53d 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppActionDispatcher.kt @@ -5,7 +5,6 @@ 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.auth.domain.interactor.AuthInteractor import org.hyperskill.app.auth.domain.model.UserDeauthorized @@ -19,7 +18,6 @@ import org.hyperskill.app.main.presentation.AppFeature.InternalMessage import org.hyperskill.app.main.presentation.AppFeature.Message import org.hyperskill.app.notification.local.domain.interactor.NotificationInteractor import org.hyperskill.app.notification.remote.domain.interactor.PushNotificationsInteractor -import org.hyperskill.app.profile.domain.model.isMobileContentTrialEnabled import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.sentry.domain.model.breadcrumb.HyperskillSentryBreadcrumbBuilder @@ -30,7 +28,6 @@ import org.hyperskill.app.subscriptions.domain.model.Subscription import org.hyperskill.app.subscriptions.domain.model.SubscriptionType import org.hyperskill.app.subscriptions.domain.model.isExpired import org.hyperskill.app.subscriptions.domain.model.isValidTillPassed -import org.hyperskill.app.subscriptions.domain.model.orContentTrial import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher @@ -48,8 +45,6 @@ internal class AppActionDispatcher( private val logger: Logger ) : CoroutineActionDispatcher(config.createConfig()) { - private var isMobileContentTrialEnabled: Boolean = false - init { authInteractor .observeUserDeauthorization() @@ -73,12 +68,6 @@ internal class AppActionDispatcher( currentSubscriptionStateRepository .changes - .map { - it.orContentTrial( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments() - ) - } .distinctUntilChanged() .onEach { subscription -> onNewMessage(InternalMessage.SubscriptionChanged(subscription)) @@ -138,8 +127,6 @@ internal class AppActionDispatcher( val profile = profileDeferred.await().getOrThrow() - this@AppActionDispatcher.isMobileContentTrialEnabled = profile.features.isMobileContentTrialEnabled - val subscription = subscriptionDeferred.await() val canMakePayments = if (isAuthorized) { diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/profile/cache/CurrentProfileStateHolderImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/profile/cache/CurrentProfileStateHolderImpl.kt index 8bb316fd52..866e15cc0e 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/profile/cache/CurrentProfileStateHolderImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/profile/cache/CurrentProfileStateHolderImpl.kt @@ -2,17 +2,23 @@ package org.hyperskill.app.profile.cache import com.russhwolf.settings.Settings import kotlinx.serialization.json.Json +import org.hyperskill.app.features.data.source.FeaturesDataSource import org.hyperskill.app.profile.data.source.CurrentProfileStateHolder +import org.hyperskill.app.profile.domain.model.FeatureValues +import org.hyperskill.app.profile.domain.model.FeaturesMap import org.hyperskill.app.profile.domain.model.Profile class CurrentProfileStateHolderImpl( private val json: Json, private val settings: Settings -) : CurrentProfileStateHolder { +) : CurrentProfileStateHolder, FeaturesDataSource { private var cachedProfile: Profile? = null - override suspend fun getState(): Profile? { + override suspend fun getState(): Profile? = + getStateInternal() + + private fun getStateInternal(): Profile? { if (cachedProfile == null) { cachedProfile = readProfileFromSettings() } @@ -55,4 +61,10 @@ class CurrentProfileStateHolderImpl( settings.remove(ProfileCacheKeyValues.GUEST_PROFILE) settings.remove(ProfileCacheKeyValues.CURRENT_PROFILE) } + + override fun getFeaturesMap(): FeaturesMap = + getStateInternal()?.features ?: FeaturesMap(emptyMap()) + + override fun getFeatureValues(): FeatureValues = + getStateInternal()?.featureValues ?: FeatureValues() } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/profile/domain/model/FeaturesMap.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/profile/domain/model/FeaturesMap.kt index 11ce0534ea..2009058866 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/profile/domain/model/FeaturesMap.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/profile/domain/model/FeaturesMap.kt @@ -1,5 +1,7 @@ package org.hyperskill.app.profile.domain.model +import org.hyperskill.app.subscriptions.domain.model.FreemiumChargeLimitsStrategy + data class FeaturesMap(internal val origin: Map) : Map by origin val FeaturesMap.isRecommendationsJavaProjectsFeatureEnabled: Boolean @@ -17,6 +19,13 @@ val FeaturesMap.isFreemiumIncreaseLimitsForFirstStepCompletionEnabled: Boolean val FeaturesMap.isFreemiumWrongSubmissionChargeLimitsEnabled: Boolean get() = get(FeatureKeys.FREEMIUM_WRONG_SUBMISSION_CHARGE_LIMITS) ?: false +internal val FeaturesMap.freemiumChargeLimitsStrategy: FreemiumChargeLimitsStrategy + get() = if (isFreemiumWrongSubmissionChargeLimitsEnabled) { + FreemiumChargeLimitsStrategy.AFTER_WRONG_SUBMISSION + } else { + FreemiumChargeLimitsStrategy.AFTER_CORRECT_SUBMISSION + } + val FeaturesMap.isLearningPathDividedTrackTopicsEnabled: Boolean get() = get(FeatureKeys.LEARNING_PATH_DIVIDED_TRACK_TOPICS) ?: false diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/profile/injection/ProfileDataComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/profile/injection/ProfileDataComponent.kt index 2a81198105..f3ffae23ea 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/profile/injection/ProfileDataComponent.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/profile/injection/ProfileDataComponent.kt @@ -1,9 +1,11 @@ package org.hyperskill.app.profile.injection +import org.hyperskill.app.features.data.source.FeaturesDataSource import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository import org.hyperskill.app.profile.domain.repository.ProfileRepository interface ProfileDataComponent { val profileRepository: ProfileRepository val currentProfileStateRepository: CurrentProfileStateRepository + val featuresDataSource: FeaturesDataSource } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/profile/injection/ProfileDataComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/profile/injection/ProfileDataComponentImpl.kt index 965bae52cd..5a9885f9ad 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/profile/injection/ProfileDataComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/profile/injection/ProfileDataComponentImpl.kt @@ -1,11 +1,11 @@ package org.hyperskill.app.profile.injection import org.hyperskill.app.core.injection.CommonComponent +import org.hyperskill.app.features.data.source.FeaturesDataSource import org.hyperskill.app.network.injection.NetworkComponent import org.hyperskill.app.profile.cache.CurrentProfileStateHolderImpl import org.hyperskill.app.profile.data.repository.CurrentProfileStateRepositoryImpl import org.hyperskill.app.profile.data.repository.ProfileRepositoryImpl -import org.hyperskill.app.profile.data.source.CurrentProfileStateHolder import org.hyperskill.app.profile.data.source.ProfileRemoteDataSource import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository import org.hyperskill.app.profile.domain.repository.ProfileRepository @@ -22,7 +22,7 @@ internal class ProfileDataComponentImpl( ) } - private val currentProfileStateHolder: CurrentProfileStateHolder by lazy { + private val currentProfileStateHolder by lazy { CurrentProfileStateHolderImpl( commonComponent.json, commonComponent.settings @@ -38,4 +38,7 @@ internal class ProfileDataComponentImpl( stateHolder = currentProfileStateHolder ) } + + override val featuresDataSource: FeaturesDataSource + get() = currentProfileStateHolder } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/injection/StepCompletionComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/injection/StepCompletionComponentImpl.kt index beb7133046..27ca661827 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/injection/StepCompletionComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/injection/StepCompletionComponentImpl.kt @@ -34,9 +34,7 @@ internal class StepCompletionComponentImpl( .currentGamificationToolbarDataStateRepository, dailyStepCompletedFlow = appGraph.stepCompletionFlowDataComponent.dailyStepCompletedFlow, topicCompletedFlow = appGraph.stepCompletionFlowDataComponent.topicCompletedFlow, - topicProgressFlow = appGraph.progressesFlowDataComponent.topicProgressFlow, - purchaseInteractor = appGraph.buildPurchaseComponent().purchaseInteractor, - currentSubscriptionStateRepository = appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository + topicProgressFlow = appGraph.progressesFlowDataComponent.topicProgressFlow ) override val stepCompletionActionDispatcher: StepCompletionActionDispatcher diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/MainStepCompletionActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/MainStepCompletionActionDispatcher.kt index e2666c99cd..0deb04f3fd 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/MainStepCompletionActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/MainStepCompletionActionDispatcher.kt @@ -10,11 +10,9 @@ import org.hyperskill.app.core.presentation.ActionDispatcherOptions import org.hyperskill.app.core.view.mapper.ResourceProvider import org.hyperskill.app.gamification_toolbar.domain.repository.CurrentGamificationToolbarDataStateRepository import org.hyperskill.app.learning_activities.domain.repository.NextLearningActivityStateRepository -import org.hyperskill.app.profile.domain.model.isMobileContentTrialEnabled import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository import org.hyperskill.app.progresses.domain.flow.TopicProgressFlow import org.hyperskill.app.progresses.domain.interactor.ProgressesInteractor -import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.request_review.domain.interactor.RequestReviewInteractor import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder @@ -37,9 +35,6 @@ import org.hyperskill.app.step_completion.presentation.StepCompletionFeature.Mes import org.hyperskill.app.streaks.domain.model.StreakState import org.hyperskill.app.subscriptions.domain.interactor.SubscriptionsInteractor import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType -import org.hyperskill.app.subscriptions.domain.model.getSubscriptionLimitType -import org.hyperskill.app.subscriptions.domain.model.orContentTrial -import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository import org.hyperskill.app.topics.domain.repository.TopicsRepository import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher @@ -60,9 +55,7 @@ internal class MainStepCompletionActionDispatcher( private val currentGamificationToolbarDataStateRepository: CurrentGamificationToolbarDataStateRepository, private val dailyStepCompletedFlow: DailyStepCompletedFlow, private val topicCompletedFlow: TopicCompletedFlow, - private val topicProgressFlow: TopicProgressFlow, - private val purchaseInteractor: PurchaseInteractor, - private val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository + private val topicProgressFlow: TopicProgressFlow ) : CoroutineActionDispatcher(config.createConfig()) { init { @@ -168,7 +161,6 @@ internal class MainStepCompletionActionDispatcher( val isTopicsLimitReached = isTopicsLimitReached( trackId = trackId, - isMobileContentTrialEnabled = profile.features.isMobileContentTrialEnabled, mobileContentTrialFreeTopics = profile.featureValues.mobileContentTrialFreeTopics ) @@ -197,21 +189,11 @@ internal class MainStepCompletionActionDispatcher( private suspend fun isTopicsLimitReached( trackId: Long, - isMobileContentTrialEnabled: Boolean, mobileContentTrialFreeTopics: Int ): Boolean = coroutineScope { - val canMakePayments = purchaseInteractor.canMakePayments().getOrDefault(false) - - val subscriptionDeferred = async { - currentSubscriptionStateRepository - .getState(forceUpdate = true) - .map { subscription -> - subscription.orContentTrial( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) - } + val subscriptionWithLimitTypeDeferred = async { + subscriptionsInteractor.getSubscriptionLimitType(forceUpdate = true) } val trackProgressDeferred = async { progressesInteractor @@ -221,14 +203,9 @@ internal class MainStepCompletionActionDispatcher( ) } - val subscription = subscriptionDeferred.await().getOrThrow() + val subscriptionLimitType = subscriptionWithLimitTypeDeferred.await().getOrThrow() val trackProgress = requireNotNull(trackProgressDeferred.await().getOrThrow()) - val subscriptionLimitType = subscription.getSubscriptionLimitType( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) - subscriptionLimitType == SubscriptionLimitType.TOPICS && trackProgress.learnedTopicsCount >= mobileContentTrialFreeTopics } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/injection/StepQuizComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/injection/StepQuizComponentImpl.kt index c2d226289e..cb7e315e64 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/injection/StepQuizComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/injection/StepQuizComponentImpl.kt @@ -72,8 +72,7 @@ internal class StepQuizComponentImpl( analyticInteractor = appGraph.analyticComponent.analyticInteractor, sentryInteractor = appGraph.sentryComponent.sentryInteractor, onboardingInteractor = appGraph.buildOnboardingDataComponent().onboardingInteractor, - currentSubscriptionStateRepository = appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository, - purchaseInteractor = appGraph.buildPurchaseComponent().purchaseInteractor, + featuresDataSource = appGraph.profileDataComponent.featuresDataSource, logger = appGraph.loggerComponent.logger, buildVariant = appGraph.commonComponent.buildKonfig.buildVariant, stepQuizHintsActionDispatcher = stepQuizHintsComponent.stepQuizHintsActionDispatcher, diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/injection/StepQuizFeatureBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/injection/StepQuizFeatureBuilder.kt index 035ab1ca4c..5a15acdd09 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/injection/StepQuizFeatureBuilder.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/injection/StepQuizFeatureBuilder.kt @@ -5,11 +5,11 @@ import org.hyperskill.app.analytic.domain.interactor.AnalyticInteractor import org.hyperskill.app.analytic.presentation.wrapWithAnalyticLogger import org.hyperskill.app.core.domain.BuildVariant import org.hyperskill.app.core.presentation.ActionDispatcherOptions +import org.hyperskill.app.features.data.source.FeaturesDataSource import org.hyperskill.app.logging.presentation.wrapWithLogger import org.hyperskill.app.magic_links.domain.interactor.UrlPathProcessor import org.hyperskill.app.onboarding.domain.interactor.OnboardingInteractor import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository -import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.step.domain.model.StepRoute import org.hyperskill.app.step_quiz.domain.interactor.StepQuizInteractor @@ -28,7 +28,6 @@ import org.hyperskill.app.step_quiz_toolbar.presentation.StepQuizToolbarActionDi import org.hyperskill.app.step_quiz_toolbar.presentation.StepQuizToolbarFeature import org.hyperskill.app.step_quiz_toolbar.presentation.StepQuizToolbarReducer import org.hyperskill.app.subscriptions.domain.interactor.SubscriptionsInteractor -import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository import ru.nobird.app.core.model.safeCast import ru.nobird.app.presentation.redux.dispatcher.transform import ru.nobird.app.presentation.redux.dispatcher.wrapWithActionDispatcher @@ -48,8 +47,7 @@ internal object StepQuizFeatureBuilder { analyticInteractor: AnalyticInteractor, sentryInteractor: SentryInteractor, onboardingInteractor: OnboardingInteractor, - currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, - purchaseInteractor: PurchaseInteractor, + featuresDataSource: FeaturesDataSource, stepQuizHintsReducer: StepQuizHintsReducer, stepQuizHintsActionDispatcher: StepQuizHintsActionDispatcher, stepQuizToolbarReducer: StepQuizToolbarReducer, @@ -74,12 +72,11 @@ internal object StepQuizFeatureBuilder { stepQuizReplyValidator = stepQuizReplyValidator, subscriptionsInteractor = subscriptionsInteractor, currentProfileStateRepository = currentProfileStateRepository, - currentSubscriptionStateRepository = currentSubscriptionStateRepository, + featuresDataSource = featuresDataSource, urlPathProcessor = urlPathProcessor, analyticInteractor = analyticInteractor, sentryInteractor = sentryInteractor, onboardingInteractor = onboardingInteractor, - purchaseInteractor = purchaseInteractor, logger = logger.withTag(LOG_TAG) ) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/presentation/StepQuizActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/presentation/StepQuizActionDispatcher.kt index ee05934e22..de85daa87e 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/presentation/StepQuizActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz/presentation/StepQuizActionDispatcher.kt @@ -3,19 +3,16 @@ package org.hyperskill.app.step_quiz.presentation import co.touchlab.kermit.Logger import kotlinx.coroutines.flow.distinctUntilChangedBy import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch import org.hyperskill.app.analytic.domain.interactor.AnalyticInteractor import org.hyperskill.app.core.domain.url.HyperskillUrlPath import org.hyperskill.app.core.presentation.ActionDispatcherOptions +import org.hyperskill.app.features.data.source.FeaturesDataSource import org.hyperskill.app.magic_links.domain.interactor.UrlPathProcessor import org.hyperskill.app.onboarding.domain.interactor.OnboardingInteractor import org.hyperskill.app.profile.domain.model.freemiumChargeLimitsStrategy import org.hyperskill.app.profile.domain.model.isFreemiumWrongSubmissionChargeLimitsEnabled -import org.hyperskill.app.profile.domain.model.isMobileContentTrialEnabled import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository -import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder import org.hyperskill.app.sentry.domain.withTransaction @@ -32,8 +29,6 @@ import org.hyperskill.app.submissions.domain.model.SubmissionStatus import org.hyperskill.app.submissions.domain.model.isWrongOrRejected import org.hyperskill.app.subscriptions.domain.interactor.SubscriptionsInteractor import org.hyperskill.app.subscriptions.domain.model.isProblemsLimitReached -import org.hyperskill.app.subscriptions.domain.model.orContentTrial -import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher internal class StepQuizActionDispatcher( @@ -41,44 +36,28 @@ internal class StepQuizActionDispatcher( private val stepQuizInteractor: StepQuizInteractor, private val stepQuizReplyValidator: StepQuizReplyValidator, private val subscriptionsInteractor: SubscriptionsInteractor, - private val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, private val currentProfileStateRepository: CurrentProfileStateRepository, + private val featuresDataSource: FeaturesDataSource, private val urlPathProcessor: UrlPathProcessor, private val analyticInteractor: AnalyticInteractor, private val sentryInteractor: SentryInteractor, private val onboardingInteractor: OnboardingInteractor, - private val purchaseInteractor: PurchaseInteractor, private val logger: Logger ) : CoroutineActionDispatcher(config.createConfig()) { init { - actionScope.launch { - val isMobileContentTrialEnabled = currentProfileStateRepository - .getState() - .map { it.features.isMobileContentTrialEnabled } - .getOrDefault(false) - currentSubscriptionStateRepository - .changes - .map { subscription -> - val canMakePayments = canMakePayments() - subscription to subscription - .orContentTrial( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) - .isProblemsLimitReached( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) - } - .distinctUntilChangedBy { (_, isProblemsLimitReached) -> isProblemsLimitReached } - .onEach { (subscription, isProblemsLimitReached) -> - onNewMessage( - InternalMessage.ProblemsLimitChanged(subscription, isProblemsLimitReached) + subscriptionsInteractor + .subscribeOnSubscriptionWithLimitType() + .distinctUntilChangedBy { it.isProblemsLimitReached } + .onEach { + onNewMessage( + InternalMessage.ProblemsLimitChanged( + subscription = it.subscription, + isProblemsLimitReached = it.isProblemsLimitReached ) - } - .launchIn(this) - } + ) + } + .launchIn(actionScope) } override suspend fun doSuspendableAction(action: Action) { @@ -238,18 +217,8 @@ internal class StepQuizActionDispatcher( .getState() .getOrThrow() - val canMakePayments = canMakePayments() - - val currentSubscription = - currentSubscriptionStateRepository - .getState() - .map { subscription -> - subscription.orContentTrial( - isMobileContentTrialEnabled = currentProfile.features.isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) - } - .getOrThrow() + val subscriptionWithLimitType = + subscriptionsInteractor.getSubscriptionWithLimitType().getOrThrow() val attempt = stepQuizInteractor @@ -263,14 +232,10 @@ internal class StepQuizActionDispatcher( step = action.step, attempt = attempt, submissionState = submissionState, - subscription = currentSubscription, + subscription = subscriptionWithLimitType.subscription, chargeLimitsStrategy = currentProfile.freemiumChargeLimitsStrategy, problemsOnboardingFlags = onboardingInteractor.getProblemsOnboardingFlags(), - isProblemsLimitReached = currentSubscription - .isProblemsLimitReached( - isMobileContentTrialEnabled = currentProfile.features.isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) + isProblemsLimitReached = subscriptionWithLimitType.isProblemsLimitReached ) }.let(onNewMessage) } @@ -294,37 +259,20 @@ internal class StepQuizActionDispatcher( action: InternalAction.UpdateProblemsLimit, onNewMessage: (Message) -> Unit ) { - val currentProfile = currentProfileStateRepository.getState().getOrElse { return } - if (!currentProfile.features.isFreemiumWrongSubmissionChargeLimitsEnabled) return + val features = featuresDataSource.getFeaturesMap() + if (!features.isFreemiumWrongSubmissionChargeLimitsEnabled) return subscriptionsInteractor.chargeProblemsLimits(action.chargeStrategy) - val canMakePayments = canMakePayments() - - val currentSubscription = - currentSubscriptionStateRepository - .getState() - .map { subscription -> - subscription.orContentTrial( - isMobileContentTrialEnabled = currentProfile.features.isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) - } - .getOrElse { return } + val currentSubscriptionWithLimitType = + subscriptionsInteractor.getSubscriptionWithLimitType().getOrElse { return } onNewMessage( InternalMessage.UpdateProblemsLimitResult( - subscription = currentSubscription, - isProblemsLimitReached = currentSubscription - .isProblemsLimitReached( - isMobileContentTrialEnabled = currentProfile.features.isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ), - chargeLimitsStrategy = currentProfile.freemiumChargeLimitsStrategy + subscription = currentSubscriptionWithLimitType.subscription, + isProblemsLimitReached = currentSubscriptionWithLimitType.isProblemsLimitReached, + chargeLimitsStrategy = features.freemiumChargeLimitsStrategy ) ) } - - private suspend fun canMakePayments(): Boolean = - purchaseInteractor.canMakePayments().getOrDefault(false) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/injection/StepQuizToolbarComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/injection/StepQuizToolbarComponentImpl.kt index 877adef2f8..16b707b544 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/injection/StepQuizToolbarComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/injection/StepQuizToolbarComponentImpl.kt @@ -22,9 +22,8 @@ internal class StepQuizToolbarComponentImpl( private val mainStepQuizToolbarActionDispatcher: MainStepQuizToolbarActionDispatcher get() = MainStepQuizToolbarActionDispatcher( config = ActionDispatcherOptions(), - currentSubscriptionStateRepository = appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository, - currentProfileStateRepository = appGraph.profileDataComponent.currentProfileStateRepository, - purchaseInteractor = appGraph.buildPurchaseComponent().purchaseInteractor, + subscriptionsInteractor = appGraph.subscriptionDataComponent.subscriptionsInteractor, + featuresDataSource = appGraph.profileDataComponent.featuresDataSource, logger = appGraph.loggerComponent.logger.withTag(LOG_TAG) ) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/MainStepQuizToolbarActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/MainStepQuizToolbarActionDispatcher.kt index 262dbb31ac..72281c0a01 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/MainStepQuizToolbarActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/MainStepQuizToolbarActionDispatcher.kt @@ -3,40 +3,27 @@ package org.hyperskill.app.step_quiz_toolbar.presentation import co.touchlab.kermit.Logger 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.presentation.ActionDispatcherOptions +import org.hyperskill.app.features.data.source.FeaturesDataSource import org.hyperskill.app.profile.domain.model.freemiumChargeLimitsStrategy -import org.hyperskill.app.profile.domain.model.isMobileContentTrialEnabled -import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository -import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.step_quiz_toolbar.presentation.StepQuizToolbarFeature.Action import org.hyperskill.app.step_quiz_toolbar.presentation.StepQuizToolbarFeature.InternalAction import org.hyperskill.app.step_quiz_toolbar.presentation.StepQuizToolbarFeature.InternalMessage import org.hyperskill.app.step_quiz_toolbar.presentation.StepQuizToolbarFeature.Message -import org.hyperskill.app.subscriptions.domain.model.orContentTrial -import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository +import org.hyperskill.app.subscriptions.domain.interactor.SubscriptionsInteractor import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher internal class MainStepQuizToolbarActionDispatcher( config: ActionDispatcherOptions, - private val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, - private val currentProfileStateRepository: CurrentProfileStateRepository, - private val purchaseInteractor: PurchaseInteractor, + private val subscriptionsInteractor: SubscriptionsInteractor, + private val featuresDataSource: FeaturesDataSource, private val logger: Logger ) : CoroutineActionDispatcher(config.createConfig()) { - private var isMobileContentTrialEnabled = false - init { - currentSubscriptionStateRepository - .changes - .map { subscription -> - subscription.orContentTrial( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments() - ) - } + subscriptionsInteractor + .subscribeOnSubscriptionWithLimitType() .distinctUntilChanged() .onEach { onNewMessage(InternalMessage.SubscriptionChanged(it)) @@ -54,39 +41,23 @@ internal class MainStepQuizToolbarActionDispatcher( } private suspend fun handleFetchSubscription(onNewMessage: (Message) -> Unit) { - val profile = currentProfileStateRepository.getState().getOrElse { - logger.e(it) { "Failed to fetch profile" } - onNewMessage(InternalMessage.SubscriptionFetchError) - return - } - - this.isMobileContentTrialEnabled = profile.features.isMobileContentTrialEnabled + val features = featuresDataSource.getFeaturesMap() - val canMakePayments = canMakePayments() + val subscriptionWithLimitType = + subscriptionsInteractor + .getSubscriptionWithLimitType() + .getOrElse { + logger.e(it) { "Failed to fetch subscription" } + onNewMessage(InternalMessage.SubscriptionFetchError) + return + } - val subscription = currentSubscriptionStateRepository - .getState() - .map { subscription -> - subscription.orContentTrial( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) - } - .getOrElse { - logger.e(it) { "Failed to fetch subscription" } - onNewMessage(InternalMessage.SubscriptionFetchError) - return - } onNewMessage( InternalMessage.SubscriptionFetchSuccess( - subscription = subscription, - isMobileContentTrialEnabled = profile.features.isMobileContentTrialEnabled, - canMakePayment = canMakePayments, - chargeLimitsStrategy = profile.freemiumChargeLimitsStrategy + subscription = subscriptionWithLimitType.subscription, + subscriptionLimitType = subscriptionWithLimitType.subscriptionLimitType, + chargeLimitsStrategy = features.freemiumChargeLimitsStrategy ) ) } - - private suspend fun canMakePayments(): Boolean = - purchaseInteractor.canMakePayments().getOrDefault(false) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/StepQuizToolbarFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/StepQuizToolbarFeature.kt index aec77bf891..ff78f8948b 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/StepQuizToolbarFeature.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/StepQuizToolbarFeature.kt @@ -5,6 +5,8 @@ import org.hyperskill.app.problems_limit_info.domain.model.ProblemsLimitInfoModa import org.hyperskill.app.step.domain.model.StepRoute import org.hyperskill.app.subscriptions.domain.model.FreemiumChargeLimitsStrategy import org.hyperskill.app.subscriptions.domain.model.Subscription +import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType +import org.hyperskill.app.subscriptions.domain.model.SubscriptionWithLimitType object StepQuizToolbarFeature { sealed interface State { @@ -14,8 +16,7 @@ object StepQuizToolbarFeature { object Error : State data class Content( val subscription: Subscription, - val isMobileContentTrialEnabled: Boolean, - val canMakePayment: Boolean, + val subscriptionLimitType: SubscriptionLimitType, val chargeLimitsStrategy: FreemiumChargeLimitsStrategy ) : State } @@ -47,12 +48,13 @@ object StepQuizToolbarFeature { object SubscriptionFetchError : InternalMessage data class SubscriptionFetchSuccess( val subscription: Subscription, - val isMobileContentTrialEnabled: Boolean, - val canMakePayment: Boolean, + val subscriptionLimitType: SubscriptionLimitType, val chargeLimitsStrategy: FreemiumChargeLimitsStrategy ) : InternalMessage - data class SubscriptionChanged(val subscription: Subscription) : InternalMessage + data class SubscriptionChanged( + val subscriptionWithLimitType: SubscriptionWithLimitType + ) : InternalMessage } sealed interface Action { diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/StepQuizToolbarReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/StepQuizToolbarReducer.kt index 92a4b82590..47ef21cb39 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/StepQuizToolbarReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/presentation/StepQuizToolbarReducer.kt @@ -40,8 +40,7 @@ class StepQuizToolbarReducer( ): StepQuizToolbarReducerResult = State.Content( subscription = message.subscription, - isMobileContentTrialEnabled = message.isMobileContentTrialEnabled, - canMakePayment = message.canMakePayment, + subscriptionLimitType = message.subscriptionLimitType, chargeLimitsStrategy = message.chargeLimitsStrategy ) to emptySet() @@ -53,7 +52,10 @@ class StepQuizToolbarReducer( message: InternalMessage.SubscriptionChanged ): StepQuizToolbarReducerResult = if (state is State.Content) { - state.copy(subscription = message.subscription) to emptySet() + state.copy( + subscription = message.subscriptionWithLimitType.subscription, + subscriptionLimitType = message.subscriptionWithLimitType.subscriptionLimitType + ) to emptySet() } else { state to emptySet() } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/view/StepQuizToolbarViewStateMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/view/StepQuizToolbarViewStateMapper.kt index f58fb71fcc..355c613ef1 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/view/StepQuizToolbarViewStateMapper.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_quiz_toolbar/view/StepQuizToolbarViewStateMapper.kt @@ -3,7 +3,6 @@ package org.hyperskill.app.step_quiz_toolbar.view import org.hyperskill.app.step_quiz_toolbar.presentation.StepQuizToolbarFeature.State import org.hyperskill.app.step_quiz_toolbar.presentation.StepQuizToolbarFeature.ViewState import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType -import org.hyperskill.app.subscriptions.domain.model.getSubscriptionLimitType object StepQuizToolbarViewStateMapper { @@ -15,12 +14,7 @@ object StepQuizToolbarViewStateMapper { State.Error -> ViewState.Error is State.Content -> { val stepsLimitLeft = state.subscription.stepsLimitLeft - val subscriptionLimitType = - state.subscription.getSubscriptionLimitType( - isMobileContentTrialEnabled = state.isMobileContentTrialEnabled, - canMakePayments = state.canMakePayment - ) - if (subscriptionLimitType == SubscriptionLimitType.PROBLEMS && stepsLimitLeft != null) { + if (state.subscriptionLimitType == SubscriptionLimitType.PROBLEMS && stepsLimitLeft != null) { ViewState.Content.Visible(stepsLimitLabel = stepsLimitLeft.toString()) } else { ViewState.Content.Hidden diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/injection/StudyPlanWidgetComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/injection/StudyPlanWidgetComponentImpl.kt index ae6d6923e4..45be5661b5 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/injection/StudyPlanWidgetComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/injection/StudyPlanWidgetComponentImpl.kt @@ -16,9 +16,8 @@ internal class StudyPlanWidgetComponentImpl(private val appGraph: AppGraph) : St .stateRepositoriesComponent.nextLearningActivityStateRepository, currentProfileStateRepository = appGraph.profileDataComponent.currentProfileStateRepository, currentStudyPlanStateRepository = appGraph.stateRepositoriesComponent.currentStudyPlanStateRepository, - currentSubscriptionStateRepository = appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository, + subscriptionInteractor = appGraph.subscriptionDataComponent.subscriptionsInteractor, progressesRepository = appGraph.buildProgressesDataComponent().progressesRepository, - purchaseInteractor = appGraph.buildPurchaseComponent().purchaseInteractor, sentryInteractor = appGraph.sentryComponent.sentryInteractor ) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/MainStudyPlanWidgetActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/MainStudyPlanWidgetActionDispatcher.kt index 63e9813a8e..ca36f34844 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/MainStudyPlanWidgetActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/MainStudyPlanWidgetActionDispatcher.kt @@ -8,10 +8,8 @@ import kotlinx.coroutines.flow.onEach import org.hyperskill.app.core.presentation.ActionDispatcherOptions import org.hyperskill.app.learning_activities.domain.repository.LearningActivitiesRepository import org.hyperskill.app.learning_activities.domain.repository.NextLearningActivityStateRepository -import org.hyperskill.app.profile.domain.model.isMobileContentTrialEnabled import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository import org.hyperskill.app.progresses.domain.repository.ProgressesRepository -import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder import org.hyperskill.app.sentry.domain.withTransaction @@ -20,8 +18,7 @@ import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature. import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature.InternalAction import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature.InternalMessage import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetFeature.Message -import org.hyperskill.app.subscriptions.domain.model.orContentTrial -import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository +import org.hyperskill.app.subscriptions.domain.interactor.SubscriptionsInteractor import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher internal class MainStudyPlanWidgetActionDispatcher( @@ -30,9 +27,8 @@ internal class MainStudyPlanWidgetActionDispatcher( private val nextLearningActivityStateRepository: NextLearningActivityStateRepository, private val currentProfileStateRepository: CurrentProfileStateRepository, private val currentStudyPlanStateRepository: CurrentStudyPlanStateRepository, - private val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, + private val subscriptionInteractor: SubscriptionsInteractor, private val progressesRepository: ProgressesRepository, - private val purchaseInteractor: PurchaseInteractor, private val sentryInteractor: SentryInteractor ) : CoroutineActionDispatcher(config.createConfig()) { @@ -43,6 +39,12 @@ internal class MainStudyPlanWidgetActionDispatcher( onNewMessage(InternalMessage.ProfileChanged(profile)) } .launchIn(actionScope) + subscriptionInteractor + .subscribeOnSubscriptionLimitType() + .onEach { + onNewMessage(InternalMessage.SubscriptionLimitTypeChanged(it)) + } + .launchIn(actionScope) } override suspend fun doSuspendableAction(action: Action) { @@ -76,14 +78,6 @@ internal class MainStudyPlanWidgetActionDispatcher( is InternalAction.CaptureSentryException -> { sentryInteractor.captureException(action.throwable) } - is InternalAction.FetchPaymentAbility -> { - purchaseInteractor - .canMakePayments() - .getOrDefault(false) - .let { - onNewMessage(InternalMessage.FetchPaymentAbilityResult(it)) - } - } else -> { // no op } @@ -107,8 +101,8 @@ internal class MainStudyPlanWidgetActionDispatcher( learningActivityStates = action.learningActivityStates ) } - val subscriptionDeferred = async { - currentSubscriptionStateRepository.getState() + val subscriptionWithLimitTypeDeferred = async { + subscriptionInteractor.getSubscriptionWithLimitType() } val profile = currentProfileStateRepository.getState().getOrNull() @@ -124,25 +118,20 @@ internal class MainStudyPlanWidgetActionDispatcher( } } - val canMakePayments = purchaseInteractor.canMakePayments().getOrDefault(false) - val learningActivitiesResponse = learningActivitiesDeferred.await().getOrThrow() - val subscription = - subscriptionDeferred + val subscriptionWithLimitType = + subscriptionWithLimitTypeDeferred .await() .getOrThrow() - .orContentTrial( - isMobileContentTrialEnabled = profile?.features?.isMobileContentTrialEnabled == true, - canMakePayments = canMakePayments - ) + val trackProgress = trackProgressDeferred.await().getOrThrow() StudyPlanWidgetFeature.LearningActivitiesWithSectionsFetchResult.Success( learningActivities = learningActivitiesResponse.learningActivities, studyPlanSections = learningActivitiesResponse.studyPlanSections, - subscription = subscription, learnedTopicsCount = trackProgress?.learnedTopicsCount ?: 0, - canMakePayments = canMakePayments + subscription = subscriptionWithLimitType.subscription, + subscriptionLimitType = subscriptionWithLimitType.subscriptionLimitType ) } }.let(onNewMessage) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetFeature.kt index 2b6f0f8d52..122480a548 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetFeature.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetFeature.kt @@ -8,11 +8,11 @@ import org.hyperskill.app.learning_activities.presentation.model.LearningActivit import org.hyperskill.app.paywall.domain.model.PaywallTransitionSource import org.hyperskill.app.profile.domain.model.Profile import org.hyperskill.app.profile.domain.model.isLearningPathDividedTrackTopicsEnabled -import org.hyperskill.app.profile.domain.model.isMobileContentTrialEnabled import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransaction import org.hyperskill.app.study_plan.domain.model.StudyPlanSection import org.hyperskill.app.study_plan.domain.model.StudyPlanSectionType import org.hyperskill.app.subscriptions.domain.model.Subscription +import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType import org.hyperskill.app.topics.domain.model.TopicProgress object StudyPlanWidgetFeature { @@ -36,29 +36,21 @@ object StudyPlanWidgetFeature { */ val isRefreshing: Boolean = false, - /** - * Current user subscription - */ - val subscription: Subscription? = null, - /** * Actual learnedTopicsCount in the current track */ val learnedTopicsCount: Int = 0, - val canMakePayments: Boolean = false + /** + * Subscription limit type + */ + val subscriptionLimitType: SubscriptionLimitType = SubscriptionLimitType.NONE ) { /** * Divided track topics feature enabled flag */ val isLearningPathDividedTrackTopicsEnabled: Boolean get() = profile?.features?.isLearningPathDividedTrackTopicsEnabled ?: false - - /** - * MobileContentTrial feature flag - */ - val isMobileContentTrialEnabled: Boolean - get() = profile?.features?.isMobileContentTrialEnabled ?: false } enum class SectionStatus { @@ -116,16 +108,22 @@ object StudyPlanWidgetFeature { data class ProfileChanged(val profile: Profile) : InternalMessage - data class FetchPaymentAbilityResult(val canMakePayments: Boolean) : InternalMessage + data class FetchSubscriptionLimitTypeResult( + val subscriptionLimitType: SubscriptionLimitType + ) : InternalMessage + + data class SubscriptionLimitTypeChanged( + val subscriptionLimitType: SubscriptionLimitType + ) : InternalMessage } internal sealed interface LearningActivitiesWithSectionsFetchResult : Message { data class Success( val learningActivities: List, val studyPlanSections: List, - val subscription: Subscription, val learnedTopicsCount: Int, - val canMakePayments: Boolean + val subscription: Subscription, + val subscriptionLimitType: SubscriptionLimitType ) : LearningActivitiesWithSectionsFetchResult data object Failed : LearningActivitiesWithSectionsFetchResult @@ -178,8 +176,6 @@ object StudyPlanWidgetFeature { data class PutTopicsProgressesToCache(val topicsProgresses: List) : InternalAction - data object FetchPaymentAbility : InternalAction - data class CaptureSentryException(val throwable: Throwable) : InternalAction data class LogAnalyticEvent(val analyticEvent: AnalyticEvent) : InternalAction } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt index b7d62f5c7f..2ea1eac615 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetReducer.kt @@ -112,8 +112,10 @@ class StudyPlanWidgetReducer : StateReducer { ) ) ) - is InternalMessage.FetchPaymentAbilityResult -> - handleFetchPaymentAbilityResult(state, message) + is InternalMessage.FetchSubscriptionLimitTypeResult -> + handleFetchSubscriptionLimitTypeResult(state, message) + is InternalMessage.SubscriptionLimitTypeChanged -> + handleSubscriptionLimitTypeChanged(state, message) } ?: (state to emptySet()) private fun coldContentFetch(state: State, message: InternalMessage.Initialize): StudyPlanWidgetReducerResult = @@ -130,8 +132,7 @@ class StudyPlanWidgetReducer : StateReducer { setOfNotNull( InternalAction.FetchLearningActivitiesWithSections(), InternalAction.FetchProfile, - InternalAction.UpdateCurrentStudyPlanState(forceUpdate), - if (forceUpdate) InternalAction.FetchPaymentAbility else null + InternalAction.UpdateCurrentStudyPlanState(forceUpdate) ) private fun handleLearningActivitiesWithSectionsFetchSuccess( @@ -143,7 +144,8 @@ class StudyPlanWidgetReducer : StateReducer { val currentSectionId = visibleSections.firstOrNull()?.id ?: return state.copy( studyPlanSections = emptyMap(), sectionsStatus = StudyPlanWidgetFeature.SectionStatus.LOADED, - isRefreshing = false + isRefreshing = false, + subscriptionLimitType = message.subscriptionLimitType ) to emptySet() val supportedSections = visibleSections @@ -172,10 +174,9 @@ class StudyPlanWidgetReducer : StateReducer { studyPlanSections = studyPlanSections, sectionsStatus = StudyPlanWidgetFeature.SectionStatus.LOADED, isRefreshing = false, - subscription = message.subscription, learnedTopicsCount = message.learnedTopicsCount, - canMakePayments = message.canMakePayments, - activities = message.learningActivities.associateBy { it.id } + activities = message.learningActivities.associateBy { it.id }, + subscriptionLimitType = message.subscriptionLimitType ) return if (loadedSectionsState.studyPlanSections.isNotEmpty()) { @@ -477,11 +478,17 @@ class StudyPlanWidgetReducer : StateReducer { state to emptySet() } - private fun handleFetchPaymentAbilityResult( + private fun handleFetchSubscriptionLimitTypeResult( + state: State, + message: InternalMessage.FetchSubscriptionLimitTypeResult + ): StudyPlanWidgetReducerResult = + state.copy(subscriptionLimitType = message.subscriptionLimitType) to emptySet() + + private fun handleSubscriptionLimitTypeChanged( state: State, - message: InternalMessage.FetchPaymentAbilityResult + message: InternalMessage.SubscriptionLimitTypeChanged ): StudyPlanWidgetReducerResult = - state.copy(canMakePayments = message.canMakePayments) to emptySet() + state.copy(subscriptionLimitType = message.subscriptionLimitType) to emptySet() private fun getFetchLearningActivitiesSentryTransaction( state: State, diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetStateExtensions.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetStateExtensions.kt index daa5a202e5..d5e2904a35 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetStateExtensions.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/widget/presentation/StudyPlanWidgetStateExtensions.kt @@ -6,7 +6,6 @@ import org.hyperskill.app.study_plan.domain.model.StudyPlanSection import org.hyperskill.app.study_plan.domain.model.StudyPlanSectionType import org.hyperskill.app.study_plan.domain.model.rootTopicsActivitiesToBeLoaded import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType -import org.hyperskill.app.subscriptions.domain.model.getSubscriptionLimitType /** * @return current [StudyPlanSection]. @@ -100,11 +99,7 @@ internal fun StudyPlanWidgetFeature.State.isActivityLocked( internal fun StudyPlanWidgetFeature.State.getUnlockedActivitiesCount(sectionId: Long): Int? { val section = studyPlanSections[sectionId]?.studyPlanSection ?: return null val isRootTopicsSection = section.type == StudyPlanSectionType.ROOT_TOPICS - val isTopicsLimitEnabled = - subscription?.getSubscriptionLimitType( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) == SubscriptionLimitType.TOPICS + val isTopicsLimitEnabled = subscriptionLimitType == SubscriptionLimitType.TOPICS val unlockedActivitiesCount = profile?.featureValues?.mobileContentTrialFreeTopics?.minus(learnedTopicsCount) return if (isRootTopicsSection && isTopicsLimitEnabled && unlockedActivitiesCount != null) { unlockedActivitiesCount diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/data/repository/CurrentSubscriptionStateRepositoryImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/data/repository/CurrentSubscriptionStateRepositoryImpl.kt index 026de22c60..3e4bd04eff 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/data/repository/CurrentSubscriptionStateRepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/data/repository/CurrentSubscriptionStateRepositoryImpl.kt @@ -1,15 +1,34 @@ package org.hyperskill.app.subscriptions.data.repository +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import org.hyperskill.app.core.data.repository.BaseStateRepository +import org.hyperskill.app.features.data.source.FeaturesDataSource +import org.hyperskill.app.profile.domain.model.isMobileContentTrialEnabled +import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.subscriptions.data.source.CurrentSubscriptionStateHolder import org.hyperskill.app.subscriptions.data.source.SubscriptionsRemoteDataSource import org.hyperskill.app.subscriptions.domain.model.Subscription +import org.hyperskill.app.subscriptions.domain.model.orContentTrial import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository internal class CurrentSubscriptionStateRepositoryImpl( private val subscriptionsRemoteDataSource: SubscriptionsRemoteDataSource, - override val stateHolder: CurrentSubscriptionStateHolder + override val stateHolder: CurrentSubscriptionStateHolder, + private val featuresDataSource: FeaturesDataSource, + private val purchaseInteractor: PurchaseInteractor, ) : CurrentSubscriptionStateRepository, BaseStateRepository() { override suspend fun loadState(): Result = - subscriptionsRemoteDataSource.getCurrentSubscription() + subscriptionsRemoteDataSource + .getCurrentSubscription() + .map { mapSubscription(it) } + + override val changes: Flow + get() = super.changes.map(::mapSubscription) + + private suspend fun mapSubscription(subscription: Subscription): Subscription = + subscription.orContentTrial( + isMobileContentTrialEnabled = featuresDataSource.getFeaturesMap().isMobileContentTrialEnabled, + canMakePayments = purchaseInteractor.canMakePayments().getOrDefault(false) + ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/interactor/SubscriptionsInteractor.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/interactor/SubscriptionsInteractor.kt index 0cebfbb86f..239427da88 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/interactor/SubscriptionsInteractor.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/interactor/SubscriptionsInteractor.kt @@ -6,6 +6,8 @@ import kotlin.time.toDuration import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.datetime.Clock import kotlinx.datetime.Instant @@ -13,7 +15,9 @@ import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import org.hyperskill.app.auth.domain.interactor.AuthInteractor +import org.hyperskill.app.core.domain.repository.StateWithSource import org.hyperskill.app.core.domain.repository.updateState +import org.hyperskill.app.features.data.source.FeaturesDataSource import org.hyperskill.app.profile.domain.model.isFreemiumIncreaseLimitsForFirstStepCompletionEnabled import org.hyperskill.app.profile.domain.model.isMobileContentTrialEnabled import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository @@ -21,14 +25,17 @@ import org.hyperskill.app.profile.domain.repository.isFreemiumWrongSubmissionCha import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.subscriptions.domain.model.FreemiumChargeLimitsStrategy import org.hyperskill.app.subscriptions.domain.model.Subscription +import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType import org.hyperskill.app.subscriptions.domain.model.SubscriptionType +import org.hyperskill.app.subscriptions.domain.model.SubscriptionWithLimitType +import org.hyperskill.app.subscriptions.domain.model.getSubscriptionLimitType import org.hyperskill.app.subscriptions.domain.model.isActive import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository -import org.hyperskill.app.subscriptions.domain.repository.areProblemsLimited class SubscriptionsInteractor( private val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, private val currentProfileStateRepository: CurrentProfileStateRepository, + private val featuresDataSource: FeaturesDataSource, private val purchaseInteractor: PurchaseInteractor, private val authInteractor: AuthInteractor, logger: Logger @@ -44,19 +51,71 @@ class SubscriptionsInteractor( // Problems limits - suspend fun getCurrentSubscription(): Result = - currentSubscriptionStateRepository.getState(forceUpdate = false) - - suspend fun chargeProblemsLimits(chargeStrategy: FreemiumChargeLimitsStrategy) { - val isMobileContentTrialEnabled = currentProfileStateRepository - .getState() - .map { it.features.isMobileContentTrialEnabled } + suspend fun isProblemsLimitEnabled(forceUpdate: Boolean = false): Boolean = + currentSubscriptionStateRepository + .getState(forceUpdate = forceUpdate) + .map { + val subscriptionLimitType = getSubscriptionLimitType(it) + subscriptionLimitType == SubscriptionLimitType.PROBLEMS + } .getOrDefault(false) - val areProblemsLimitEnabled = currentSubscriptionStateRepository.areProblemsLimited( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, + + suspend fun getSubscriptionLimitType(forceUpdate: Boolean = false): Result = + currentSubscriptionStateRepository + .getState(forceUpdate = forceUpdate) + .map { + getSubscriptionLimitType(it) + } + + suspend fun getSubscriptionWithLimitType(forceUpdate: Boolean = false): Result = + currentSubscriptionStateRepository + .getState(forceUpdate = forceUpdate) + .map { + SubscriptionWithLimitType( + subscription = it, + subscriptionLimitType = getSubscriptionLimitType(it) + ) + } + + suspend fun getSubscriptionWithLimitTypeWithSource( + forceUpdate: Boolean = false + ): Result> = + currentSubscriptionStateRepository + .getStateWithSource(forceUpdate = forceUpdate) + .map { + StateWithSource( + state = SubscriptionWithLimitType( + subscription = it.state, + subscriptionLimitType = getSubscriptionLimitType(it.state) + ), + usedDataSourceType = it.usedDataSourceType + ) + } + + fun subscribeOnSubscriptionLimitType(): Flow = + currentSubscriptionStateRepository + .changes + .map(::getSubscriptionLimitType) + + fun subscribeOnSubscriptionWithLimitType(): Flow = + currentSubscriptionStateRepository + .changes + .map { + SubscriptionWithLimitType( + subscription = it, + subscriptionLimitType = getSubscriptionLimitType(it) + ) + } + + private suspend fun getSubscriptionLimitType(subscription: Subscription): SubscriptionLimitType = + subscription.getSubscriptionLimitType( + isMobileContentTrialEnabled = featuresDataSource.getFeaturesMap().isMobileContentTrialEnabled, canMakePayments = purchaseInteractor.canMakePayments().getOrDefault(false) ) - if (areProblemsLimitEnabled) { + + suspend fun chargeProblemsLimits(chargeStrategy: FreemiumChargeLimitsStrategy) { + val isProblemsLimitEnabled = isProblemsLimitEnabled() + if (isProblemsLimitEnabled) { when (chargeStrategy) { FreemiumChargeLimitsStrategy.AFTER_WRONG_SUBMISSION -> chargeLimitsAfterWrongSubmission() FreemiumChargeLimitsStrategy.AFTER_CORRECT_SUBMISSION -> chargeLimitsAfterCorrectSubmission() diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/model/Subscription.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/model/Subscription.kt index 0de77e2126..86c91605cf 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/model/Subscription.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/model/Subscription.kt @@ -54,22 +54,11 @@ internal fun Subscription.getSubscriptionLimitType( else -> type.subscriptionLimitType } -internal fun Subscription.isProblemsLimitReached( - isMobileContentTrialEnabled: Boolean, - canMakePayments: Boolean -): Boolean { - val subscriptionLimitType = getSubscriptionLimitType( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) - return subscriptionLimitType == SubscriptionLimitType.PROBLEMS && stepsLimitLeft == 0 -} - internal val Subscription.isFreemium: Boolean get() = type == SubscriptionType.FREEMIUM || type == SubscriptionType.MOBILE_ONLY && status != SubscriptionStatus.ACTIVE -fun Subscription.orContentTrial( +internal fun Subscription.orContentTrial( isMobileContentTrialEnabled: Boolean, canMakePayments: Boolean ): Subscription = diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/model/SubscriptionWithLimitType.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/model/SubscriptionWithLimitType.kt new file mode 100644 index 0000000000..dffbe15434 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/model/SubscriptionWithLimitType.kt @@ -0,0 +1,9 @@ +package org.hyperskill.app.subscriptions.domain.model + +data class SubscriptionWithLimitType( + val subscription: Subscription, + val subscriptionLimitType: SubscriptionLimitType +) + +val SubscriptionWithLimitType.isProblemsLimitReached: Boolean + get() = subscriptionLimitType == SubscriptionLimitType.PROBLEMS && subscription.stepsLimitLeft == 0 \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/repository/CurrentSubscriptionStateRepository.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/repository/CurrentSubscriptionStateRepository.kt index 18b63597aa..2a95409c46 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/repository/CurrentSubscriptionStateRepository.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/domain/repository/CurrentSubscriptionStateRepository.kt @@ -2,22 +2,5 @@ package org.hyperskill.app.subscriptions.domain.repository import org.hyperskill.app.core.domain.repository.StateRepository import org.hyperskill.app.subscriptions.domain.model.Subscription -import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType -import org.hyperskill.app.subscriptions.domain.model.getSubscriptionLimitType -interface CurrentSubscriptionStateRepository : StateRepository - -internal suspend fun CurrentSubscriptionStateRepository.areProblemsLimited( - isMobileContentTrialEnabled: Boolean, - canMakePayments: Boolean -): Boolean = - getState(forceUpdate = false) - .map { - val subscriptionLimitType = - it.getSubscriptionLimitType( - isMobileContentTrialEnabled = isMobileContentTrialEnabled, - canMakePayments = canMakePayments - ) - subscriptionLimitType == SubscriptionLimitType.PROBLEMS - } - .getOrDefault(defaultValue = false) \ No newline at end of file +interface CurrentSubscriptionStateRepository : StateRepository \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/injection/SubscriptionsDataComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/injection/SubscriptionsDataComponentImpl.kt index 74dc9050b3..5f2b0a4de0 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/injection/SubscriptionsDataComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/injection/SubscriptionsDataComponentImpl.kt @@ -3,6 +3,7 @@ package org.hyperskill.app.subscriptions.injection import co.touchlab.kermit.Logger import org.hyperskill.app.auth.domain.interactor.AuthInteractor import org.hyperskill.app.core.injection.AppGraph +import org.hyperskill.app.features.data.source.FeaturesDataSource import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor import org.hyperskill.app.subscriptions.data.repository.SubscriptionsRepositoryImpl @@ -24,6 +25,9 @@ class SubscriptionsDataComponentImpl( private val currentProfileStateRepository: CurrentProfileStateRepository = appGraph.profileDataComponent.currentProfileStateRepository + private val featuresDataSource: FeaturesDataSource = + appGraph.profileDataComponent.featuresDataSource + private val purchaseInteractor: PurchaseInteractor = appGraph.buildPurchaseComponent().purchaseInteractor @@ -42,6 +46,7 @@ class SubscriptionsDataComponentImpl( currentProfileStateRepository = currentProfileStateRepository, purchaseInteractor = purchaseInteractor, authInteractor = authInteractor, + featuresDataSource = featuresDataSource, logger = logger ) } diff --git a/shared/src/commonTest/kotlin/org/hyperskill/study_plan/widget/StudyPlanWidgetStateExtensionsTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/study_plan/widget/StudyPlanWidgetStateExtensionsTest.kt index bd83db57db..8d4e0108c0 100644 --- a/shared/src/commonTest/kotlin/org/hyperskill/study_plan/widget/StudyPlanWidgetStateExtensionsTest.kt +++ b/shared/src/commonTest/kotlin/org/hyperskill/study_plan/widget/StudyPlanWidgetStateExtensionsTest.kt @@ -20,9 +20,7 @@ import org.hyperskill.app.study_plan.widget.presentation.getLoadedSectionActivit import org.hyperskill.app.study_plan.widget.presentation.getUnlockedActivitiesCount import org.hyperskill.app.study_plan.widget.presentation.isActivityLocked import org.hyperskill.app.study_plan.widget.presentation.isPaywallShown -import org.hyperskill.app.subscriptions.domain.model.Subscription -import org.hyperskill.app.subscriptions.domain.model.SubscriptionStatus -import org.hyperskill.app.subscriptions.domain.model.SubscriptionType +import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType import org.hyperskill.learning_activities.domain.model.stub import org.hyperskill.profile.stub import org.hyperskill.study_plan.domain.model.stub @@ -155,7 +153,7 @@ class StudyPlanWidgetStateExtensionsTest { } @Test - fun `isActivityLocked should return true if activity is locked`() { + fun `isActivityLocked should return true for locked activities in case of topics limit`() { val section = StudyPlanSection.stub( id = 1, type = StudyPlanSectionType.ROOT_TOPICS, @@ -171,10 +169,7 @@ class StudyPlanWidgetStateExtensionsTest { ), activities = mapOf(1L to LearningActivity.stub(id = 1)), profile = Profile.stub(), - subscription = Subscription( - type = SubscriptionType.MOBILE_CONTENT_TRIAL, - status = SubscriptionStatus.ACTIVE - ), + subscriptionLimitType = SubscriptionLimitType.TOPICS, learnedTopicsCount = 1 ) @@ -201,10 +196,7 @@ class StudyPlanWidgetStateExtensionsTest { featureValues = FeatureValues(mobileContentTrialFreeTopics = 2), featuresMap = mapOf(FeatureKeys.MOBILE_CONTENT_TRIAL to true), ), - subscription = Subscription( - type = SubscriptionType.MOBILE_CONTENT_TRIAL, - status = SubscriptionStatus.ACTIVE - ), + subscriptionLimitType = SubscriptionLimitType.TOPICS, learnedTopicsCount = 1 ) @@ -231,10 +223,7 @@ class StudyPlanWidgetStateExtensionsTest { featureValues = FeatureValues(mobileContentTrialFreeTopics = 2), featuresMap = mapOf(FeatureKeys.MOBILE_CONTENT_TRIAL to true), ), - subscription = Subscription( - type = SubscriptionType.MOBILE_CONTENT_TRIAL, - status = SubscriptionStatus.ACTIVE - ), + subscriptionLimitType = SubscriptionLimitType.TOPICS, learnedTopicsCount = 1 ) @@ -261,10 +250,7 @@ class StudyPlanWidgetStateExtensionsTest { featureValues = FeatureValues(mobileContentTrialFreeTopics = 10), featuresMap = mapOf(FeatureKeys.MOBILE_CONTENT_TRIAL to true), ), - subscription = Subscription( - type = SubscriptionType.MOBILE_CONTENT_TRIAL, - status = SubscriptionStatus.ACTIVE - ), + subscriptionLimitType = SubscriptionLimitType.TOPICS, learnedTopicsCount = 10 ) @@ -291,10 +277,7 @@ class StudyPlanWidgetStateExtensionsTest { featureValues = FeatureValues(mobileContentTrialFreeTopics = 10), featuresMap = mapOf(FeatureKeys.MOBILE_CONTENT_TRIAL to true), ), - subscription = Subscription( - type = SubscriptionType.MOBILE_CONTENT_TRIAL, - status = SubscriptionStatus.ACTIVE - ), + subscriptionLimitType = SubscriptionLimitType.TOPICS, learnedTopicsCount = 10 ) @@ -332,10 +315,7 @@ class StudyPlanWidgetStateExtensionsTest { featureValues = FeatureValues(mobileContentTrialFreeTopics = 10), featuresMap = mapOf(FeatureKeys.MOBILE_CONTENT_TRIAL to true), ), - subscription = Subscription( - type = SubscriptionType.MOBILE_CONTENT_TRIAL, - status = SubscriptionStatus.ACTIVE - ), + subscriptionLimitType = SubscriptionLimitType.TOPICS, learnedTopicsCount = 10 ) @@ -362,10 +342,7 @@ class StudyPlanWidgetStateExtensionsTest { featureValues = FeatureValues(mobileContentTrialFreeTopics = 10), featuresMap = mapOf(FeatureKeys.MOBILE_CONTENT_TRIAL to true), ), - subscription = Subscription( - type = SubscriptionType.MOBILE_CONTENT_TRIAL, - status = SubscriptionStatus.ACTIVE - ), + subscriptionLimitType = SubscriptionLimitType.TOPICS, learnedTopicsCount = 5 ) diff --git a/shared/src/commonTest/kotlin/org/hyperskill/study_plan/widget/StudyPlanWidgetTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/study_plan/widget/StudyPlanWidgetTest.kt index ba5e78c649..4c024675d6 100644 --- a/shared/src/commonTest/kotlin/org/hyperskill/study_plan/widget/StudyPlanWidgetTest.kt +++ b/shared/src/commonTest/kotlin/org/hyperskill/study_plan/widget/StudyPlanWidgetTest.kt @@ -29,6 +29,7 @@ import org.hyperskill.app.study_plan.widget.presentation.StudyPlanWidgetReducer import org.hyperskill.app.study_plan.widget.view.mapper.StudyPlanWidgetViewStateMapper import org.hyperskill.app.study_plan.widget.view.model.StudyPlanWidgetViewState import org.hyperskill.app.subscriptions.domain.model.Subscription +import org.hyperskill.app.subscriptions.domain.model.SubscriptionLimitType import org.hyperskill.app.subscriptions.domain.model.SubscriptionStatus import org.hyperskill.app.subscriptions.domain.model.SubscriptionType import org.hyperskill.learning_activities.domain.model.stub @@ -62,7 +63,7 @@ class StudyPlanWidgetTest { learningActivities = emptyList(), studyPlanSections = emptyList(), subscription = Subscription.stub(), - canMakePayments = false, + subscriptionLimitType = SubscriptionLimitType.NONE, learnedTopicsCount = 0 ) ) @@ -90,7 +91,7 @@ class StudyPlanWidgetTest { visibleSection ), subscription = Subscription.stub(), - canMakePayments = false, + subscriptionLimitType = SubscriptionLimitType.NONE, learnedTopicsCount = 0 ) ) @@ -121,7 +122,7 @@ class StudyPlanWidgetTest { learningActivities = listOf(stubLearningActivity(id = 1L)), studyPlanSections = listOf(visibleSection, currentSection), subscription = Subscription.stub(), - canMakePayments = false, + subscriptionLimitType = SubscriptionLimitType.NONE, learnedTopicsCount = 0 ) ) @@ -159,7 +160,7 @@ class StudyPlanWidgetTest { ) ), subscription = subscription, - canMakePayments = false, + subscriptionLimitType = SubscriptionLimitType.NONE, learnedTopicsCount = 0 ) ) @@ -186,7 +187,7 @@ class StudyPlanWidgetTest { studyPlanSectionStub(id = 1, activities = listOf(1)) ), subscription = Subscription.stub(), - canMakePayments = false, + subscriptionLimitType = SubscriptionLimitType.NONE, learnedTopicsCount = 0 ) ) @@ -239,7 +240,7 @@ class StudyPlanWidgetTest { ) }, subscription = Subscription.stub(), - canMakePayments = false, + subscriptionLimitType = SubscriptionLimitType.NONE, learnedTopicsCount = 0 ) ) @@ -264,7 +265,7 @@ class StudyPlanWidgetTest { ), studyPlanSections = listOf(firstSection, secondSection), subscription = Subscription.stub(), - canMakePayments = false, + subscriptionLimitType = SubscriptionLimitType.NONE, learnedTopicsCount = 0 ) )