diff --git a/androidHyperskillApp/fastlane/release-notes.txt b/androidHyperskillApp/fastlane/release-notes.txt index d0a339bc12..9db14fae72 100644 --- a/androidHyperskillApp/fastlane/release-notes.txt +++ b/androidHyperskillApp/fastlane/release-notes.txt @@ -1,3 +1,6 @@ What to Test: -1. Dark mode support for the code editor ALTAPPS-583 -2. Fix passed tracks count calculation ALTAPPS-646 \ No newline at end of file +1. Optimize determine user account on application launch ALTAPPS-532 +2. Daily limits for learners with Freemium ALTAPPS-668 +3. Fix next topic card can't be render if there is no topic to load ALTAPPS-689 +4. Fix app crash after limit is reached bottom sheet (navigate to HomeScreen) ALTAPPS-692 +5. Fix app blocked if no track selected in mobile and selected in web in the same time ALTAPPS-693 \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/injection/AndroidAppComponent.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/injection/AndroidAppComponent.kt index 61378ba5e1..2a35362509 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/injection/AndroidAppComponent.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/injection/AndroidAppComponent.kt @@ -4,6 +4,7 @@ import android.content.Context import org.hyperskill.app.android.code.injection.PlatformCodeEditorComponent import org.hyperskill.app.android.image_loading.injection.ImageLoadingComponent import org.hyperskill.app.android.latex.injection.PlatformLatexComponent +import org.hyperskill.app.android.main.injection.NavigationComponent import org.hyperskill.app.android.notification.injection.PlatformNotificationComponent import org.hyperskill.app.auth.injection.AuthCredentialsComponent import org.hyperskill.app.auth.injection.AuthSocialComponent @@ -20,6 +21,7 @@ import org.hyperskill.app.onboarding.injection.OnboardingComponent import org.hyperskill.app.onboarding.injection.PlatformOnboardingComponent import org.hyperskill.app.placeholder_new_user.injection.PlaceholderNewUserComponent import org.hyperskill.app.placeholder_new_user.injection.PlatformPlaceholderNewUserComponent +import org.hyperskill.app.problems_limit.injection.PlatformProblemsLimitComponent import org.hyperskill.app.profile.injection.PlatformProfileComponent import org.hyperskill.app.profile.injection.ProfileComponent import org.hyperskill.app.profile_settings.injection.PlatformProfileSettingsComponent @@ -41,6 +43,7 @@ interface AndroidAppComponent : AppGraph { val platformMainComponent: PlatformMainComponent val platformNotificationComponent: PlatformNotificationComponent val imageLoadingComponent: ImageLoadingComponent + val navigationComponent: NavigationComponent fun buildPlatformAuthSocialWebViewComponent(): PlatformAuthSocialWebViewComponent fun buildPlatformAuthSocialComponent(authSocialComponent: AuthSocialComponent): PlatformAuthSocialComponent @@ -59,4 +62,6 @@ interface AndroidAppComponent : AppGraph { fun buildPlatformTopicsRepetitionsComponent(): PlatformTopicsRepetitionComponent fun buildPlatformDebugComponent(debugComponent: DebugComponent): PlatformDebugComponent fun buildPlatformStageImplementationComponent(projectId: Long, stageId: Long): PlatformStageImplementationComponent + + fun buildPlatformProblemsLimitComponent(): PlatformProblemsLimitComponent } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/injection/AndroidAppComponentImpl.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/injection/AndroidAppComponentImpl.kt index 4ac39c34a0..f24521f4ca 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/injection/AndroidAppComponentImpl.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/injection/AndroidAppComponentImpl.kt @@ -10,6 +10,8 @@ import org.hyperskill.app.android.image_loading.injection.ImageLoadingComponent import org.hyperskill.app.android.image_loading.injection.ImageLoadingComponentImpl import org.hyperskill.app.android.latex.injection.PlatformLatexComponent import org.hyperskill.app.android.latex.injection.PlatformLatexComponentImpl +import org.hyperskill.app.android.main.injection.NavigationComponent +import org.hyperskill.app.android.main.injection.NavigationComponentImpl import org.hyperskill.app.android.notification.injection.PlatformNotificationComponent import org.hyperskill.app.android.notification.injection.PlatformNotificationComponentImpl import org.hyperskill.app.android.sentry.domain.model.manager.SentryManagerImpl @@ -30,6 +32,8 @@ import org.hyperskill.app.comments.injection.CommentsDataComponentImpl import org.hyperskill.app.core.domain.BuildVariant import org.hyperskill.app.core.injection.CommonComponent import org.hyperskill.app.core.injection.CommonComponentImpl +import org.hyperskill.app.core.injection.StateRepositoriesComponent +import org.hyperskill.app.core.injection.StateRepositoriesComponentImpl import org.hyperskill.app.core.remote.UserAgentInfo import org.hyperskill.app.debug.injection.DebugComponent import org.hyperskill.app.debug.injection.DebugComponentImpl @@ -73,6 +77,8 @@ import org.hyperskill.app.placeholder_new_user.injection.PlaceholderNewUserCompo import org.hyperskill.app.placeholder_new_user.injection.PlaceholderNewUserComponentImpl import org.hyperskill.app.placeholder_new_user.injection.PlatformPlaceholderNewUserComponent import org.hyperskill.app.placeholder_new_user.injection.PlatformPlaceholderNewUserComponentImpl +import org.hyperskill.app.problems_limit.injection.PlatformProblemsLimitComponent +import org.hyperskill.app.problems_limit.injection.PlatformProblemsLimitComponentImpl import org.hyperskill.app.problems_limit.injection.ProblemsLimitComponent import org.hyperskill.app.problems_limit.injection.ProblemsLimitComponentImpl import org.hyperskill.app.products.injection.ProductsDataComponent @@ -132,8 +138,6 @@ import org.hyperskill.app.streaks.injection.StreaksDataComponent import org.hyperskill.app.streaks.injection.StreaksDataComponentImpl import org.hyperskill.app.study_plan.injection.StudyPlanDataComponent import org.hyperskill.app.study_plan.injection.StudyPlanDataComponentImpl -import org.hyperskill.app.subscriptions.injection.SubscriptionsDataComponent -import org.hyperskill.app.subscriptions.injection.SubscriptionsDataComponentImpl import org.hyperskill.app.topics.injection.TopicsDataComponent import org.hyperskill.app.topics.injection.TopicsDataComponentImpl import org.hyperskill.app.topics_repetitions.injection.PlatformTopicsRepetitionComponent @@ -187,6 +191,9 @@ class AndroidAppComponentImpl( override val authComponent: AuthComponent = AuthComponentImpl(this) + override val navigationComponent: NavigationComponent = + NavigationComponentImpl() + override val profileHypercoinsDataComponent: ProfileHypercoinsDataComponent = ProfileHypercoinsDataComponentImpl() @@ -205,8 +212,8 @@ class AndroidAppComponentImpl( override val notificationFlowDataComponent: NotificationFlowDataComponent = NotificationFlowDataComponentImpl() - override val subscriptionsDataComponent: SubscriptionsDataComponent = - SubscriptionsDataComponentImpl(this) + override val stateRepositoriesComponent: StateRepositoriesComponent = + StateRepositoriesComponentImpl(this) override val sentryComponent: SentryComponent = SentryComponentImpl(SentryManagerImpl(commonComponent.buildKonfig)) @@ -414,6 +421,15 @@ class AndroidAppComponentImpl( override fun buildTopicsToDiscoverNextDataComponent(): TopicsToDiscoverNextDataComponent = TopicsToDiscoverNextDataComponentImpl(this) + /** + * ProblemsLimit component + */ + override fun buildProblemsLimitComponent(): ProblemsLimitComponent = + ProblemsLimitComponentImpl(this) + + override fun buildPlatformProblemsLimitComponent(): PlatformProblemsLimitComponent = + PlatformProblemsLimitComponentImpl(problemsLimitComponent = buildProblemsLimitComponent()) + override fun buildUserStorageComponent(): UserStorageComponent = UserStorageComponentImpl(this) @@ -464,7 +480,4 @@ class AndroidAppComponentImpl( override fun buildFreemiumDataComponent(): FreemiumDataComponent = FreemiumDataComponentImpl(this) - - override fun buildProblemsLimitComponent(): ProblemsLimitComponent = - ProblemsLimitComponentImpl(this) } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/navigation/MainNavigationContainer.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/navigation/MainNavigationContainer.kt deleted file mode 100644 index 1e3fbf61f4..0000000000 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/navigation/MainNavigationContainer.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.hyperskill.app.android.core.view.ui.navigation - -import ru.nobird.android.view.navigation.router.RetainedRouter - -interface MainNavigationContainer { - val router: RetainedRouter - - companion object { - const val ContainerTag: String = "MainNavigationContainer" - } -} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/navigation/NavigationExtensions.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/navigation/NavigationExtensions.kt index 5e194fe668..4d26bb9631 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/navigation/NavigationExtensions.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/navigation/NavigationExtensions.kt @@ -1,13 +1,9 @@ package org.hyperskill.app.android.core.view.ui.navigation import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentManager import com.github.terrakok.cicerone.Router import org.hyperskill.app.android.core.view.ui.fragment.parentOfType -import ru.nobird.android.view.navigation.router.RetainedRouter import ru.nobird.android.view.navigation.ui.fragment.NavigationContainer -import ru.nobird.app.core.model.safeCast -import org.hyperskill.app.android.main.view.ui.navigation.MainScreen fun Fragment.requireAppRouter(): Router = requireNotNull(parentOfType(AppNavigationContainer::class.java)?.router) { @@ -18,19 +14,3 @@ fun Fragment.requireRouter(): Router = requireNotNull(parentOfType(NavigationContainer::class.java)?.router) { "Fragment $this not attached to a NavigationContainer." } - -/** - * Is used to retrieve router from [MainScreen]s child fragment - * */ -fun Fragment.requireMainRouter(): RetainedRouter = - requireNotNull(parentOfType(MainNavigationContainer::class.java)?.router) { - "Fragment $this not attached to a MainNavigationContainer." - } - -/** - * I used to retrieve router from non [MainScreen]s child fragment. - * */ -fun FragmentManager.requireMainRouter(): RetainedRouter = - requireNotNull(findFragmentByTag(MainNavigationContainer.ContainerTag)?.safeCast()?.router) { - "FragmentManger $this does not contain Fragment implemented MainNavigationContainer" - } 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 ee8a9ac052..286a179903 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 @@ -16,11 +16,13 @@ import org.hyperskill.app.android.R import org.hyperskill.app.android.core.extensions.openUrl import org.hyperskill.app.android.core.view.ui.dialog.LoadingProgressDialogFragment import org.hyperskill.app.android.core.view.ui.dialog.dismissDialogFragmentIfExists -import org.hyperskill.app.android.core.view.ui.navigation.requireMainRouter +import org.hyperskill.app.android.core.view.ui.fragment.setChildFragment import org.hyperskill.app.android.core.view.ui.navigation.requireRouter import org.hyperskill.app.android.databinding.FragmentHomeBinding import org.hyperskill.app.android.gamification_toolbar.view.ui.delegate.GamificationToolbarDelegate +import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter import org.hyperskill.app.android.problem_of_day.view.delegate.ProblemOfDayCardFormDelegate +import org.hyperskill.app.android.problems_limit.fragment.ProblemsLimitFragment import org.hyperskill.app.android.profile.view.navigation.ProfileScreen import org.hyperskill.app.android.step.view.screen.StepScreen import org.hyperskill.app.android.topics.view.delegate.TopicsToDiscoverNextDelegate @@ -82,6 +84,9 @@ class HomeFragment : } } + private val mainScreenRouter: MainScreenRouter = + HyperskillApp.graph().navigationComponent.mainScreenCicerone.router + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) injectComponents() @@ -90,6 +95,7 @@ class HomeFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + setProblemsLimitFragment() initViewStateDelegate() initGamificationToolbarDelegate() problemOfDayCardFormDelegate.setup(viewBinding.homeScreenProblemOfDayCard) @@ -114,10 +120,14 @@ class HomeFragment : homeViewModel.onNewMessage(HomeFeature.Message.ViewedEventMessage) } + override fun onDestroyView() { + super.onDestroyView() + gamificationToolbarDelegate = null + } + override fun onDestroy() { super.onDestroy() requireActivity().lifecycle.removeObserver(onForegroundObserver) - gamificationToolbarDelegate = null } private fun injectComponents() { @@ -147,7 +157,8 @@ class HomeFragment : viewBinding.homeScreenKeepPracticingTextView, viewBinding.homeScreenProblemOfDayCard.root, viewBinding.homeScreenTopicsRepetitionCard.root, - viewBinding.homeScreenKeepLearningInWebButton + viewBinding.homeScreenKeepLearningInWebButton, + viewBinding.homeProblemsLimit ) } } @@ -178,7 +189,7 @@ class HomeFragment : is HomeFeature.Action.ViewAction.GamificationToolbarViewAction -> when (action.viewAction) { is GamificationToolbarFeature.Action.ViewAction.ShowProfileTab -> - requireMainRouter().switch(ProfileScreen(isInitCurrent = true)) + mainScreenRouter.switch(ProfileScreen(isInitCurrent = true)) } is HomeFeature.Action.ViewAction.TopicsToDiscoverNextViewAction -> { when (action.viewAction) { @@ -201,8 +212,8 @@ class HomeFragment : val homeState = state.homeState if (homeState is HomeFeature.HomeState.Content) { renderMagicLinkState(homeState.isLoadingMagicLink) - renderProblemOfDay(viewBinding, homeState.problemOfDayState) - renderTopicsRepetition(homeState.repetitionsState) + renderProblemOfDay(viewBinding, homeState.problemOfDayState, homeState.isFreemiumEnabled) + renderTopicsRepetition(homeState.repetitionsState, homeState.isFreemiumEnabled) } gamificationToolbarDelegate?.render(state.toolbarState) @@ -219,20 +230,33 @@ class HomeFragment : } } - private fun renderProblemOfDay(viewBinding: FragmentHomeBinding, state: HomeFeature.ProblemOfDayState) { - problemOfDayCardFormDelegate.render(requireContext(), viewBinding.homeScreenProblemOfDayCard, state) + private fun renderProblemOfDay( + viewBinding: FragmentHomeBinding, + state: HomeFeature.ProblemOfDayState, + isFreemiumEnabled: Boolean + ) { + problemOfDayCardFormDelegate.render( + context = requireContext(), + binding = viewBinding.homeScreenProblemOfDayCard, + state = state, + isFreemiumEnabled = isFreemiumEnabled + ) viewBinding.homeScreenKeepLearningInWebButton.isVisible = state is HomeFeature.ProblemOfDayState.Solved || state is HomeFeature.ProblemOfDayState.Empty } - private fun renderTopicsRepetition(repetitionsState: HomeFeature.RepetitionsState) { + private fun renderTopicsRepetition( + repetitionsState: HomeFeature.RepetitionsState, + isFreemiumEnabled: Boolean + ) { viewBinding.homeScreenTopicsRepetitionCard.root.isVisible = repetitionsState is HomeFeature.RepetitionsState.Available if (repetitionsState is HomeFeature.RepetitionsState.Available) { topicsRepetitionDelegate.render( - requireContext(), - viewBinding.homeScreenTopicsRepetitionCard, - repetitionsState.recommendedRepetitionsCount + context = requireContext(), + binding = viewBinding.homeScreenTopicsRepetitionCard, + recommendedRepetitionsCount = repetitionsState.recommendedRepetitionsCount, + isFreemiumEnabled = isFreemiumEnabled ) } } @@ -247,4 +271,10 @@ class HomeFragment : } topicsToDiscoverNextDelegate.render(state) } + + private fun setProblemsLimitFragment() { + setChildFragment(R.id.homeProblemsLimit, ProblemsLimitFragment.TAG) { + ProblemsLimitFragment.newInstance() + } + } } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/injection/NavigationComponent.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/injection/NavigationComponent.kt new file mode 100644 index 0000000000..5ac537697b --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/injection/NavigationComponent.kt @@ -0,0 +1,8 @@ +package org.hyperskill.app.android.main.injection + +import com.github.terrakok.cicerone.Cicerone +import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter + +interface NavigationComponent { + val mainScreenCicerone: Cicerone +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/injection/NavigationComponentImpl.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/injection/NavigationComponentImpl.kt new file mode 100644 index 0000000000..b653a66321 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/injection/NavigationComponentImpl.kt @@ -0,0 +1,11 @@ +package org.hyperskill.app.android.main.injection + +import com.github.terrakok.cicerone.Cicerone +import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter + +class NavigationComponentImpl : NavigationComponent { + + override val mainScreenCicerone: Cicerone by lazy { + Cicerone.create(MainScreenRouter()) + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/BackToForegroundObserver.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/BackToForegroundObserver.kt new file mode 100644 index 0000000000..c1b3659145 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/BackToForegroundObserver.kt @@ -0,0 +1,26 @@ +package org.hyperskill.app.android.main.view.ui.activity + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner + +class BackToForegroundObserver( + private val onBackToForeground: () -> Unit +) : DefaultLifecycleObserver { + + private var hasBeenStopped: Boolean = false + + override fun onStart(owner: LifecycleOwner) { + if (hasBeenStopped) { + onBackToForeground() + hasBeenStopped = false + } + } + + override fun onStop(owner: LifecycleOwner) { + hasBeenStopped = true + } + + override fun onDestroy(owner: LifecycleOwner) { + owner.lifecycle.removeObserver(this) + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/fragment/MainFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/fragment/MainFragment.kt index 18c49c27b2..040461c279 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/fragment/MainFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/fragment/MainFragment.kt @@ -8,10 +8,10 @@ import com.github.terrakok.cicerone.Cicerone import org.hyperskill.app.analytic.domain.model.Analytic import org.hyperskill.app.android.HyperskillApp import org.hyperskill.app.android.R -import org.hyperskill.app.android.core.view.ui.navigation.MainNavigationContainer import org.hyperskill.app.android.databinding.FragmentMainBinding import org.hyperskill.app.android.debug.DebugScreen import org.hyperskill.app.android.home.view.ui.screen.HomeScreen +import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter import org.hyperskill.app.android.main.view.ui.navigation.Tabs import org.hyperskill.app.android.profile.view.navigation.ProfileScreen import org.hyperskill.app.android.track.view.navigation.TrackScreen @@ -19,10 +19,9 @@ import org.hyperskill.app.config.BuildKonfig import org.hyperskill.app.debug.presentation.DebugFeature import org.hyperskill.app.main.domain.analytic.AppClickedBottomNavigationItemHyperskillAnalyticEvent import ru.nobird.android.view.navigation.navigator.RetainedAppNavigator -import ru.nobird.android.view.navigation.router.RetainedRouter import ru.nobird.android.view.navigation.ui.fragment.addBackNavigationDelegate -class MainFragment : Fragment(R.layout.fragment_main), MainNavigationContainer { +class MainFragment : Fragment(R.layout.fragment_main) { companion object { fun newInstance(): Fragment = MainFragment() @@ -30,8 +29,8 @@ class MainFragment : Fragment(R.layout.fragment_main), MainNavigationContainer { private val viewBinding: FragmentMainBinding by viewBinding(FragmentMainBinding::bind) - private val localCicerone: Cicerone = Cicerone.create(RetainedRouter()) - override val router: RetainedRouter = localCicerone.router + private val localCicerone: Cicerone = + HyperskillApp.graph().navigationComponent.mainScreenCicerone private val navigator by lazy(LazyThreadSafetyMode.NONE) { RetainedAppNavigator( @@ -74,7 +73,7 @@ class MainFragment : Fragment(R.layout.fragment_main), MainNavigationContainer { super.onViewCreated(view, savedInstanceState) if (savedInstanceState == null && childFragmentManager.fragments.isEmpty()) { - router.switch(HomeScreen) + localCicerone.router.switch(HomeScreen) } viewBinding.mainBottomNavigation.menu.findItem(R.id.debug_tab).isVisible = @@ -93,16 +92,16 @@ class MainFragment : Fragment(R.layout.fragment_main), MainNavigationContainer { when (item.itemId) { R.id.home_tab -> { - router.switch(HomeScreen) + localCicerone.router.switch(HomeScreen) } R.id.track_tab -> { - router.switch(TrackScreen) + localCicerone.router.switch(TrackScreen) } R.id.profile_tab -> { - router.switch(ProfileScreen(isInitCurrent = true)) + localCicerone.router.switch(ProfileScreen(isInitCurrent = true)) } R.id.debug_tab -> { - router.switch(DebugScreen) + localCicerone.router.switch(DebugScreen) } } return@setOnItemSelectedListener true diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/navigation/MainScreen.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/navigation/MainScreen.kt index 15bf982b66..f6312206a7 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/navigation/MainScreen.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/navigation/MainScreen.kt @@ -3,13 +3,9 @@ package org.hyperskill.app.android.main.view.ui.navigation import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentFactory import com.github.terrakok.cicerone.androidx.FragmentScreen -import org.hyperskill.app.android.core.view.ui.navigation.MainNavigationContainer import org.hyperskill.app.android.main.view.ui.fragment.MainFragment object MainScreen : FragmentScreen { override fun createFragment(factory: FragmentFactory): Fragment = MainFragment.newInstance() - - override val screenKey: String - get() = MainNavigationContainer.ContainerTag } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/navigation/MainScreenRouter.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/navigation/MainScreenRouter.kt new file mode 100644 index 0000000000..200f162d0f --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/navigation/MainScreenRouter.kt @@ -0,0 +1,5 @@ +package org.hyperskill.app.android.main.view.ui.navigation + +import ru.nobird.android.view.navigation.router.RetainedRouter + +typealias MainScreenRouter = RetainedRouter \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problem_of_day/view/delegate/ProblemOfDayCardFormDelegate.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problem_of_day/view/delegate/ProblemOfDayCardFormDelegate.kt index f2b992d7f2..e559e34b72 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problem_of_day/view/delegate/ProblemOfDayCardFormDelegate.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problem_of_day/view/delegate/ProblemOfDayCardFormDelegate.kt @@ -24,7 +24,8 @@ class ProblemOfDayCardFormDelegate( fun render( context: Context, binding: LayoutProblemOfTheDayCardBinding, - state: HomeFeature.ProblemOfDayState + state: HomeFeature.ProblemOfDayState, + isFreemiumEnabled: Boolean ) { with(binding) { when (state) { @@ -110,12 +111,13 @@ class ProblemOfDayCardFormDelegate( } } } - renderFooter(binding, state) + renderFooter(binding, state, isFreemiumEnabled) } private fun renderFooter( binding: LayoutProblemOfTheDayCardBinding, - state: HomeFeature.ProblemOfDayState + state: HomeFeature.ProblemOfDayState, + isFreemiumEnabled: Boolean ) { val needToRefresh = when (state) { HomeFeature.ProblemOfDayState.Empty -> false @@ -139,5 +141,10 @@ class ProblemOfDayCardFormDelegate( problemOfDayNextProblemInCounterView.text = nextProblemIn } } + binding.problemOfDayFreemiumBadge.isVisible = when (state) { + is HomeFeature.ProblemOfDayState.NeedToSolve -> isFreemiumEnabled + HomeFeature.ProblemOfDayState.Empty, + is HomeFeature.ProblemOfDayState.Solved -> false + } } } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problems_limit/dialog/ProblemsLimitReachedBottomSheet.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problems_limit/dialog/ProblemsLimitReachedBottomSheet.kt new file mode 100644 index 0000000000..a1d90a102e --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problems_limit/dialog/ProblemsLimitReachedBottomSheet.kt @@ -0,0 +1,72 @@ +package org.hyperskill.app.android.problems_limit.dialog + +import android.app.Dialog +import android.content.DialogInterface +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import by.kirich1409.viewbindingdelegate.viewBinding +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.hyperskill.app.android.R +import org.hyperskill.app.android.databinding.FragmentProblemsLimitReachedBinding +import org.hyperskill.app.android.view.base.ui.extension.wrapWithTheme +import org.hyperskill.app.step_quiz.presentation.StepQuizFeature +import org.hyperskill.app.step_quiz.presentation.StepQuizViewModel + +class ProblemsLimitReachedBottomSheet : BottomSheetDialogFragment() { + + companion object { + + const val TAG = "ProblemsLimitReachedBottomSheet" + + fun newInstance(): ProblemsLimitReachedBottomSheet = + ProblemsLimitReachedBottomSheet() + } + + private val viewBinding: FragmentProblemsLimitReachedBinding by viewBinding(FragmentProblemsLimitReachedBinding::bind) + + // View model should be created in parent fragment + private val viewModel: StepQuizViewModel by viewModels(ownerProducer = ::requireParentFragment) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.TopCornersRoundedBottomSheetDialog) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog = + BottomSheetDialog(requireContext(), theme).also { dialog -> + dialog.setOnShowListener { + dialog.behavior.state = BottomSheetBehavior.STATE_EXPANDED + if (savedInstanceState == null) { + viewModel.onNewMessage(StepQuizFeature.Message.ProblemsLimitReachedModalShownEventMessage) + } + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? = + inflater.wrapWithTheme(requireActivity()) + .inflate( + R.layout.fragment_problems_limit_reached, + container, + false + ) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewBinding.problemsLimitReachedHomeButton.setOnClickListener { + viewModel.onNewMessage(StepQuizFeature.Message.ProblemsLimitReachedModalGoToHomeScreenClicked) + } + } + + override fun onDismiss(dialog: DialogInterface) { + super.onDismiss(dialog) + viewModel.onNewMessage(StepQuizFeature.Message.ProblemsLimitReachedModalHiddenEventMessage) + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problems_limit/fragment/ProblemsLimitFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problems_limit/fragment/ProblemsLimitFragment.kt new file mode 100644 index 0000000000..9056692a5a --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problems_limit/fragment/ProblemsLimitFragment.kt @@ -0,0 +1,130 @@ +package org.hyperskill.app.android.problems_limit.fragment + +import android.os.Bundle +import android.view.View +import android.view.ViewGroup.MarginLayoutParams +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.fragment.app.Fragment +import by.kirich1409.viewbindingdelegate.viewBinding +import org.hyperskill.app.android.HyperskillApp +import org.hyperskill.app.android.R +import org.hyperskill.app.android.databinding.FragmentProblemsLimitBinding +import org.hyperskill.app.core.injection.ReduxViewModelFactory +import org.hyperskill.app.problems_limit.presentation.ProblemsLimitFeature +import org.hyperskill.app.problems_limit.presentation.ProblemsLimitViewModel +import ru.nobird.android.view.base.ui.delegate.ViewStateDelegate +import ru.nobird.android.view.base.ui.extension.argument +import ru.nobird.android.view.base.ui.extension.setTextIfChanged +import ru.nobird.android.view.redux.ui.extension.reduxViewModel +import ru.nobird.app.presentation.redux.container.ReduxView + +class ProblemsLimitFragment : + Fragment(R.layout.fragment_problems_limit), + ReduxView { + + companion object { + + const val TAG = "ProblemsLimitFragment" + + fun newInstance(isDividerVisible: Boolean = false): ProblemsLimitFragment = + ProblemsLimitFragment().apply { + this.isDividerVisible = isDividerVisible + } + } + + private val viewBinding: FragmentProblemsLimitBinding by viewBinding(FragmentProblemsLimitBinding::bind) + + private lateinit var viewModelFactory: ReduxViewModelFactory + private val problemsLimitViewModel: ProblemsLimitViewModel by reduxViewModel(this) { viewModelFactory } + + private var viewStateDelegate: ViewStateDelegate? = null + + private var isDividerVisible: Boolean by argument() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + injectComponent() + } + + private fun injectComponent() { + val problemsLimitComponent = HyperskillApp.graph().buildPlatformProblemsLimitComponent() + viewModelFactory = problemsLimitComponent.reduxViewModelFactory + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + viewBinding.problemsLimitDivider.root.isVisible = isDividerVisible + viewBinding.problemsLimitsContent.updateLayoutParams { + topMargin = if (isDividerVisible) { + resources.getDimensionPixelOffset(R.dimen.problems_limit_diver_margin) + } else { + 0 + } + } + viewStateDelegate = ViewStateDelegate().apply { + addState() + addState() + + addState( + viewBinding.problemsLimitSkeleton + ) + if (isDividerVisible) { + addState( + viewBinding.problemsLimitDivider.root, + viewBinding.problemsLimitsContent + ) + } else { + addState( + viewBinding.problemsLimitsContent + ) + } + + if (isDividerVisible) { + addState( + viewBinding.problemsLimitDivider.root, + viewBinding.problemsLimitRetryButton + ) + } else { + addState( + viewBinding.problemsLimitRetryButton + ) + } + } + + viewBinding.problemsLimitRetryButton.setOnClickListener { + problemsLimitViewModel.onNewMessage( + ProblemsLimitFeature.Message.Initialize(forceUpdate = true) + ) + } + } + + override fun onDestroyView() { + viewStateDelegate = null + super.onDestroyView() + } + + override fun render(state: ProblemsLimitFeature.ViewState) { + viewStateDelegate?.switchState(state) + when (state) { + is ProblemsLimitFeature.ViewState.Content.Widget -> { + viewBinding.problemsLimitsDots.setData( + totalCount = state.stepsLimitTotal, + activeCount = state.stepsLimitLeft + ) + viewBinding.problemsLimitCount.setTextIfChanged(state.stepsLimitLabel) + viewBinding.problemsLimitUpdatedIn.setTextIfChanged(state.updateInLabel) + } + else -> { + // no op + } + } + } + + override fun onAction(action: ProblemsLimitFeature.Action.ViewAction) { + when (action) { + else -> { + // no op + } + } + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problems_limit/view/widget/ProblemsLimitView.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problems_limit/view/widget/ProblemsLimitView.kt new file mode 100644 index 0000000000..f552412079 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/problems_limit/view/widget/ProblemsLimitView.kt @@ -0,0 +1,84 @@ +package org.hyperskill.app.android.problems_limit.view.widget + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.graphics.withTranslation +import ru.nobird.android.view.base.ui.extension.Dp +import ru.nobird.android.view.base.ui.extension.toPx +import kotlin.math.roundToInt + +class ProblemsLimitView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + companion object { + private const val DEFAULT_CIRCLES_COUNT = 5 + } + + private val circleRadius = Dp(4f).toPx().value + private val circlesMargin = Dp(4f).toPx().value + + private val activeCirclePaint: Paint = Paint().apply { + isAntiAlias = true + color = ContextCompat.getColor(context, org.hyperskill.app.R.color.color_overlay_violet) + style = Paint.Style.FILL + } + + private val inactiveCirclePaint: Paint = Paint().apply { + isAntiAlias = true + color = ContextCompat.getColor(context, org.hyperskill.app.R.color.color_on_surface_alpha_12) + style = Paint.Style.FILL + } + + private var totalCount = DEFAULT_CIRCLES_COUNT + private var activeCount = 2 + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val circleDiameter = circleRadius * 2 + + val desiredWidth = (circleDiameter * totalCount + circlesMargin * (totalCount - 1)).roundToInt() + val widthMode = MeasureSpec.getMode(widthMeasureSpec) + val widthSize = MeasureSpec.getSize(widthMeasureSpec) + val width: Int = when (widthMode) { + MeasureSpec.EXACTLY -> widthSize + MeasureSpec.AT_MOST -> minOf(desiredWidth, widthSize) + else -> desiredWidth + } + + val desiredHeight = circleDiameter.roundToInt() + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val heightSize = MeasureSpec.getSize(heightMeasureSpec) + val height: Int = when (heightMode) { + MeasureSpec.EXACTLY -> heightSize + MeasureSpec.AT_MOST -> minOf(desiredHeight, heightSize) + else -> desiredHeight + } + + setMeasuredDimension(width, height) + } + + override fun onDraw(canvas: Canvas) { + val circleDiameter = circleRadius * 2 + for (i in 0 until totalCount) { + canvas.withTranslation(x = i * (circleDiameter + circlesMargin)) { + val paint = if (i <= activeCount - 1) activeCirclePaint else inactiveCirclePaint + drawCircle(circleRadius, circleRadius, circleRadius, paint) + } + } + } + + fun setData(totalCount: Int, activeCount: Int) { + if (this.totalCount == totalCount && this.activeCount == activeCount) { + return + } + this.totalCount = totalCount + this.activeCount = activeCount + invalidate() + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/ProfileFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/ProfileFragment.kt index c464031624..29c51619c1 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/ProfileFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/profile/view/fragment/ProfileFragment.kt @@ -19,9 +19,9 @@ import org.hyperskill.app.android.core.extensions.isChannelNotificationsEnabled import org.hyperskill.app.android.core.extensions.openUrl import org.hyperskill.app.android.core.view.ui.dialog.LoadingProgressDialogFragment import org.hyperskill.app.android.core.view.ui.dialog.dismissDialogFragmentIfExists -import org.hyperskill.app.android.core.view.ui.navigation.requireMainRouter import org.hyperskill.app.android.databinding.FragmentProfileBinding import org.hyperskill.app.android.home.view.ui.screen.HomeScreen +import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter import org.hyperskill.app.android.notification.model.HyperskillNotificationChannel import org.hyperskill.app.android.profile.view.delegate.StreakCardFormDelegate import org.hyperskill.app.android.profile.view.dialog.StreakFreezeDialogFragment @@ -75,6 +75,9 @@ class ProfileFragment : NotificationManagerCompat.from(requireContext()) } + private val mainScreenRouter: MainScreenRouter = + HyperskillApp.graph().navigationComponent.mainScreenCicerone.router + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) injectComponents() @@ -231,7 +234,7 @@ class ProfileFragment : viewBinding.root.snackbar(org.hyperskill.app.R.string.streak_freeze_bought_error) ProfileFeature.Action.ViewAction.NavigateTo.HomeScreen -> { - requireMainRouter().switch(HomeScreen) + mainScreenRouter.switch(HomeScreen) } } } @@ -316,12 +319,11 @@ class ProfileFragment : } if (profile.languages?.isEmpty() == false) { + val languages = profile.languages!!.joinToString(", ") { + Locale(it).getDisplayLanguage(Locale.ENGLISH) + } profileAboutSpeaksTextView.text = - "${resources.getString(org.hyperskill.app.R.string.profile_speaks_text)} ${ - profile.languages!!.joinToString(", ") { - Locale(it).getDisplayLanguage(Locale.ENGLISH) - } - }" + "${resources.getString(org.hyperskill.app.R.string.profile_speaks_text)} $languages" } else { profileAboutSpeaksTextView.visibility = View.GONE } diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/view/dialog/UnsupportedStageBottomSheet.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/view/dialog/UnsupportedStageBottomSheet.kt index d76cd07b0b..9b1bf616b2 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/view/dialog/UnsupportedStageBottomSheet.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/view/dialog/UnsupportedStageBottomSheet.kt @@ -1,20 +1,18 @@ package org.hyperskill.app.android.stage_implementation.view.dialog import android.app.Dialog -import android.content.Context import android.content.DialogInterface import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.StyleRes -import androidx.appcompat.view.ContextThemeWrapper import by.kirich1409.viewbindingdelegate.viewBinding import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.hyperskill.app.android.R import org.hyperskill.app.android.databinding.FragmentUnsupportedStageBinding +import org.hyperskill.app.android.view.base.ui.extension.wrapWithTheme class UnsupportedStageBottomSheet : BottomSheetDialogFragment() { @@ -76,10 +74,4 @@ class UnsupportedStageBottomSheet : BottomSheetDialogFragment() { fun onHomeClick() } -} - -fun LayoutInflater.wrapWithTheme( - context: Context, - @StyleRes themeRes: Int = R.style.AppTheme -): LayoutInflater = - cloneInContext(ContextThemeWrapper(context, themeRes)) \ No newline at end of file +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/view/fragment/StageStepWrapperFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/view/fragment/StageStepWrapperFragment.kt index 851515ee54..f60b78fb7c 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/view/fragment/StageStepWrapperFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/stage_implementation/view/fragment/StageStepWrapperFragment.kt @@ -11,6 +11,7 @@ import org.hyperskill.app.android.core.extensions.argument import org.hyperskill.app.android.core.view.ui.fragment.setChildFragment import org.hyperskill.app.android.core.view.ui.navigation.requireRouter import org.hyperskill.app.android.databinding.FragmentStageStepWrapperBinding +import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter import org.hyperskill.app.android.step.view.delegate.StepDelegate import org.hyperskill.app.android.step.view.fragment.StepFragment import org.hyperskill.app.android.step.view.model.StepCompletionHost @@ -69,6 +70,9 @@ class StageStepWrapperFragment : private var viewStateDelegate: ViewStateDelegate? = null + private val mainScreenRouter: MainScreenRouter = + HyperskillApp.graph().navigationComponent.mainScreenCicerone.router + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) injectComponent() @@ -129,7 +133,11 @@ class StageStepWrapperFragment : } override fun onAction(action: StepFeature.Action.ViewAction) { - StepDelegate.onAction(fragment = this, action = action) + StepDelegate.onAction( + fragment = this, + mainScreenRouter = mainScreenRouter, + action = action + ) } override fun onNewMessage(message: StepCompletionFeature.Message) { diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt index 5b08fdb709..3f442fb060 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/delegate/StepDelegate.kt @@ -1,11 +1,11 @@ package org.hyperskill.app.android.step.view.delegate import androidx.fragment.app.Fragment -import org.hyperskill.app.android.core.view.ui.navigation.requireMainRouter import org.hyperskill.app.android.core.view.ui.navigation.requireRouter import org.hyperskill.app.android.databinding.ErrorNoConnectionWithButtonBinding import org.hyperskill.app.android.home.view.ui.screen.HomeScreen import org.hyperskill.app.android.main.view.ui.navigation.MainScreen +import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter import org.hyperskill.app.android.step.view.dialog.TopicPracticeCompletedBottomSheet import org.hyperskill.app.android.step.view.screen.StepScreen import org.hyperskill.app.android.view.base.ui.extension.snackbar @@ -21,7 +21,11 @@ object StepDelegate { } } - fun onAction(fragment: Fragment, action: StepFeature.Action.ViewAction) { + fun onAction( + fragment: Fragment, + mainScreenRouter: MainScreenRouter, + action: StepFeature.Action.ViewAction + ) { when (action) { is StepFeature.Action.ViewAction.StepCompletionViewAction -> { when (val stepCompletionAction = action.viewAction) { @@ -31,7 +35,7 @@ object StepDelegate { StepCompletionFeature.Action.ViewAction.NavigateTo.HomeScreen -> { fragment.requireRouter().backTo(MainScreen) - fragment.parentFragmentManager.requireMainRouter().switch(HomeScreen) + mainScreenRouter.switch(HomeScreen) } is StepCompletionFeature.Action.ViewAction.ReloadStep -> { diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/fragment/StepFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/fragment/StepFragment.kt index c7f63aa6a9..e40704a7d5 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/fragment/StepFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step/view/fragment/StepFragment.kt @@ -10,6 +10,7 @@ import org.hyperskill.app.android.R import org.hyperskill.app.android.core.extensions.argument import org.hyperskill.app.android.core.view.ui.fragment.setChildFragment import org.hyperskill.app.android.databinding.FragmentStepBinding +import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter import org.hyperskill.app.android.step.view.delegate.StepDelegate import org.hyperskill.app.android.step.view.model.StepCompletionHost import org.hyperskill.app.android.step.view.model.StepCompletionView @@ -46,6 +47,9 @@ class StepFragment : private var viewStateDelegate: ViewStateDelegate? = null private var stepRoute: StepRoute by argument(serializer = StepRoute.serializer()) + private val mainScreenRouter: MainScreenRouter = + HyperskillApp.graph().navigationComponent.mainScreenCicerone.router + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) injectComponent() @@ -72,7 +76,11 @@ class StepFragment : } override fun onAction(action: StepFeature.Action.ViewAction) { - StepDelegate.onAction(fragment = this, action) + StepDelegate.onAction( + fragment = this, + mainScreenRouter = mainScreenRouter, + action = action + ) } override fun render(state: StepFeature.State) { diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz/view/factory/StepQuizViewStateDelegateFactory.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz/view/factory/StepQuizViewStateDelegateFactory.kt index 66abfd34b8..756c8371b8 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz/view/factory/StepQuizViewStateDelegateFactory.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz/view/factory/StepQuizViewStateDelegateFactory.kt @@ -29,6 +29,7 @@ object StepQuizViewStateDelegateFactory { descriptionBinding.stepQuizDescription, fragmentStepQuizBinding.stepQuizButtons.stepQuizSubmitButton, fragmentStepQuizBinding.stepQuizStatistics, + fragmentStepQuizBinding.stepQuizProblemsLimit, *quizViews ) addState(fragmentStepQuizBinding.stepQuizNetworkError.root) diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz/view/fragment/DefaultStepQuizFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz/view/fragment/DefaultStepQuizFragment.kt index 0a4a1ddb83..71a90becf9 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz/view/fragment/DefaultStepQuizFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/step_quiz/view/fragment/DefaultStepQuizFragment.kt @@ -23,7 +23,12 @@ import org.hyperskill.app.android.core.view.ui.fragment.setChildFragment import org.hyperskill.app.android.core.view.ui.navigation.requireRouter import org.hyperskill.app.android.databinding.FragmentStepQuizBinding import org.hyperskill.app.android.databinding.LayoutStepQuizDescriptionBinding +import org.hyperskill.app.android.home.view.ui.screen.HomeScreen +import org.hyperskill.app.android.main.view.ui.navigation.MainScreen +import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter import org.hyperskill.app.android.notification.model.HyperskillNotificationChannel +import org.hyperskill.app.android.problems_limit.dialog.ProblemsLimitReachedBottomSheet +import org.hyperskill.app.android.problems_limit.fragment.ProblemsLimitFragment import org.hyperskill.app.android.step.view.model.StepCompletionHost import org.hyperskill.app.android.step.view.model.StepCompletionView import org.hyperskill.app.android.step_quiz.view.delegate.StepQuizFeedbackBlocksDelegate @@ -84,6 +89,9 @@ abstract class DefaultStepQuizFragment : private val platformNotificationComponent = HyperskillApp.graph().platformNotificationComponent + private val mainScreenRouter: MainScreenRouter = + HyperskillApp.graph().navigationComponent.mainScreenCicerone.router + protected abstract val quizViews: Array protected abstract val skeletonView: View protected abstract val descriptionBinding: LayoutStepQuizDescriptionBinding @@ -224,7 +232,10 @@ abstract class DefaultStepQuizFragment : is StepQuizFeature.Action.ViewAction.NavigateTo.Back -> { requireRouter().exit() } - is StepQuizFeature.Action.ViewAction.NavigateTo.Home -> TODO() + is StepQuizFeature.Action.ViewAction.NavigateTo.Home -> { + requireRouter().backTo(MainScreen) + mainScreenRouter.switch(HomeScreen) + } is StepQuizFeature.Action.ViewAction.RequestUserPermission -> { when (action.userPermissionRequest) { StepQuizUserPermissionRequest.RESET_CODE -> { @@ -240,7 +251,10 @@ abstract class DefaultStepQuizFragment : .newInstance(earnedGemsText = action.earnedGemsText) .showIfNotExists(childFragmentManager, CompletedStepOfTheDayDialogFragment.TAG) } - StepQuizFeature.Action.ViewAction.ShowProblemsLimitReachedModal -> TODO() + StepQuizFeature.Action.ViewAction.ShowProblemsLimitReachedModal -> { + ProblemsLimitReachedBottomSheet.newInstance() + .showIfNotExists(childFragmentManager, ProblemsLimitReachedBottomSheet.TAG) + } } } @@ -323,6 +337,7 @@ abstract class DefaultStepQuizFragment : } is StepQuizFeature.State.AttemptLoaded -> { setStepHintsFragment(step) + setProblemsLimitFragment(stepRoute) renderAttemptLoaded(state) } else -> { @@ -390,6 +405,21 @@ abstract class DefaultStepQuizFragment : } } + private fun setProblemsLimitFragment(stepRoute: StepRoute) { + when (stepRoute) { + is StepRoute.Learn -> { + setChildFragment(R.id.stepQuizProblemsLimit, ProblemsLimitFragment.TAG) { + ProblemsLimitFragment.newInstance(isDividerVisible = true) + } + } + is StepRoute.StageImplement, + is StepRoute.LearnDaily, + is StepRoute.Repeat -> { + // no op + } + } + } + final override fun render(isPracticingLoading: Boolean) { if (isResumed) { with(viewBinding) { 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 fc9e9c4f75..fc75533fee 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 @@ -9,7 +9,8 @@ class TopicsRepetitionCardFormDelegate { fun render( context: Context, binding: LayoutTopicsRepetitionCardBinding, - recommendedRepetitionsCount: Int + recommendedRepetitionsCount: Int, + isFreemiumEnabled: Boolean ) { with(binding) { topicsRepetitionBackgroundImageView.setImageResource( @@ -47,6 +48,7 @@ class TopicsRepetitionCardFormDelegate { org.hyperskill.app.R.string.good_job } ) + topicsRepetitionFreemiumBadge.isVisible = isFreemiumEnabled && recommendedRepetitionsCount > 0 } } } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/track/view/fragment/TrackFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/track/view/fragment/TrackFragment.kt index a62516465a..fcd87d7a67 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/track/view/fragment/TrackFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/track/view/fragment/TrackFragment.kt @@ -19,10 +19,10 @@ import org.hyperskill.app.android.R import org.hyperskill.app.android.core.extensions.openUrl import org.hyperskill.app.android.core.view.ui.dialog.LoadingProgressDialogFragment import org.hyperskill.app.android.core.view.ui.dialog.dismissDialogFragmentIfExists -import org.hyperskill.app.android.core.view.ui.navigation.requireMainRouter import org.hyperskill.app.android.core.view.ui.navigation.requireRouter import org.hyperskill.app.android.databinding.FragmentTrackBinding import org.hyperskill.app.android.gamification_toolbar.view.ui.delegate.GamificationToolbarDelegate +import org.hyperskill.app.android.main.view.ui.navigation.MainScreenRouter import org.hyperskill.app.android.profile.view.navigation.ProfileScreen import org.hyperskill.app.android.step.view.screen.StepScreen import org.hyperskill.app.android.topics.view.delegate.TopicsToDiscoverNextDelegate @@ -67,6 +67,9 @@ class TrackFragment : HyperskillApp.graph().imageLoadingComponent.imageLoader } + private val mainScreenRouter: MainScreenRouter = + HyperskillApp.graph().navigationComponent.mainScreenCicerone.router + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) injectComponents() @@ -134,7 +137,7 @@ class TrackFragment : is TrackFeature.Action.ViewAction.GamificationToolbarViewAction -> when (action.viewAction) { is GamificationToolbarFeature.Action.ViewAction.ShowProfileTab -> - requireMainRouter().switch(ProfileScreen(isInitCurrent = true)) + mainScreenRouter.switch(ProfileScreen(isInitCurrent = true)) } is TrackFeature.Action.ViewAction.TopicsToDiscoverNextViewAction -> when (action.viewAction) { diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/view/base/ui/extension/ThemeExtensions.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/view/base/ui/extension/ThemeExtensions.kt new file mode 100644 index 0000000000..ecdb8fa679 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/view/base/ui/extension/ThemeExtensions.kt @@ -0,0 +1,13 @@ +package org.hyperskill.app.android.view.base.ui.extension + +import android.content.Context +import android.view.LayoutInflater +import androidx.annotation.StyleRes +import androidx.appcompat.view.ContextThemeWrapper +import org.hyperskill.app.android.R + +fun LayoutInflater.wrapWithTheme( + context: Context, + @StyleRes themeRes: Int = R.style.AppTheme +): LayoutInflater = + cloneInContext(ContextThemeWrapper(context, themeRes)) \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/drawable-hdpi/problems_limit_reached.webp b/androidHyperskillApp/src/main/res/drawable-hdpi/problems_limit_reached.webp new file mode 100644 index 0000000000..cd7cc49b90 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-hdpi/problems_limit_reached.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-mdpi/problems_limit_reached.webp b/androidHyperskillApp/src/main/res/drawable-mdpi/problems_limit_reached.webp new file mode 100644 index 0000000000..042a96cfc8 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-mdpi/problems_limit_reached.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xhdpi/problems_limit_reached.webp b/androidHyperskillApp/src/main/res/drawable-xhdpi/problems_limit_reached.webp new file mode 100644 index 0000000000..30640f6a8c Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xhdpi/problems_limit_reached.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxhdpi/problems_limit_reached.webp b/androidHyperskillApp/src/main/res/drawable-xxhdpi/problems_limit_reached.webp new file mode 100644 index 0000000000..2570373f70 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxhdpi/problems_limit_reached.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxxhdpi/problems_limit_reached.webp b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/problems_limit_reached.webp new file mode 100644 index 0000000000..daa8939889 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/problems_limit_reached.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable/bg_home_freemium_badge.xml b/androidHyperskillApp/src/main/res/drawable/bg_home_freemium_badge.xml new file mode 100644 index 0000000000..f9b914061e --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/bg_home_freemium_badge.xml @@ -0,0 +1,11 @@ + + + + + + + + \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/layout/fragment_home.xml b/androidHyperskillApp/src/main/res/layout/fragment_home.xml index 8c6530ec8b..e5838641e8 100644 --- a/androidHyperskillApp/src/main/res/layout/fragment_home.xml +++ b/androidHyperskillApp/src/main/res/layout/fragment_home.xml @@ -42,6 +42,14 @@ android:layout_marginHorizontal="20dp" /> + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/layout/fragment_problems_limit_reached.xml b/androidHyperskillApp/src/main/res/layout/fragment_problems_limit_reached.xml new file mode 100644 index 0000000000..067c567071 --- /dev/null +++ b/androidHyperskillApp/src/main/res/layout/fragment_problems_limit_reached.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/layout/fragment_step_quiz.xml b/androidHyperskillApp/src/main/res/layout/fragment_step_quiz.xml index 483105f047..50abae1ac8 100644 --- a/androidHyperskillApp/src/main/res/layout/fragment_step_quiz.xml +++ b/androidHyperskillApp/src/main/res/layout/fragment_step_quiz.xml @@ -51,9 +51,18 @@ android:layout_margin="20dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintBottom_toTopOf="@+id/stepQuizFeedbackBlocks" + app:layout_constraintBottom_toTopOf="@+id/stepQuizProblemsLimit" tools:text="2438 users solved this problem. Latest completion was about 13 hours ago." /> + + - + + \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/layout/layout_topics_repetition_card.xml b/androidHyperskillApp/src/main/res/layout/layout_topics_repetition_card.xml index a669ba6e80..1c82257362 100644 --- a/androidHyperskillApp/src/main/res/layout/layout_topics_repetition_card.xml +++ b/androidHyperskillApp/src/main/res/layout/layout_topics_repetition_card.xml @@ -62,9 +62,10 @@ android:gravity="center" android:layout_marginTop="17dp" android:layout_marginStart="20dp" - android:layout_marginBottom="20dp" + android:layout_marginBottom="12dp" + app:layout_goneMarginBottom="20dp" app:layout_constraintEnd_toStartOf="@+id/topicsRepetitionCountDescriptionTextView" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/topicsRepetitionFreemiumBadge" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/topicsRepetitionTitleTextView" tools:text="4" @@ -84,6 +85,17 @@ tools:text="topics to repeat today" /> + + \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/values/dimens.xml b/androidHyperskillApp/src/main/res/values/dimens.xml index 236911b4c6..bf7cf167ea 100644 --- a/androidHyperskillApp/src/main/res/values/dimens.xml +++ b/androidHyperskillApp/src/main/res/values/dimens.xml @@ -89,4 +89,6 @@ 8dp + 20dp + \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/values/styles.xml b/androidHyperskillApp/src/main/res/values/styles.xml index 7101ead078..f978ba5bce 100644 --- a/androidHyperskillApp/src/main/res/values/styles.xml +++ b/androidHyperskillApp/src/main/res/values/styles.xml @@ -160,5 +160,18 @@ wrap_content @style/AlertDialogTitleStyle + + + + \ No newline at end of file diff --git a/gradle/app.versions.toml b/gradle/app.versions.toml index 0a2727cf55..65363b554e 100644 --- a/gradle/app.versions.toml +++ b/gradle/app.versions.toml @@ -2,5 +2,5 @@ minSdk = '24' targetSdk = '31' compileSdk = '33' -versionName = '1.12' -versionCode = '71' \ No newline at end of file +versionName = '1.13' +versionCode = '76' \ No newline at end of file diff --git a/iosHyperskillApp/fastlane/release-notes.txt b/iosHyperskillApp/fastlane/release-notes.txt index 32a8a4a43c..13a2126128 100644 --- a/iosHyperskillApp/fastlane/release-notes.txt +++ b/iosHyperskillApp/fastlane/release-notes.txt @@ -1,3 +1,4 @@ What to Test: -1. Fix passed tracks count calculation ALTAPPS-646 -2. Daily limits for learners with Freemium ALTAPPS-667 \ No newline at end of file +1. Optimize determine user account on application launch ALTAPPS-532 +2. Fix next topic card can't be render if there is no topic to load ALTAPPS-689 +3. Fix app blocked if no track selected in mobile and selected in web in the same time ALTAPPS-693 \ No newline at end of file diff --git a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj index 8cdd3397bf..ae48ccec2e 100644 --- a/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj +++ b/iosHyperskillApp/iosHyperskillApp.xcodeproj/project.pbxproj @@ -3411,7 +3411,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 82; DEBUG_INFORMATION_FORMAT = dwarf; DEVELOPMENT_TEAM = 3DWS674B2M; GENERATE_INFOPLIST_FILE = YES; @@ -3432,7 +3432,7 @@ BUNDLE_LOADER = "$(TEST_HOST)"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 77; + CURRENT_PROJECT_VERSION = 82; DEVELOPMENT_TEAM = 3DWS674B2M; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.4; diff --git a/iosHyperskillApp/iosHyperskillApp/Info.plist b/iosHyperskillApp/iosHyperskillApp/Info.plist index c7a8dbab8e..373ae12edf 100644 --- a/iosHyperskillApp/iosHyperskillApp/Info.plist +++ b/iosHyperskillApp/iosHyperskillApp/Info.plist @@ -21,7 +21,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.12 + 1.13 CFBundleURLTypes @@ -34,7 +34,7 @@ CFBundleVersion - 77 + 82 LSRequiresIPhoneOS UIApplicationSceneManifest diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorView.swift index 49a0fdbe08..367ad659af 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/CodeEditor/View/UIKit/CodeEditorView/CodeEditorView.swift @@ -162,23 +162,24 @@ final class CodeEditorView: UIView { ) } + #warning("ALTAPPS-673: Code completion temporarily disabled") private func analyzeCodeAndComplete() { - guard let language = language, - let viewController = delegate?.codeEditorViewDidRequestSuggestionPresentationController(self) - else { - return - } - - codePlaygroundManager.analyzeAndComplete( - textView: codeTextView, - previousText: oldCode ?? "", - language: language, - tabSize: tabSize, - inViewController: viewController, - suggestionsDelegate: self - ) - - oldCode = code +// guard let language = language, +// let viewController = delegate?.codeEditorViewDidRequestSuggestionPresentationController(self) +// else { +// return +// } +// +// codePlaygroundManager.analyzeAndComplete( +// textView: codeTextView, +// previousText: oldCode ?? "", +// language: language, +// tabSize: tabSize, +// inViewController: viewController, +// suggestionsDelegate: self +// ) +// +// oldCode = code } } diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/problems_limit/injection/PlatformProblemsLimitComponent.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/problems_limit/injection/PlatformProblemsLimitComponent.kt new file mode 100644 index 0000000000..3605a803e1 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/problems_limit/injection/PlatformProblemsLimitComponent.kt @@ -0,0 +1,7 @@ +package org.hyperskill.app.problems_limit.injection + +import org.hyperskill.app.core.injection.ReduxViewModelFactory + +interface PlatformProblemsLimitComponent { + val reduxViewModelFactory: ReduxViewModelFactory +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/problems_limit/injection/PlatformProblemsLimitComponentImpl.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/problems_limit/injection/PlatformProblemsLimitComponentImpl.kt new file mode 100644 index 0000000000..270c124714 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/problems_limit/injection/PlatformProblemsLimitComponentImpl.kt @@ -0,0 +1,21 @@ +package org.hyperskill.app.problems_limit.injection + +import org.hyperskill.app.core.injection.ReduxViewModelFactory +import org.hyperskill.app.problems_limit.presentation.ProblemsLimitViewModel +import ru.nobird.app.presentation.redux.container.wrapWithViewContainer + +class PlatformProblemsLimitComponentImpl( + private val problemsLimitComponent: ProblemsLimitComponent +) : PlatformProblemsLimitComponent { + override val reduxViewModelFactory: ReduxViewModelFactory + get() = ReduxViewModelFactory( + viewModelMap = mapOf( + ProblemsLimitViewModel::class.java to + { + ProblemsLimitViewModel( + problemsLimitComponent.problemsLimitFeature.wrapWithViewContainer() + ) + } + ) + ) +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/problems_limit/presentation/ProblemsLimitViewModel.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/problems_limit/presentation/ProblemsLimitViewModel.kt new file mode 100644 index 0000000000..62bc58218b --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/problems_limit/presentation/ProblemsLimitViewModel.kt @@ -0,0 +1,12 @@ +package org.hyperskill.app.problems_limit.presentation + +import ru.nobird.android.view.redux.viewmodel.ReduxViewModel +import ru.nobird.app.presentation.redux.container.ReduxViewContainer + +class ProblemsLimitViewModel( + viewContainer: ReduxViewContainer +) : ReduxViewModel(viewContainer) { + init { + onNewMessage(ProblemsLimitFeature.Message.Initialize()) + } +} \ No newline at end of file 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 e0a89c9da7..de0703299f 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 @@ -21,35 +21,41 @@ abstract class BaseStateRepository : StateRepository { private val mutableSharedFlow = MutableSharedFlow() - protected abstract suspend fun loadState(): Result - protected open val stateHolder: StateHolder = InMemoryStateHolder() /** - * Load state if needed and return in-memory value + * Flow of state changes * - * @return result of state loading or in-memory value + * @return shared flow */ - override suspend fun getState(): Result = + override val changes: SharedFlow + get() = mutableSharedFlow + + /** + * Load state from some source + * + * @return result of state loading + */ + protected abstract suspend fun loadState(): Result + + /** + * Load state if needed and return new or old value + * + * @param forceUpdate force loading of state + * @return current state + */ + override suspend fun getState(forceUpdate: Boolean): Result = mutex.withLock { val currentState = stateHolder.getState() - if (currentState != null) { + if (currentState != null && !forceUpdate) { return Result.success(currentState) } return loadAndAssignState() } - /** - * Flow of state changes - * - * @return shared flow - */ - override val changes: SharedFlow - get() = mutableSharedFlow - /** * Update state locally in app * @@ -75,7 +81,6 @@ abstract class BaseStateRepository : StateRepository { /** * Reset current local state * next call of getState will load it - * */ override suspend fun resetState() { stateHolder.resetState() diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/InMemoryStateHolder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/InMemoryStateHolder.kt index 09ce54117d..9c6939c9e5 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/InMemoryStateHolder.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/InMemoryStateHolder.kt @@ -8,8 +8,8 @@ class InMemoryStateHolder : StateHolder { override fun getState(): State? = state - override fun setState(state: State) { - this.state = state + override fun setState(newState: State) { + this.state = newState } override fun resetState() { diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/StateHolder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/StateHolder.kt index 663f3af2cb..7f023ca695 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/StateHolder.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/domain/repository/StateHolder.kt @@ -6,6 +6,6 @@ package org.hyperskill.app.core.domain.repository */ interface StateHolder { fun getState(): State? - fun setState(state: State) + fun setState(newState: State) fun resetState() } \ No newline at end of file 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 aa4245a7fc..cac9166a28 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 @@ -6,9 +6,10 @@ interface StateRepository { /** * Load state if needed and return new or old value * + * @param forceUpdate force loading of state * @return current state */ - suspend fun getState(): Result + suspend fun getState(forceUpdate: Boolean = false): Result /** * Flow of state changes diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/AppGraph.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/AppGraph.kt index bc130d2812..3ce27aa532 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/AppGraph.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/AppGraph.kt @@ -45,7 +45,6 @@ import org.hyperskill.app.step_quiz_hints.injection.StepQuizHintsComponent import org.hyperskill.app.streaks.injection.StreakFlowDataComponent import org.hyperskill.app.streaks.injection.StreaksDataComponent import org.hyperskill.app.study_plan.injection.StudyPlanDataComponent -import org.hyperskill.app.subscriptions.injection.SubscriptionsDataComponent import org.hyperskill.app.topics.injection.TopicsDataComponent import org.hyperskill.app.topics_repetitions.injection.TopicsRepetitionsComponent import org.hyperskill.app.topics_repetitions.injection.TopicsRepetitionsDataComponent @@ -71,7 +70,7 @@ interface AppGraph { val stepCompletionFlowDataComponent: StepCompletionFlowDataComponent val progressesFlowDataComponent: ProgressesFlowDataComponent val notificationFlowDataComponent: NotificationFlowDataComponent - val subscriptionsDataComponent: SubscriptionsDataComponent + val stateRepositoriesComponent: StateRepositoriesComponent /** * Auth components diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponent.kt new file mode 100644 index 0000000000..27d56fcde0 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponent.kt @@ -0,0 +1,12 @@ +package org.hyperskill.app.core.injection + +import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository + +interface StateRepositoriesComponent { + // Note: add state reset of every new state repository to resetStateRepositories method + val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository + + suspend fun resetRepositories() { + currentSubscriptionStateRepository.resetState() + } +} \ 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/core/injection/StateRepositoriesComponentImpl.kt similarity index 78% rename from shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/injection/SubscriptionsDataComponentImpl.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponentImpl.kt index 1402c71a5c..288fd473c8 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/injection/SubscriptionsDataComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/StateRepositoriesComponentImpl.kt @@ -1,12 +1,14 @@ -package org.hyperskill.app.subscriptions.injection +package org.hyperskill.app.core.injection -import org.hyperskill.app.core.injection.AppGraph import org.hyperskill.app.subscriptions.data.repository.CurrentSubscriptionStateRepositoryImpl import org.hyperskill.app.subscriptions.data.source.SubscriptionsRemoteDataSource import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository import org.hyperskill.app.subscriptions.remote.SubscriptionsRemoteDataSourceImpl -class SubscriptionsDataComponentImpl(appGraph: AppGraph) : SubscriptionsDataComponent { +class StateRepositoriesComponentImpl(appGraph: AppGraph) : StateRepositoriesComponent { + /** + * Current subscription + */ private val subscriptionsRemoteDataSource: SubscriptionsRemoteDataSource = SubscriptionsRemoteDataSourceImpl(appGraph.networkComponent.authorizedHttpClient) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/freemium/injection/FreemiumDataComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/freemium/injection/FreemiumDataComponentImpl.kt index 9983a8d3b1..792cfae609 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/freemium/injection/FreemiumDataComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/freemium/injection/FreemiumDataComponentImpl.kt @@ -8,7 +8,7 @@ class FreemiumDataComponentImpl( appGraph: AppGraph ) : FreemiumDataComponent { private val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository = - appGraph.subscriptionsDataComponent.currentSubscriptionStateRepository + appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository override val freemiumInteractor: FreemiumInteractor get() = FreemiumInteractor(currentSubscriptionStateRepository) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/AppFeatureBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/AppFeatureBuilder.kt index 5e574d2788..a2ad2d82d7 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/AppFeatureBuilder.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/AppFeatureBuilder.kt @@ -1,6 +1,7 @@ package org.hyperskill.app.main.injection import org.hyperskill.app.auth.domain.interactor.AuthInteractor +import org.hyperskill.app.core.injection.StateRepositoriesComponent import org.hyperskill.app.core.presentation.ActionDispatcherOptions import org.hyperskill.app.main.domain.interactor.AppInteractor import org.hyperskill.app.main.presentation.AppActionDispatcher @@ -19,7 +20,8 @@ object AppFeatureBuilder { appInteractor: AppInteractor, authInteractor: AuthInteractor, profileInteractor: ProfileInteractor, - sentryInteractor: SentryInteractor + sentryInteractor: SentryInteractor, + stateRepositoriesComponent: StateRepositoriesComponent ): Feature { val appReducer = AppReducer() val appActionDispatcher = AppActionDispatcher( @@ -27,7 +29,8 @@ object AppFeatureBuilder { appInteractor, authInteractor, profileInteractor, - sentryInteractor + sentryInteractor, + stateRepositoriesComponent ) return ReduxFeature(State.Idle, appReducer) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/MainComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/MainComponentImpl.kt index f642ede8e9..43bd9deaa2 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/MainComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/MainComponentImpl.kt @@ -10,6 +10,7 @@ class MainComponentImpl(private val appGraph: AppGraph) : MainComponent { appGraph.buildMainDataComponent().appInteractor, appGraph.authComponent.authInteractor, appGraph.buildProfileDataComponent().profileInteractor, - appGraph.sentryComponent.sentryInteractor + appGraph.sentryComponent.sentryInteractor, + appGraph.stateRepositoriesComponent ) } \ No newline at end of file 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 dfdf29503c..6da51b987b 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,11 +5,13 @@ import kotlinx.coroutines.flow.onEach import org.hyperskill.app.auth.domain.interactor.AuthInteractor import org.hyperskill.app.auth.domain.model.UserDeauthorized import org.hyperskill.app.core.domain.DataSourceType +import org.hyperskill.app.core.injection.StateRepositoriesComponent import org.hyperskill.app.core.presentation.ActionDispatcherOptions import org.hyperskill.app.main.domain.interactor.AppInteractor import org.hyperskill.app.main.presentation.AppFeature.Action import org.hyperskill.app.main.presentation.AppFeature.Message import org.hyperskill.app.profile.domain.interactor.ProfileInteractor +import org.hyperskill.app.profile.domain.model.isNewUser import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.sentry.domain.model.breadcrumb.HyperskillSentryBreadcrumbBuilder import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder @@ -20,7 +22,8 @@ class AppActionDispatcher( private val appInteractor: AppInteractor, private val authInteractor: AuthInteractor, private val profileInteractor: ProfileInteractor, - private val sentryInteractor: SentryInteractor + private val sentryInteractor: SentryInteractor, + private val stateRepositoriesComponent: StateRepositoriesComponent ) : CoroutineActionDispatcher(config.createConfig()) { init { authInteractor @@ -35,6 +38,8 @@ class AppActionDispatcher( } } + stateRepositoriesComponent.resetRepositories() + sentryInteractor.addBreadcrumb(HyperskillSentryBreadcrumbBuilder.buildAppUserDeauthorized(it.reason)) onNewMessage(Message.UserDeauthorized(it.reason)) @@ -50,8 +55,29 @@ class AppActionDispatcher( sentryInteractor.addBreadcrumb(HyperskillSentryBreadcrumbBuilder.buildAppDetermineUserAccountStatus()) - profileInteractor - .getCurrentProfile(sourceType = DataSourceType.REMOTE) + val isAuthorized = authInteractor.isAuthorized() + .getOrDefault(false) + // TODO: Move this logic to reducer + val profileResult = if (isAuthorized) { + profileInteractor + .getCurrentProfile(sourceType = DataSourceType.CACHE) + .fold( + onSuccess = { profile -> + // ALTAPPS-693: + // If user is new, we need to fetch profile from remote to check if track selected + if (profile.isNewUser) { + profileInteractor.getCurrentProfile(sourceType = DataSourceType.REMOTE) + } else { + Result.success(profile) + } + }, + onFailure = { profileInteractor.getCurrentProfile(sourceType = DataSourceType.REMOTE) } + ) + } else { + profileInteractor.getCurrentProfile(sourceType = DataSourceType.REMOTE) + } + + profileResult .fold( onSuccess = { profile -> sentryInteractor.addBreadcrumb( diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/problems_limit/injection/ProblemsLimitComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/problems_limit/injection/ProblemsLimitComponentImpl.kt index 686a688a77..09eac92a21 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/problems_limit/injection/ProblemsLimitComponentImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/problems_limit/injection/ProblemsLimitComponentImpl.kt @@ -8,7 +8,7 @@ class ProblemsLimitComponentImpl(private val appGraph: AppGraph) : ProblemsLimit override val problemsLimitFeature: Feature get() = ProblemsLimitFeatureBuilder.build( appGraph.buildFreemiumDataComponent().freemiumInteractor, - appGraph.subscriptionsDataComponent.currentSubscriptionStateRepository, + appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository, appGraph.commonComponent.resourceProvider, appGraph.commonComponent.dateFormatter ) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/problems_limit/presentation/ProblemsLimitActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/problems_limit/presentation/ProblemsLimitActionDispatcher.kt index f68420bf9c..a166943339 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/problems_limit/presentation/ProblemsLimitActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/problems_limit/presentation/ProblemsLimitActionDispatcher.kt @@ -38,7 +38,9 @@ class ProblemsLimitActionDispatcher( } onNewMessage( - currentSubscriptionStateRepository.getState() + // TODO: use force update from reducer Initialize message + // after refactoring of feature integration + currentSubscriptionStateRepository.getState(forceUpdate = true) .fold( onSuccess = { Message.SubscriptionLoadingResult.Success( diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/domain/model/StudyPlan.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/domain/model/StudyPlan.kt index 718e98a5bd..d5b08c7c37 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/domain/model/StudyPlan.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/study_plan/domain/model/StudyPlan.kt @@ -15,15 +15,15 @@ data class StudyPlan( @SerialName("sections") val sections: List, @SerialName("seconds_to_reach_track") - val secondsToReachTrack: Long, + val secondsToReachTrack: Float, @SerialName("seconds_to_reach_project") - val secondsToReachProject: Long, + val secondsToReachProject: Float, @SerialName("created_at") val createdAt: String ) { val minutesToReachTrack: Int = - (secondsToReachTrack.toDouble() / 60).roundToInt() + (secondsToReachTrack / 60.0).roundToInt() val hoursToReachTrack: Int = - (secondsToReachTrack.toDouble() / 3600).roundToInt() + (secondsToReachTrack / 3600.0).roundToInt() } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/injection/SubscriptionsDataComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/injection/SubscriptionsDataComponent.kt deleted file mode 100644 index 18fe87a20a..0000000000 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/subscriptions/injection/SubscriptionsDataComponent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.hyperskill.app.subscriptions.injection - -import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository - -interface SubscriptionsDataComponent { - val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository -} \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/org/hyperskill/app/core/injection/AppGraphImpl.kt b/shared/src/iosMain/kotlin/org/hyperskill/app/core/injection/AppGraphImpl.kt index e039366725..43bdf4cdf2 100644 --- a/shared/src/iosMain/kotlin/org/hyperskill/app/core/injection/AppGraphImpl.kt +++ b/shared/src/iosMain/kotlin/org/hyperskill/app/core/injection/AppGraphImpl.kt @@ -92,8 +92,6 @@ import org.hyperskill.app.streaks.injection.StreaksDataComponent import org.hyperskill.app.streaks.injection.StreaksDataComponentImpl import org.hyperskill.app.study_plan.injection.StudyPlanDataComponent import org.hyperskill.app.study_plan.injection.StudyPlanDataComponentImpl -import org.hyperskill.app.subscriptions.injection.SubscriptionsDataComponent -import org.hyperskill.app.subscriptions.injection.SubscriptionsDataComponentImpl import org.hyperskill.app.topics.injection.TopicsDataComponent import org.hyperskill.app.topics.injection.TopicsDataComponentImpl import org.hyperskill.app.topics_repetitions.injection.TopicsRepetitionsComponent @@ -149,8 +147,8 @@ class AppGraphImpl( override val notificationFlowDataComponent: NotificationFlowDataComponent = NotificationFlowDataComponentImpl() - override val subscriptionsDataComponent: SubscriptionsDataComponent = - SubscriptionsDataComponentImpl(this) + override val stateRepositoriesComponent: StateRepositoriesComponent = + StateRepositoriesComponentImpl(this) override val sentryComponent: SentryComponent = SentryComponentImpl(sentryManager)