diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/challenge/ui/ChallengeDescription.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/challenge/ui/ChallengeDescription.kt index 6e9aa7d814..f0df2e639a 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/challenge/ui/ChallengeDescription.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/challenge/ui/ChallengeDescription.kt @@ -7,7 +7,7 @@ import androidx.compose.ui.res.colorResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.tooling.preview.Preview import org.hyperskill.app.R -import org.hyperskill.app.android.core.view.ui.widget.compose.HtmlText +import org.hyperskill.app.android.core.view.ui.widget.compose.ClickableHtmlText import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme @Composable @@ -16,7 +16,7 @@ fun ChallengeDescription( onLinkClick: (String) -> Unit, modifier: Modifier = Modifier ) { - HtmlText( + ClickableHtmlText( text = description, modifier = modifier, baseSpanStyle = SpanStyle(color = colorResource(id = R.color.color_on_surface_alpha_60)), diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HtmlText.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HtmlText.kt index 423bbb28ab..8dff8a8876 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HtmlText.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HtmlText.kt @@ -27,6 +27,26 @@ private const val URL_TAG = "url" @Composable fun HtmlText( + text: String, + baseSpanStyle: SpanStyle? = null, + content: @Composable (AnnotatedString) -> Unit +) { + val annotatedString = remember(text) { + HtmlCompat + .fromHtml( + text.replace("\n", "
"), + HtmlCompat.FROM_HTML_MODE_COMPACT + ) + .toAnnotatedString( + baseSpanStyle = baseSpanStyle, + underlineLinks = false + ) + } + content(annotatedString) +} + +@Composable +fun ClickableHtmlText( text: String, modifier: Modifier = Modifier, baseSpanStyle: SpanStyle? = null, @@ -35,15 +55,19 @@ fun HtmlText( style: TextStyle = LocalTextStyle.current, onUrlClick: ((url: String) -> Unit)? = null ) { - val spannedText = remember(text) { - HtmlCompat.fromHtml(text.replace("\n", "
"), HtmlCompat.FROM_HTML_MODE_COMPACT) + val annotatedString = remember(text) { + HtmlCompat + .fromHtml( + text.replace("\n", "
"), + HtmlCompat.FROM_HTML_MODE_COMPACT + ) + .toAnnotatedString( + baseSpanStyle = baseSpanStyle, + linkColor = if (isHighlightLink) linkColor else Color.Unspecified, + underlineLinks = false + ) } val uriHandler = LocalUriHandler.current - val annotatedString = spannedText.toAnnotatedString( - baseSpanStyle = baseSpanStyle, - linkColor = if (isHighlightLink) linkColor else Color.Unspecified, - underlineLinks = false - ) ClickableText( modifier = modifier, text = annotatedString, @@ -62,7 +86,7 @@ fun HtmlText( fun Spanned.toAnnotatedString( baseSpanStyle: SpanStyle?, underlineLinks: Boolean, - linkColor: Color + linkColor: Color = Color.Blue ): AnnotatedString = buildAnnotatedString { val spanned = this@toAnnotatedString @@ -101,7 +125,7 @@ fun Spanned.toAnnotatedString( @Preview @Composable private fun LinksHtmlTextPreview() { - HtmlText( + ClickableHtmlText( /*ktlint-disable*/ text = "Some text \n" + "link text, the rest of the text" diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HyperskillButton.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HyperskillButton.kt index d14510f12c..e53acb4ca8 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HyperskillButton.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HyperskillButton.kt @@ -44,6 +44,7 @@ object HyperskillButtonDefaults { ): ButtonColors = ButtonDefaults.buttonColors( backgroundColor = backgroundColor, + disabledBackgroundColor = backgroundColor.copy(alpha = 0.38f), contentColor = contentColor ) diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HyperskillCard.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HyperskillCard.kt index 92426759ad..c5c3329e3e 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HyperskillCard.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/HyperskillCard.kt @@ -20,9 +20,9 @@ import org.hyperskill.app.android.R @Composable fun HyperskillCard( - contentPadding: PaddingValues, modifier: Modifier = Modifier, cornerRadius: Dp = dimensionResource(id = R.dimen.corner_radius), + contentPadding: PaddingValues? = null, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, onClick: (() -> Unit)? = null, content: @Composable BoxScope.() -> Unit @@ -42,7 +42,11 @@ fun HyperskillCard( it } } - .padding(contentPadding), + .apply { + if (contentPadding != null) { + padding(contentPadding) + } + }, content = content ) } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/ShimmerModifier.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/ShimmerModifier.kt index d5c4afd1ac..c86a3e0091 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/ShimmerModifier.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/core/view/ui/widget/compose/ShimmerModifier.kt @@ -1,9 +1,13 @@ package org.hyperskill.app.android.core.view.ui.widget.compose -import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.DurationBasedAnimationSpec import androidx.compose.animation.core.Easing import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue @@ -16,16 +20,25 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +private const val INITIAL_VALUE = -2f +private const val TARGET_VALUE = 2f + +private val DefaultColors: List = listOf( + Color.Transparent, + Color.White.copy(alpha = 0.7f), + Color.Transparent +) + /** * Applies shimmer animation to the target Composable. * Animation is playing one time. - * To start animation call [ShimmerState.runShimmerAnimation] on the [ShimmerState] instance. + * To start animation call [ShimmerShotState.runShimmerAnimation] on the [ShimmerShotState] instance. */ -fun Modifier.shimmerShot(shimmerState: ShimmerState): Modifier = +fun Modifier.shimmerShot(shimmerState: ShimmerShotState): Modifier = composed { val startOffsetX by animateFloatAsState( targetValue = shimmerState.targetValue, - animationSpec = shimmerState.startOffsetXAnimationSpec, + animationSpec = shimmerState.animationSpec, label = "shimmer" ) drawWithContent { @@ -43,26 +56,65 @@ fun Modifier.shimmerShot(shimmerState: ShimmerState): Modifier = } } +fun Modifier.shimmer( + isLoading: Boolean, + colors: List = DefaultColors, + durationMillis: Int = 2000, + easing: Easing = FastOutSlowInEasing +): Modifier = + if (isLoading) { + shimmer(colors, durationMillis, easing) + } else { + this + } + +private fun Modifier.shimmer( + colors: List, + durationMillis: Int, + easing: Easing +): Modifier = + composed { + val transition = rememberInfiniteTransition(label = "") + + val translateAnimation = transition.animateFloat( + initialValue = INITIAL_VALUE, + targetValue = TARGET_VALUE, + animationSpec = infiniteRepeatable( + animation = tween( + durationMillis = durationMillis, + easing = easing + ), + repeatMode = RepeatMode.Restart, + ), + label = "Shimmer loading animation", + ) + + drawWithContent { + val width = size.width + val height = size.height + val offset = translateAnimation.value * width + + drawContent() + val brush = Brush.linearGradient( + colors = colors, + start = Offset(offset, 0f), + end = Offset(offset + width, height) + ) + drawRect(brush) + } + } + @Stable -class ShimmerState( - val colors: List = listOf( - Color.Transparent, - Color.White.copy(alpha = 0.7f), - Color.Transparent - ), +class ShimmerShotState( + val colors: List = DefaultColors, durationMillis: Int = 1200, easing: Easing = FastOutSlowInEasing ) { - companion object { - private const val INITIAL_VALUE = -2f - private const val TARGET_VALUE = 2f - } - var targetValue: Float by mutableStateOf(INITIAL_VALUE) private set - val startOffsetXAnimationSpec: AnimationSpec = tween( + val animationSpec: DurationBasedAnimationSpec = tween( durationMillis = durationMillis, easing = easing ) diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/MainActivity.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/MainActivity.kt index e3f0d588c6..8208301ae5 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/MainActivity.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/main/view/ui/activity/MainActivity.kt @@ -32,8 +32,6 @@ import org.hyperskill.app.android.core.extensions.screenOrientation import org.hyperskill.app.android.core.view.ui.fragment.ReduxViewLifecycleObserver import org.hyperskill.app.android.core.view.ui.navigation.AppNavigationContainer import org.hyperskill.app.android.databinding.ActivityMainBinding -import org.hyperskill.app.android.first_problem_onboarding.fragment.FirstProblemOnboardingFragment -import org.hyperskill.app.android.first_problem_onboarding.navigation.FirstProblemOnboardingScreen import org.hyperskill.app.android.main.view.ui.navigation.MainScreen import org.hyperskill.app.android.main.view.ui.navigation.Tabs import org.hyperskill.app.android.notification.NotificationIntentBuilder @@ -42,24 +40,22 @@ import org.hyperskill.app.android.notification.model.ClickedNotificationData import org.hyperskill.app.android.notification.model.DailyStudyReminderClickedData import org.hyperskill.app.android.notification.model.DefaultNotificationClickedData import org.hyperskill.app.android.notification.model.PushNotificationClickedData -import org.hyperskill.app.android.notification_onboarding.fragment.NotificationsOnboardingFragment -import org.hyperskill.app.android.notification_onboarding.navigation.NotificationsOnboardingScreen import org.hyperskill.app.android.paywall.fragment.PaywallFragment import org.hyperskill.app.android.paywall.navigation.PaywallScreen import org.hyperskill.app.android.step.view.navigation.StepScreen import org.hyperskill.app.android.streak_recovery.view.delegate.StreakRecoveryViewActionDelegate import org.hyperskill.app.android.track_selection.list.navigation.TrackSelectionListScreen -import org.hyperskill.app.android.users_questionnaire_onboarding.fragment.UsersQuestionnaireOnboardingFragment -import org.hyperskill.app.android.users_questionnaire_onboarding.navigation.UsersQuestionnaireOnboardingScreen import org.hyperskill.app.android.welcome.navigation.WelcomeScreen +import org.hyperskill.app.android.welcome_onbaording.root.fragment.WelcomeOnboardingFragment +import org.hyperskill.app.android.welcome_onbaording.root.model.WelcomeOnboardingCompleteResult +import org.hyperskill.app.android.welcome_onbaording.root.navigation.WelcomeOnboardingScreen import org.hyperskill.app.main.presentation.AppFeature import org.hyperskill.app.main.presentation.MainViewModel import org.hyperskill.app.notification.click_handling.presentation.NotificationClickHandlingFeature import org.hyperskill.app.notification.local.domain.analytic.NotificationDailyStudyReminderClickedHyperskillAnalyticEvent import org.hyperskill.app.profile.domain.model.Profile -import org.hyperskill.app.step.domain.model.StepRoute import org.hyperskill.app.track_selection.list.injection.TrackSelectionListParams -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingFeatureParams import ru.nobird.android.view.base.ui.delegate.ViewStateDelegate import ru.nobird.android.view.base.ui.extension.resolveColorAttribute import ru.nobird.android.view.navigation.navigator.NestedAppNavigator @@ -143,9 +139,7 @@ class MainActivity : startupViewModel(intent) observeAuthFlowSuccess() - observeNotificationsOnboardingFlowFinished() - observeFirstProblemOnboardingFlowFinished() - observeUsersQuestionnaireOnboardingCompleted() + observeWelcomeOnboardingCompleted() observePaywallIsShownChanged() mainViewModel.logScreenOrientation(screenOrientation = resources.configuration.screenOrientation) @@ -179,42 +173,42 @@ class MainActivity : } } - @SuppressLint("InlinedApi") private fun observeAuthFlowSuccess() { observeResult(AuthFragment.AUTH_SUCCESS) { profile -> mainViewModel.onNewMessage( AppFeature.Message.UserAuthorized( profile = profile, - isNotificationPermissionGranted = ContextCompat.checkSelfPermission( - this@MainActivity, - android.Manifest.permission.POST_NOTIFICATIONS - ) == PackageManager.PERMISSION_GRANTED + isNotificationPermissionGranted = isNotificationPermissionGranted() ) ) clarityDelegate.setUserId(profile.id) } } - private fun observeNotificationsOnboardingFlowFinished() { - observeResult(NotificationsOnboardingFragment.NOTIFICATIONS_ONBOARDING_FINISHED) { - mainViewModel.onNewMessage(WelcomeOnboardingFeature.Message.NotificationOnboardingCompleted) - } - } + @SuppressLint("InlinedApi") + private fun isNotificationPermissionGranted(): Boolean = + ContextCompat.checkSelfPermission( + this@MainActivity, + android.Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED - private fun observeFirstProblemOnboardingFlowFinished() { - observeResult(FirstProblemOnboardingFragment.FIRST_PROBLEM_ONBOARDING_FINISHED) { + private fun observePaywallIsShownChanged() { + observeResult(PaywallFragment.PAYWALL_IS_SHOWN_CHANGED) { mainViewModel.onNewMessage( - WelcomeOnboardingFeature.Message.FirstProblemOnboardingCompleted( - firstProblemStepRoute = it.safeCast() - ) + AppFeature.Message.IsPaywallShownChanged(it) ) } } - private fun observePaywallIsShownChanged() { - observeResult(PaywallFragment.PAYWALL_IS_SHOWN_CHANGED) { + private fun observeWelcomeOnboardingCompleted() { + observeResult(WelcomeOnboardingFragment.WELCOME_ONBOARDING_COMPLETED) { mainViewModel.onNewMessage( - AppFeature.Message.IsPaywallShownChanged(it) + AppFeature.Message.WelcomeOnboardingCompleted( + when (it) { + WelcomeOnboardingCompleteResult.Empty -> null + is WelcomeOnboardingCompleteResult.StepRoute -> it.stepRoute + } + ) ) } } @@ -239,19 +233,6 @@ class MainActivity : } } - private fun observeUsersQuestionnaireOnboardingCompleted() { - lifecycleScope.launch { - router - .observeResult(UsersQuestionnaireOnboardingFragment.USERS_QUESTIONNAIRE_ONBOARDING_FINISHED) - .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) - .collectLatest { - mainViewModel.onNewMessage( - WelcomeOnboardingFeature.Message.UsersQuestionnaireOnboardingCompleted - ) - } - } - } - override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) if (intent != null) { @@ -287,23 +268,6 @@ class MainActivity : TrackSelectionListParams(isNewUserMode = true) ) ) - is AppFeature.Action.ViewAction.WelcomeOnboardingViewAction -> - when (val viewAction = action.viewAction) { - is WelcomeOnboardingFeature.Action.ViewAction.NavigateTo.StudyPlanWithStep -> { - router.newRootChain( - MainScreen(Tabs.STUDY_PLAN), - StepScreen(viewAction.stepRoute) - ) - } - is WelcomeOnboardingFeature.Action.ViewAction.NavigateTo.FirstProblemOnboardingScreen -> - router.newRootScreen( - FirstProblemOnboardingScreen(viewAction.isNewUserMode) - ) - WelcomeOnboardingFeature.Action.ViewAction.NavigateTo.NotificationOnboardingScreen -> - router.newRootScreen(NotificationsOnboardingScreen) - WelcomeOnboardingFeature.Action.ViewAction.NavigateTo.UsersQuestionnaireOnboardingScreen -> - router.newRootScreen(UsersQuestionnaireOnboardingScreen) - } is AppFeature.Action.ViewAction.StreakRecoveryViewAction -> StreakRecoveryViewActionDelegate.handleViewAction( fragmentManager = supportFragmentManager, @@ -329,6 +293,11 @@ class MainActivity : } AppFeature.Action.ViewAction.NavigateTo.StudyPlan -> router.newRootScreen(MainScreen(Tabs.STUDY_PLAN)) + is AppFeature.Action.ViewAction.NavigateTo.Step -> + router.newRootChain( + MainScreen(Tabs.STUDY_PLAN), + StepScreen(action.stepRoute) + ) is AppFeature.Action.ViewAction.NavigateTo.Paywall -> { if (supportFragmentManager.findFragmentByTag(PaywallScreen.TAG) == null) { router.navigateTo(PaywallScreen(action.paywallTransitionSource)) @@ -339,6 +308,13 @@ class MainActivity : MainScreen(initialTab = Tabs.STUDY_PLAN), PaywallScreen(action.paywallTransitionSource) ) + is AppFeature.Action.ViewAction.NavigateTo.WelcomeOnboarding -> { + router.newRootScreen( + WelcomeOnboardingScreen( + WelcomeOnboardingFeatureParams(action.profile, isNotificationPermissionGranted()) + ) + ) + } } } diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/fragment/NotificationsOnboardingFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/fragment/NotificationsOnboardingFragment.kt index 6f9ae4de09..3a3c9da107 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/fragment/NotificationsOnboardingFragment.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/notification_onboarding/fragment/NotificationsOnboardingFragment.kt @@ -16,10 +16,10 @@ import androidx.lifecycle.ViewModelProvider import com.google.accompanist.themeadapter.material.MdcTheme import org.hyperskill.app.android.HyperskillApp import org.hyperskill.app.android.core.extensions.startAppNotificationSettingsIntent -import org.hyperskill.app.android.core.view.ui.navigation.requireAppRouter import org.hyperskill.app.android.notification.permission.NotificationPermissionDelegate import org.hyperskill.app.android.notification_onboarding.ui.NotificationsOnboardingScreen import org.hyperskill.app.android.profile.view.fragment.TimeIntervalPickerDialogFragment +import org.hyperskill.app.android.welcome_onbaording.root.model.WelcomeOnboardingHost import org.hyperskill.app.core.view.handleActions import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Action.ViewAction import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Message @@ -30,7 +30,6 @@ import ru.nobird.android.view.base.ui.extension.showIfNotExists class NotificationsOnboardingFragment : Fragment(), TimeIntervalPickerDialogFragment.Callback { companion object { - const val NOTIFICATIONS_ONBOARDING_FINISHED = "NOTIFICATIONS_ONBOARDING_FINISHED" fun newInstance(): NotificationsOnboardingFragment = NotificationsOnboardingFragment().apply { waitForNotificationPermissionResult = false @@ -105,7 +104,7 @@ class NotificationsOnboardingFragment : Fragment(), TimeIntervalPickerDialogFrag private fun onAction(action: ViewAction) { when (action) { ViewAction.CompleteNotificationOnboarding -> { - requireAppRouter().sendResult(NOTIFICATIONS_ONBOARDING_FINISHED, Any()) + (parentFragment as? WelcomeOnboardingHost)?.onNotificationOnboardingCompleted() } ViewAction.RequestNotificationPermission -> { notificationPermissionDelegate?.requestNotificationPermission() diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_completion/ui/TopicCompleted.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_completion/ui/TopicCompleted.kt index 447647b1ca..eb4eee0493 100644 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_completion/ui/TopicCompleted.kt +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/topic_completion/ui/TopicCompleted.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillButton import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme -import org.hyperskill.app.android.core.view.ui.widget.compose.ShimmerState +import org.hyperskill.app.android.core.view.ui.widget.compose.ShimmerShotState import org.hyperskill.app.android.core.view.ui.widget.compose.shimmerShot import org.hyperskill.app.topic_completed_modal.presentation.TopicCompletedModalFeature.ViewState import org.hyperskill.app.topic_completed_modal.presentation.TopicCompletedModalViewModel @@ -47,7 +47,7 @@ fun TopicCompleted( LaunchedEffect(Unit) { enterTransitionState.targetState = true } - val shimmerState = remember { ShimmerState() } + val shimmerState = remember { ShimmerShotState() } Column(modifier.padding(vertical = TopicCompletedDefaults.CONTENT_VERTICAL_PADDING)) { TopicCompletedCloseButton( onClick = onCloseClick, diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/fragment/UsersQuestionnaireOnboardingFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/fragment/UsersQuestionnaireOnboardingFragment.kt deleted file mode 100644 index 3cf39c6587..0000000000 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/fragment/UsersQuestionnaireOnboardingFragment.kt +++ /dev/null @@ -1,69 +0,0 @@ -package org.hyperskill.app.android.users_questionnaire_onboarding.fragment - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.fragment.app.Fragment -import androidx.fragment.app.viewModels -import androidx.lifecycle.ViewModelProvider -import com.google.android.material.snackbar.Snackbar -import org.hyperskill.app.android.HyperskillApp -import org.hyperskill.app.android.core.view.ui.navigation.requireAppRouter -import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme -import org.hyperskill.app.android.users_questionnaire_onboarding.ui.UsersQuestionnaireOnboardingScreen -import org.hyperskill.app.core.view.handleActions -import org.hyperskill.app.users_questionnaire_onboarding.onboarding.presentation.UsersQuestionnaireOnboardingViewModel -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.Action.ViewAction - -class UsersQuestionnaireOnboardingFragment : Fragment() { - companion object { - const val USERS_QUESTIONNAIRE_ONBOARDING_FINISHED = "USERS_QUESTIONNAIRE_ONBOARDING_FINISHED" - fun newInstance(): UsersQuestionnaireOnboardingFragment = - UsersQuestionnaireOnboardingFragment() - } - - private var viewModelFactory: ViewModelProvider.Factory? = null - private val usersQuestionnaireOnboardingViewModel: UsersQuestionnaireOnboardingViewModel by viewModels { - requireNotNull(viewModelFactory) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - injectComponent() - usersQuestionnaireOnboardingViewModel.handleActions(this, onAction = ::onAction) - } - - private fun injectComponent() { - val platformUsersQuestionnaireOnboardingComponent = - HyperskillApp.graph().buildPlatformUsersQuestionnaireOnboardingComponent() - viewModelFactory = platformUsersQuestionnaireOnboardingComponent.reduxViewModelFactory - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View = - ComposeView(requireContext()).apply { - setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner)) - setContent { - HyperskillTheme { - UsersQuestionnaireOnboardingScreen(viewModel = usersQuestionnaireOnboardingViewModel) - } - } - } - - private fun onAction(action: ViewAction) { - when (action) { - ViewAction.CompleteUsersQuestionnaireOnboarding -> { - requireAppRouter().sendResult(USERS_QUESTIONNAIRE_ONBOARDING_FINISHED, Any()) - } - is ViewAction.ShowSendSuccessMessage -> { - Snackbar.make(requireView(), action.message, Snackbar.LENGTH_SHORT).show() - } - } - } -} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/navigation/UsersQuestionnaireOnboardingScreen.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/navigation/UsersQuestionnaireOnboardingScreen.kt deleted file mode 100644 index d9f312fed1..0000000000 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/navigation/UsersQuestionnaireOnboardingScreen.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.hyperskill.app.android.users_questionnaire_onboarding.navigation - -import androidx.fragment.app.Fragment -import androidx.fragment.app.FragmentFactory -import com.github.terrakok.cicerone.androidx.FragmentScreen -import org.hyperskill.app.android.users_questionnaire_onboarding.fragment.UsersQuestionnaireOnboardingFragment - -object UsersQuestionnaireOnboardingScreen : FragmentScreen { - override fun createFragment(factory: FragmentFactory): Fragment = - UsersQuestionnaireOnboardingFragment.newInstance() -} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOnboardingDefaults.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOnboardingDefaults.kt deleted file mode 100644 index 1c875d657b..0000000000 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOnboardingDefaults.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.hyperskill.app.android.users_questionnaire_onboarding.ui - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.ui.unit.dp - -object UsersQuestionnaireOnboardingDefaults { - val ContentPadding: PaddingValues = PaddingValues( - top = 20.dp, - start = 20.dp, - end = 20.dp, - bottom = 8.dp - ) -} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOnboardingPreviewDefault.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOnboardingPreviewDefault.kt deleted file mode 100644 index 57c64a8b36..0000000000 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOnboardingPreviewDefault.kt +++ /dev/null @@ -1,54 +0,0 @@ -package org.hyperskill.app.android.users_questionnaire_onboarding.ui - -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature - -object UsersQuestionnaireOnboardingPreviewDefault { - - private enum class SelectedChoice { - NONE, - FIRST, - LAST - } - - private fun getViewState( - selectedChoice: SelectedChoice, - isSendButtonEnabled: Boolean - ) = - UsersQuestionnaireOnboardingFeature.ViewState( - title = "How did you hear about Hyperskill?", - choices = listOf( - "Google", - "Youtube", - "Instagram", - "Tiktok", - "News", - "Friends", - "Other" - ), - selectedChoice = when (selectedChoice) { - SelectedChoice.NONE -> null - SelectedChoice.FIRST -> "Google" - SelectedChoice.LAST -> "Other" - }, - textInputValue = when (selectedChoice) { - SelectedChoice.NONE -> null - SelectedChoice.FIRST, - SelectedChoice.LAST -> "example text" - }, - isTextInputVisible = selectedChoice == SelectedChoice.LAST, - isSendButtonEnabled = when (selectedChoice) { - SelectedChoice.NONE -> false - SelectedChoice.FIRST, - SelectedChoice.LAST -> isSendButtonEnabled - } - ) - - fun getUnselectedViewState(): UsersQuestionnaireOnboardingFeature.ViewState = - getViewState(SelectedChoice.NONE, false) - - fun getFirstOptionSelectedViewState(): UsersQuestionnaireOnboardingFeature.ViewState = - getViewState(SelectedChoice.FIRST, true) - - fun getOtherOptionSelectedViewState(isSendButtonEnabled: Boolean): UsersQuestionnaireOnboardingFeature.ViewState = - getViewState(SelectedChoice.LAST, isSendButtonEnabled) -} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOnboardingScreen.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOnboardingScreen.kt deleted file mode 100644 index 370b8b655e..0000000000 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOnboardingScreen.kt +++ /dev/null @@ -1,149 +0,0 @@ -package org.hyperskill.app.android.users_questionnaire_onboarding.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Scaffold -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import org.hyperskill.app.R -import org.hyperskill.app.android.core.extensions.plus -import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillButton -import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTextButton -import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme -import org.hyperskill.app.users_questionnaire_onboarding.onboarding.presentation.UsersQuestionnaireOnboardingViewModel -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.ViewState - -@Composable -fun UsersQuestionnaireOnboardingScreen(viewModel: UsersQuestionnaireOnboardingViewModel) { - DisposableEffect(viewModel) { - viewModel.onNewMessage( - UsersQuestionnaireOnboardingFeature.Message.ViewedEventMessage - ) - onDispose { - // no op - } - } - val viewState by viewModel.state.collectAsStateWithLifecycle() - UsersQuestionnaireOnboardingScreen( - viewState = viewState, - onChoiceClicked = viewModel::onChoiceClicked, - onTextInputChanged = viewModel::onTextInputChanged, - onSendClick = viewModel::onSendButtonClick, - onSkipClick = viewModel::onSkipButtonClick - ) -} - -@Composable -fun UsersQuestionnaireOnboardingScreen( - viewState: ViewState, - onChoiceClicked: (String) -> Unit, - onTextInputChanged: (String) -> Unit, - onSendClick: () -> Unit, - onSkipClick: () -> Unit, - modifier: Modifier = Modifier -) { - Scaffold { padding -> - Column( - modifier = modifier - .fillMaxSize() - .verticalScroll(rememberScrollState()) - .padding(padding + UsersQuestionnaireOnboardingDefaults.ContentPadding) - ) { - Text( - text = viewState.title, - style = MaterialTheme.typography.h5, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(20.dp)) - UsersQuestionnaireOptionsList( - choices = viewState.choices, - selectedChoice = viewState.selectedChoice, - textInputValue = viewState.textInputValue, - isTextInputVisible = viewState.isTextInputVisible, - onChoiceClicked = onChoiceClicked, - onTextInputChanged = onTextInputChanged, - onDoneClick = onSendClick - ) - Spacer(modifier = Modifier.height(32.dp)) - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - HyperskillButton( - onClick = onSendClick, - enabled = viewState.isSendButtonEnabled, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = stringResource(id = R.string.users_questionnaire_onboarding_send_button_text)) - } - HyperskillTextButton( - onClick = onSkipClick, - modifier = Modifier.fillMaxWidth() - ) { - Text(text = stringResource(id = R.string.users_questionnaire_onboarding_skip_button_text)) - } - } - } - } -} - -private class UsersQuestionnaireOnboardingPreviewParameterProvider : PreviewParameterProvider { - override val values: Sequence - get() = sequenceOf( - UsersQuestionnaireOnboardingPreviewDefault.getUnselectedViewState(), - UsersQuestionnaireOnboardingPreviewDefault.getFirstOptionSelectedViewState(), - UsersQuestionnaireOnboardingPreviewDefault.getOtherOptionSelectedViewState(false), - UsersQuestionnaireOnboardingPreviewDefault.getOtherOptionSelectedViewState(true) - ) -} - -@Preview(device = "id:pixel_3", showSystemUi = true) -@Composable -private fun UsersQuestionnaireOnboardingScreenPreview( - @PreviewParameter(UsersQuestionnaireOnboardingPreviewParameterProvider::class) - viewState: ViewState -) { - HyperskillTheme { - UsersQuestionnaireOnboardingScreen( - viewState = viewState, - onChoiceClicked = {}, - onTextInputChanged = {}, - onSendClick = {}, - onSkipClick = {} - ) - } -} - -@Preview(device = "id:Nexus S", showSystemUi = true) -@Composable -private fun UsersQuestionnaireOnboardingScreenPreviewSmallDevice( - @PreviewParameter(UsersQuestionnaireOnboardingPreviewParameterProvider::class) - viewState: ViewState -) { - HyperskillTheme { - UsersQuestionnaireOnboardingScreen( - viewState = viewState, - onChoiceClicked = {}, - onTextInputChanged = {}, - onSendClick = {}, - onSkipClick = {} - ) - } -} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOptionsList.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOptionsList.kt deleted file mode 100644 index 639a438e14..0000000000 --- a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/users_questionnaire_onboarding/ui/UsersQuestionnaireOptionsList.kt +++ /dev/null @@ -1,80 +0,0 @@ -package org.hyperskill.app.android.users_questionnaire_onboarding.ui - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.requiredSize -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material.LocalMinimumInteractiveComponentEnforcement -import androidx.compose.material.MaterialTheme -import androidx.compose.material.OutlinedTextField -import androidx.compose.material.RadioButton -import androidx.compose.material.RadioButtonDefaults -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.key -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import org.hyperskill.app.R - -@OptIn(ExperimentalMaterialApi::class) -@Composable -fun UsersQuestionnaireOptionsList( - choices: List, - selectedChoice: String?, - textInputValue: String?, - isTextInputVisible: Boolean, - onChoiceClicked: (String) -> Unit, - onTextInputChanged: (String) -> Unit, - onDoneClick: () -> Unit, - modifier: Modifier = Modifier -) { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = modifier - ) { - choices.forEach { choice -> - key(choice) { - Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - Box(modifier = Modifier.requiredSize(24.dp)) { - CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) { - RadioButton( - selected = choice == selectedChoice, - onClick = { onChoiceClicked(choice) }, - colors = RadioButtonDefaults.colors(selectedColor = MaterialTheme.colors.primary), - modifier = Modifier.align(Alignment.Center) - ) - } - } - Text( - text = choice, - textAlign = TextAlign.Center, - modifier = Modifier.align(Alignment.CenterVertically) - ) - } - } - } - AnimatedVisibility(visible = isTextInputVisible) { - OutlinedTextField( - value = textInputValue ?: "", - onValueChange = onTextInputChanged, - placeholder = { - Text(text = stringResource(id = R.string.users_questionnaire_onboarding_text_input_placeholder)) - }, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), - keyboardActions = KeyboardActions(onDone = { onDoneClick() }), - modifier = Modifier.fillMaxWidth() - ) - } - } -} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/entry_point/fragment/WelcomeOnboardingEntryPointFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/entry_point/fragment/WelcomeOnboardingEntryPointFragment.kt new file mode 100644 index 0000000000..9c756c55bf --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/entry_point/fragment/WelcomeOnboardingEntryPointFragment.kt @@ -0,0 +1,43 @@ +package org.hyperskill.app.android.welcome_onbaording.entry_point.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.android.core.view.ui.widget.compose.OnComposableShownFirstTime +import org.hyperskill.app.android.welcome_onbaording.entry_point.ui.WelcomeOnboardingEntryPoint +import org.hyperskill.app.android.welcome_onbaording.root.model.WelcomeOnboardingHost + +class WelcomeOnboardingEntryPointFragment : Fragment() { + companion object { + fun newInstance(): WelcomeOnboardingEntryPointFragment = + WelcomeOnboardingEntryPointFragment() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = + ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner)) + setContent { + OnComposableShownFirstTime(key = Unit, block = ::onShow) + HyperskillTheme { + WelcomeOnboardingEntryPoint(onStartClick = ::onStartClick) + } + } + } + + private fun onStartClick() { + (parentFragment as? WelcomeOnboardingHost)?.onStartClick() + } + + private fun onShow() { + (parentFragment as? WelcomeOnboardingHost)?.onStartOnboardingViewed() + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/entry_point/navigation/WelcomeOnboardingEntryPointScreen.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/entry_point/navigation/WelcomeOnboardingEntryPointScreen.kt new file mode 100644 index 0000000000..0de7f952e4 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/entry_point/navigation/WelcomeOnboardingEntryPointScreen.kt @@ -0,0 +1,11 @@ +package org.hyperskill.app.android.welcome_onbaording.entry_point.navigation + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import com.github.terrakok.cicerone.androidx.FragmentScreen +import org.hyperskill.app.android.welcome_onbaording.entry_point.fragment.WelcomeOnboardingEntryPointFragment + +object WelcomeOnboardingEntryPointScreen : FragmentScreen { + override fun createFragment(factory: FragmentFactory): Fragment = + WelcomeOnboardingEntryPointFragment.newInstance() +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/entry_point/ui/WelcomeOnboardingEntryPoint.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/entry_point/ui/WelcomeOnboardingEntryPoint.kt new file mode 100644 index 0000000000..042f63c7f4 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/entry_point/ui/WelcomeOnboardingEntryPoint.kt @@ -0,0 +1,83 @@ +package org.hyperskill.app.android.welcome_onbaording.entry_point.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.hyperskill.app.android.R +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillButton +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.android.welcome_onbaording.root.ui.WelcomeOnboardingDefault +import org.hyperskill.app.R as SharedR + +@Composable +fun WelcomeOnboardingEntryPoint( + modifier: Modifier = Modifier, + onStartClick: () -> Unit +) { + Column( + modifier = modifier.padding(horizontal = WelcomeOnboardingDefault.horizontalPadding) + ) { + Spacer(modifier = Modifier.height(44.dp)) + Column(modifier = Modifier.weight(1f)) { + Title(modifier = Modifier.align(Alignment.CenterHorizontally)) + Spacer(modifier = Modifier.height(16.dp)) + Description(modifier = Modifier.align(Alignment.CenterHorizontally)) + Spacer(modifier = Modifier.height(32.dp)) + Image( + painter = painterResource(id = R.drawable.img_welcome_onboarding_entry_point), + contentDescription = null, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + HyperskillButton( + onClick = onStartClick, + modifier = Modifier.fillMaxWidth().padding(bottom = WelcomeOnboardingDefault.buttonBottomPadding) + ) { + Text(text = stringResource(id = SharedR.string.welcome_onboarding_entry_point_start_btn)) + } + } +} + +@Composable +private fun Title(modifier: Modifier = Modifier) { + Text( + text = stringResource(id = SharedR.string.welcome_onboarding_entry_point_title), + style = WelcomeOnboardingDefault.titleTextStyle, + modifier = modifier + ) +} + +@Composable +private fun Description(modifier: Modifier = Modifier) { + Text( + text = stringResource(id = SharedR.string.welcome_onboarding_entry_point_description), + style = MaterialTheme.typography.body1, + fontSize = 14.sp, + color = colorResource(id = SharedR.color.text_secondary), + textAlign = TextAlign.Center, + modifier = modifier + ) +} + +@Preview +@Composable +private fun WelcomeOnboardingEntryPointPreview() { + HyperskillTheme { + WelcomeOnboardingEntryPoint {} + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/finish/fragment/WelcomeOnboardingFinishFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/finish/fragment/WelcomeOnboardingFinishFragment.kt new file mode 100644 index 0000000000..2398f02994 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/finish/fragment/WelcomeOnboardingFinishFragment.kt @@ -0,0 +1,66 @@ +package org.hyperskill.app.android.welcome_onbaording.finish.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import org.hyperskill.app.android.HyperskillApp +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.android.core.view.ui.widget.compose.OnComposableShownFirstTime +import org.hyperskill.app.android.welcome_onbaording.finish.ui.WelcomeOnboardingFinish +import org.hyperskill.app.android.welcome_onbaording.root.model.WelcomeOnboardingHost +import org.hyperskill.app.welcome_onboarding.finish.view.WelcomeOnboardingFinishViewStateMapper +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import ru.nobird.android.view.base.ui.extension.argument + +class WelcomeOnboardingFinishFragment : Fragment() { + companion object { + fun newInstance(track: WelcomeOnboardingTrack): WelcomeOnboardingFinishFragment = + WelcomeOnboardingFinishFragment().apply { + this.track = track + } + } + + private var track: WelcomeOnboardingTrack by argument() + + private var viewStateMapper: WelcomeOnboardingFinishViewStateMapper? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewStateMapper = + HyperskillApp + .graph() + .buildWelcomeOnboardingFinishComponent() + .welcomeOnboardingFinishViewStateMapper + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = + ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner)) + setContent { + HyperskillTheme { + val viewState = remember { + requireNotNull(viewStateMapper).map(track) + } + OnComposableShownFirstTime(key = Unit, block = ::onShow) + WelcomeOnboardingFinish(viewState, onFinishClick = ::onFinishClick) + } + } + } + + private fun onShow() { + (parentFragment as? WelcomeOnboardingHost)?.onFinishScreenViewed() + } + + private fun onFinishClick() { + (parentFragment as? WelcomeOnboardingHost)?.onFinishClicked() + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/finish/navigation/WelcomeOnboardingFinishScreen.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/finish/navigation/WelcomeOnboardingFinishScreen.kt new file mode 100644 index 0000000000..34cd23e225 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/finish/navigation/WelcomeOnboardingFinishScreen.kt @@ -0,0 +1,12 @@ +package org.hyperskill.app.android.welcome_onbaording.finish.navigation + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import com.github.terrakok.cicerone.androidx.FragmentScreen +import org.hyperskill.app.android.welcome_onbaording.finish.fragment.WelcomeOnboardingFinishFragment +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack + +class WelcomeOnboardingFinishScreen(val track: WelcomeOnboardingTrack) : FragmentScreen { + override fun createFragment(factory: FragmentFactory): Fragment = + WelcomeOnboardingFinishFragment.newInstance(track) +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/finish/ui/WelcomeOnboardingFinish.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/finish/ui/WelcomeOnboardingFinish.kt new file mode 100644 index 0000000000..ed8fe23973 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/finish/ui/WelcomeOnboardingFinish.kt @@ -0,0 +1,93 @@ +package org.hyperskill.app.android.welcome_onbaording.finish.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.hyperskill.app.android.R +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillButton +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.android.welcome_onbaording.root.ui.WelcomeOnboardingDefault +import org.hyperskill.app.welcome_onboarding.finish.view.WelcomeOnboardingFinishViewState + +@Composable +fun WelcomeOnboardingFinish( + viewState: WelcomeOnboardingFinishViewState, + onFinishClick: () -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .fillMaxSize() + .padding(horizontal = WelcomeOnboardingDefault.horizontalPadding) + ) { + Column( + modifier = Modifier.align(Alignment.Center) + ) { + Text( + text = viewState.title, + style = MaterialTheme.typography.h5, + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + color = colorResource(id = org.hyperskill.app.R.color.text_primary), + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = viewState.description, + style = MaterialTheme.typography.body1, + color = colorResource(id = org.hyperskill.app.R.color.color_on_surface_alpha_60), + fontSize = 14.sp, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Spacer(modifier = Modifier.height(32.dp)) + Image( + painter = painterResource(id = R.drawable.img_welcome_onboarding_finish), + contentDescription = null, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + HyperskillButton( + onClick = onFinishClick, + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(bottom = WelcomeOnboardingDefault.buttonBottomPadding) + ) { + Text(text = viewState.buttonText) + } + } +} + +@Composable +@Preview +private fun WelcomeOnboardingFinishPreview() { + HyperskillTheme { + WelcomeOnboardingFinish( + viewState = WelcomeOnboardingFinishViewState( + title = "You're all set!", + description = "Embark on your journey in '{track.title}' right now!", + buttonText = "Start my journey" + ), + onFinishClick = {} + ) + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/fragment/WelcomeOnboardingChooseProgrammingLanguageFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/fragment/WelcomeOnboardingChooseProgrammingLanguageFragment.kt new file mode 100644 index 0000000000..fdf0e9071c --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/fragment/WelcomeOnboardingChooseProgrammingLanguageFragment.kt @@ -0,0 +1,44 @@ +package org.hyperskill.app.android.welcome_onbaording.language.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.android.core.view.ui.widget.compose.OnComposableShownFirstTime +import org.hyperskill.app.android.welcome_onbaording.language.ui.WelcomeOnboardingChooseProgrammingLanguage +import org.hyperskill.app.android.welcome_onbaording.root.model.WelcomeOnboardingHost +import org.hyperskill.app.welcome_onboarding.root.model.WelcomeOnboardingProgrammingLanguage + +class WelcomeOnboardingChooseProgrammingLanguageFragment : Fragment() { + companion object { + fun newInstance(): WelcomeOnboardingChooseProgrammingLanguageFragment = + WelcomeOnboardingChooseProgrammingLanguageFragment() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = + ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner)) + setContent { + HyperskillTheme { + OnComposableShownFirstTime(key = Unit, block = ::onShow) + WelcomeOnboardingChooseProgrammingLanguage(onLangClick = ::onLanguageSelected) + } + } + } + + private fun onLanguageSelected(language: WelcomeOnboardingProgrammingLanguage) { + (parentFragment as? WelcomeOnboardingHost)?.onProgrammingLanguageSelected(language) + } + + private fun onShow() { + (parentFragment as? WelcomeOnboardingHost)?.onSelectProgrammingLanguageViewed() + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/navigation/WelcomeOnboardingChooseProgrammingLanguageScreen.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/navigation/WelcomeOnboardingChooseProgrammingLanguageScreen.kt new file mode 100644 index 0000000000..688bf87470 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/navigation/WelcomeOnboardingChooseProgrammingLanguageScreen.kt @@ -0,0 +1,11 @@ +package org.hyperskill.app.android.welcome_onbaording.language.navigation + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import com.github.terrakok.cicerone.androidx.FragmentScreen +import org.hyperskill.app.android.welcome_onbaording.language.fragment.WelcomeOnboardingChooseProgrammingLanguageFragment + +object WelcomeOnboardingChooseProgrammingLanguageScreen : FragmentScreen { + override fun createFragment(factory: FragmentFactory): Fragment = + WelcomeOnboardingChooseProgrammingLanguageFragment.newInstance() +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/ui/WelcomeOnboardingChooseProgrammingLanguage.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/ui/WelcomeOnboardingChooseProgrammingLanguage.kt new file mode 100644 index 0000000000..04e804a6d9 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/ui/WelcomeOnboardingChooseProgrammingLanguage.kt @@ -0,0 +1,97 @@ +package org.hyperskill.app.android.welcome_onbaording.language.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.hyperskill.app.R +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.android.welcome_onbaording.questionnaire.ui.WelcomeQuestionnaireItem +import org.hyperskill.app.welcome_onboarding.root.model.WelcomeOnboardingProgrammingLanguage + +@Composable +fun WelcomeOnboardingChooseProgrammingLanguage( + onLangClick: (WelcomeOnboardingProgrammingLanguage) -> Unit, + modifier: Modifier = Modifier +) { + val languages = remember { + WelcomeOnboardingProgrammingLanguage.values().toList() + } + Box(modifier = modifier.fillMaxSize()) { + Column( + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = 20.dp) + ) { + Text( + text = stringResource(id = R.string.welcome_onboarding_pick_language_title), + fontSize = 28.sp, + fontWeight = FontWeight.Bold, + color = colorResource(id = R.color.text_primary), + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 14.dp) + ) + Spacer(modifier = Modifier.height(32.dp)) + ProgrammingLanguageList( + items = languages, + onLangClick = onLangClick + ) + } + } +} + +@Composable +private fun ProgrammingLanguageList( + items: List, + onLangClick: (type: WelcomeOnboardingProgrammingLanguage) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.verticalScroll(rememberScrollState()) + ) { + items.forEachIndexed { i, item -> + WelcomeQuestionnaireItem( + title = item.title, + iconPainter = item.iconPainter, + onClick = { onLangClick(item) }, + modifier = Modifier.fillMaxWidth() + ) + Spacer( + modifier = Modifier.height( + if (i == items.lastIndex) { + 16.dp + } else { + 12.dp + } + ) + ) + } + } +} + +@Preview(showSystemUi = true) +@Composable +private fun WelcomeOnboardingChooseProgrammingLanguagePreview() { + HyperskillTheme { + WelcomeOnboardingChooseProgrammingLanguage(onLangClick = {}) + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/ui/WelcomeOnboardingProgrammingLanguageResources.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/ui/WelcomeOnboardingProgrammingLanguageResources.kt new file mode 100644 index 0000000000..cfe9731506 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/language/ui/WelcomeOnboardingProgrammingLanguageResources.kt @@ -0,0 +1,37 @@ +package org.hyperskill.app.android.welcome_onbaording.language.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import org.hyperskill.app.android.R +import org.hyperskill.app.welcome_onboarding.root.model.WelcomeOnboardingProgrammingLanguage +import org.hyperskill.app.R as SharedR + +val WelcomeOnboardingProgrammingLanguage.title: String + @Composable + get() { + val res = when (this) { + WelcomeOnboardingProgrammingLanguage.JAVA -> SharedR.string.welcome_onboarding_lang_java + WelcomeOnboardingProgrammingLanguage.JAVA_SCRIPT -> SharedR.string.welcome_onboarding_lang_js + WelcomeOnboardingProgrammingLanguage.KOTLIN -> SharedR.string.welcome_onboarding_lang_kotlin + WelcomeOnboardingProgrammingLanguage.PYTHON -> SharedR.string.welcome_onboarding_lang_python + WelcomeOnboardingProgrammingLanguage.SQL -> SharedR.string.welcome_onboarding_lang_sql + WelcomeOnboardingProgrammingLanguage.UNDEFINED -> SharedR.string.welcome_onboarding_lang_not_sure + } + return stringResource(id = res) + } + +val WelcomeOnboardingProgrammingLanguage.iconPainter: Painter + @Composable + get() { + val res = when (this) { + WelcomeOnboardingProgrammingLanguage.JAVA -> R.drawable.ic_welcome_onboarding_lang_java + WelcomeOnboardingProgrammingLanguage.JAVA_SCRIPT -> R.drawable.ic_welcome_onboarding_lang_js + WelcomeOnboardingProgrammingLanguage.KOTLIN -> R.drawable.ic_welcome_onboarding_lang_kotlin + WelcomeOnboardingProgrammingLanguage.PYTHON -> R.drawable.ic_welcome_onboarding_lang_python + WelcomeOnboardingProgrammingLanguage.SQL -> R.drawable.ic_welcome_onboarding_lang_sql + WelcomeOnboardingProgrammingLanguage.UNDEFINED -> R.drawable.ic_welcome_questionnaire_other + } + return painterResource(id = res) + } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/fragment/WelcomeQuestionnaireFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/fragment/WelcomeQuestionnaireFragment.kt new file mode 100644 index 0000000000..518d82378d --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/fragment/WelcomeQuestionnaireFragment.kt @@ -0,0 +1,71 @@ +package org.hyperskill.app.android.welcome_onbaording.questionnaire.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import org.hyperskill.app.android.HyperskillApp +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.android.core.view.ui.widget.compose.OnComposableShownFirstTime +import org.hyperskill.app.android.welcome_onbaording.questionnaire.ui.WelcomeQuestionnaire +import org.hyperskill.app.android.welcome_onbaording.root.model.WelcomeOnboardingHost +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireType +import org.hyperskill.app.welcome_onboarding.questionnaire.view.WelcomeQuestionnaireViewState +import org.hyperskill.app.welcome_onboarding.questionnaire.view.WelcomeQuestionnaireViewStateMapper +import ru.nobird.android.view.base.ui.extension.argument + +class WelcomeQuestionnaireFragment : Fragment() { + companion object { + fun newInstance(type: WelcomeQuestionnaireType): WelcomeQuestionnaireFragment = + WelcomeQuestionnaireFragment().apply { + this.type = type + } + } + + private var type: WelcomeQuestionnaireType by argument() + + private var viewStateMapper: WelcomeQuestionnaireViewStateMapper? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewStateMapper = + HyperskillApp + .graph() + .buildWelcomeQuestionnaireComponent() + .welcomeQuestionnaireViewStateMapper + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = + ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner)) + setContent { + val viewState: WelcomeQuestionnaireViewState = remember { + requireNotNull(viewStateMapper).mapQuestionnaireTypeToViewState(type) + } + OnComposableShownFirstTime(key = Unit, block = ::onShow) + HyperskillTheme { + WelcomeQuestionnaire(viewState, onItemClick = ::onItemClick) + } + } + } + + private fun onItemClick(itemType: WelcomeQuestionnaireItemType) { + (parentFragment as? WelcomeOnboardingHost)?.onQuestionnaireItemClicked( + questionnaireType = type, + itemType = itemType + ) + } + + private fun onShow() { + (parentFragment as? WelcomeOnboardingHost)?.onUserQuestionnaireViewed(type) + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/navigation/WelcomeQuestionnaireScreen.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/navigation/WelcomeQuestionnaireScreen.kt new file mode 100644 index 0000000000..69b44c27f7 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/navigation/WelcomeQuestionnaireScreen.kt @@ -0,0 +1,14 @@ +package org.hyperskill.app.android.welcome_onbaording.questionnaire.navigation + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import com.github.terrakok.cicerone.androidx.FragmentScreen +import org.hyperskill.app.android.welcome_onbaording.questionnaire.fragment.WelcomeQuestionnaireFragment +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireType + +class WelcomeQuestionnaireScreen( + private val type: WelcomeQuestionnaireType +) : FragmentScreen { + override fun createFragment(factory: FragmentFactory): Fragment = + WelcomeQuestionnaireFragment.newInstance(type) +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/ui/WelcomeQuestionnaire.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/ui/WelcomeQuestionnaire.kt new file mode 100644 index 0000000000..6e866f9c3b --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/ui/WelcomeQuestionnaire.kt @@ -0,0 +1,200 @@ +package org.hyperskill.app.android.welcome_onbaording.questionnaire.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.android.welcome_onbaording.root.ui.WelcomeOnboardingDefault +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType.ClientSource +import org.hyperskill.app.welcome_onboarding.questionnaire.view.WelcomeQuestionnaireItem +import org.hyperskill.app.welcome_onboarding.questionnaire.view.WelcomeQuestionnaireViewState + +@Composable +fun WelcomeQuestionnaire( + viewState: WelcomeQuestionnaireViewState, + modifier: Modifier = Modifier, + onItemClick: (type: WelcomeQuestionnaireItemType) -> Unit +) { + Box(modifier = modifier.fillMaxSize()) { + Column( + modifier = Modifier + .align(Alignment.Center) + .padding(horizontal = 20.dp) + ) { + Text( + text = viewState.title, + style = WelcomeOnboardingDefault.titleTextStyle, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 14.dp) + ) + Spacer(modifier = Modifier.height(32.dp)) + ItemsList( + items = viewState.items, + onItemClick = onItemClick + ) + } + } +} + +@Composable +private fun ItemsList( + items: List, + onItemClick: (type: WelcomeQuestionnaireItemType) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.verticalScroll(rememberScrollState()) + ) { + items.forEachIndexed { i, item -> + WelcomeQuestionnaireItem( + title = item.text, + iconPainter = item.type.iconPainter, + onClick = { onItemClick(item.type) }, + modifier = Modifier.fillMaxWidth() + ) + Spacer( + modifier = Modifier.height( + if (i == items.lastIndex) { + 16.dp + } else { + 12.dp + } + ) + ) + } + } +} + +private class WelcomeQuestionnairePreviewProvider : PreviewParameterProvider> { + override val values: Sequence> + get() = sequenceOf( + listOf( + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Tik tok" + ), + WelcomeQuestionnaireItem( + ClientSource.GOOGLE_SEARCH, + "Google search" + ), + WelcomeQuestionnaireItem( + ClientSource.NEWS, + "News" + ), + ), + listOf( + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Tik tok" + ), + WelcomeQuestionnaireItem( + ClientSource.GOOGLE_SEARCH, + "Google search" + ), + WelcomeQuestionnaireItem( + ClientSource.NEWS, + "News" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + ), + listOf( + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Tik tok" + ), + WelcomeQuestionnaireItem( + ClientSource.GOOGLE_SEARCH, + "Google search" + ), + WelcomeQuestionnaireItem( + ClientSource.NEWS, + "News" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ), + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + "Some long text Some long text Some long text" + ) + ) + ) +} + +@Composable +@Preview(showSystemUi = true) +private fun WelcomeQuestionnairePreview( + @PreviewParameter(WelcomeQuestionnairePreviewProvider::class) items: List +) { + HyperskillTheme { + WelcomeQuestionnaire( + WelcomeQuestionnaireViewState( + title = "How did you hear about Hyperskill?", + items = items + ), + onItemClick = {} + ) + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/ui/WelcomeQuestionnaireItem.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/ui/WelcomeQuestionnaireItem.kt new file mode 100644 index 0000000000..c9dd5030a6 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/ui/WelcomeQuestionnaireItem.kt @@ -0,0 +1,73 @@ +package org.hyperskill.app.android.welcome_onbaording.questionnaire.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.requiredSize +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.view.HapticFeedbackConstantsCompat +import org.hyperskill.app.android.R +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillCard +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme + +@Composable +fun WelcomeQuestionnaireItem( + title: String, + iconPainter: Painter, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + val view = LocalView.current + val currentOnClick by rememberUpdatedState( + newValue = { + view.performHapticFeedback(HapticFeedbackConstantsCompat.CONTEXT_CLICK) + onClick() + } + ) + HyperskillCard( + modifier = modifier, + onClick = currentOnClick + ) { + Row( + modifier = Modifier + .padding( + horizontal = 16.dp, + vertical = 12.dp + ) + ) { + Image( + painter = iconPainter, + contentDescription = null, + modifier = Modifier + .requiredSize(16.dp) + .align(Alignment.CenterVertically) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = title) + } + } +} + +@Composable +@Preview(showBackground = true) +private fun WelcomeQuestionnaireItemPreview() { + HyperskillTheme { + WelcomeQuestionnaireItem( + title = "Tik tok Tik tok Tik tok", + iconPainter = painterResource(id = R.drawable.ic_welcome_questionnaire_tiktok), + onClick = {} + ) + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/ui/WelcomeQuestionnaireItemIcon.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/ui/WelcomeQuestionnaireItemIcon.kt new file mode 100644 index 0000000000..09874cea3b --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/questionnaire/ui/WelcomeQuestionnaireItemIcon.kt @@ -0,0 +1,36 @@ +package org.hyperskill.app.android.welcome_onbaording.questionnaire.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import org.hyperskill.app.android.R +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType.ClientSource +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType.CodingBackground +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType.LearningGoal + +val WelcomeQuestionnaireItemType.iconPainter: Painter + @Composable + get() { + val res: Int = when (this) { + ClientSource.TIK_TOK -> R.drawable.ic_welcome_questionnaire_tiktok + ClientSource.GOOGLE_SEARCH -> R.drawable.ic_welcome_questionnaire_google + ClientSource.NEWS -> R.drawable.ic_welcome_questionnaire_news + ClientSource.APP_STORE -> R.drawable.ic_welcome_questionnaire_play_store + ClientSource.FACEBOOK -> R.drawable.ic_welcome_questionnaire_facebook + ClientSource.FRIENDS -> R.drawable.ic_welcome_questionnaire_friends + ClientSource.YOUTUBE -> R.drawable.ic_welcome_questionnaire_youtube + ClientSource.OTHER -> R.drawable.ic_welcome_questionnaire_other + LearningGoal.START_CAREER -> R.drawable.ic_welcome_questionnaire_start_career + LearningGoal.CURRENT_JOB -> R.drawable.ic_welcome_questionnaire_current_job + LearningGoal.CHANGE_STACK -> R.drawable.ic_welcome_questionnaire_change_stack + LearningGoal.STUDIES -> R.drawable.ic_welcome_questionnaire_studies + LearningGoal.FUN -> R.drawable.ic_welcome_questionnaire_fun + LearningGoal.OTHER -> R.drawable.ic_welcome_questionnaire_other_learning_goal + CodingBackground.NO_CODING_EXPERIENCE -> R.drawable.ic_welcome_questionnaire_no_experience + CodingBackground.BASIC_UNDERSTANDING -> R.drawable.ic_welcome_questionnaire_basic_experience + CodingBackground.WRITTEN_SOME_PROJECTS -> R.drawable.ic_welcome_questionnaire_medium_experience + CodingBackground.WORKING_PROFESSIONALLY -> R.drawable.ic_welcome_questionnaire_pro + } + return painterResource(id = res) + } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/fragment/WelcomeOnboardingFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/fragment/WelcomeOnboardingFragment.kt new file mode 100644 index 0000000000..9079a7b9be --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/fragment/WelcomeOnboardingFragment.kt @@ -0,0 +1,166 @@ +package org.hyperskill.app.android.welcome_onbaording.root.fragment + +import android.annotation.SuppressLint +import android.content.pm.PackageManager +import android.os.Bundle +import android.view.View +import androidx.core.content.ContextCompat +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import org.hyperskill.app.android.HyperskillApp +import org.hyperskill.app.android.core.extensions.argument +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.requireAppRouter +import org.hyperskill.app.android.notification_onboarding.navigation.NotificationsOnboardingScreen +import org.hyperskill.app.android.welcome_onbaording.entry_point.navigation.WelcomeOnboardingEntryPointScreen +import org.hyperskill.app.android.welcome_onbaording.finish.navigation.WelcomeOnboardingFinishScreen +import org.hyperskill.app.android.welcome_onbaording.language.navigation.WelcomeOnboardingChooseProgrammingLanguageScreen +import org.hyperskill.app.android.welcome_onbaording.questionnaire.navigation.WelcomeQuestionnaireScreen +import org.hyperskill.app.android.welcome_onbaording.root.model.WelcomeOnboardingCompleteResult +import org.hyperskill.app.android.welcome_onbaording.root.model.WelcomeOnboardingHost +import org.hyperskill.app.android.welcome_onbaording.track.navigation.WelcomeOnboardingTrackDetailsScreen +import org.hyperskill.app.core.view.handleActions +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingFeatureParams +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireType +import org.hyperskill.app.welcome_onboarding.root.model.WelcomeOnboardingProgrammingLanguage +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Action.ViewAction +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingViewModel +import ru.nobird.android.view.base.ui.extension.showIfNotExists +import ru.nobird.android.view.navigation.ui.fragment.FlowFragment + +class WelcomeOnboardingFragment : FlowFragment(), WelcomeOnboardingHost { + companion object { + const val WELCOME_ONBOARDING_COMPLETED = "WELCOME_ONBOARDING_COMPLETED" + fun newInstance(params: WelcomeOnboardingFeatureParams): WelcomeOnboardingFragment = + WelcomeOnboardingFragment().apply { + this.params = params + } + } + + private var viewModelFactory: ViewModelProvider.Factory? = null + private val welcomeOnboardingViewModel: WelcomeOnboardingViewModel by viewModels { + requireNotNull(viewModelFactory) + } + + private var params: WelcomeOnboardingFeatureParams by argument(WelcomeOnboardingFeatureParams.serializer()) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + injectComponent() + welcomeOnboardingViewModel.handleActions(this, ::onAction) + } + + private fun injectComponent() { + viewModelFactory = + HyperskillApp + .graph() + .buildPlatformWelcomeOnboardingComponent(params) + .reduxViewModelFactory + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + welcomeOnboardingViewModel + .state + .flowWithLifecycle(viewLifecycleOwner.lifecycle) + .distinctUntilChanged() + .onEach(::render) + .launchIn(viewLifecycleOwner.lifecycleScope) + } + + private fun render(state: WelcomeOnboardingFeature.State) { + if (state.isNextLearningActivityLoadingShown) { + LoadingProgressDialogFragment.newInstance() + .showIfNotExists(childFragmentManager, LoadingProgressDialogFragment.TAG) + } else { + childFragmentManager.dismissDialogFragmentIfExists(LoadingProgressDialogFragment.TAG) + } + } + + override fun onStartClick() { + welcomeOnboardingViewModel.onStartClick() + } + + override fun onQuestionnaireItemClicked( + questionnaireType: WelcomeQuestionnaireType, + itemType: WelcomeQuestionnaireItemType + ) { + welcomeOnboardingViewModel.onQuestionnaireItemClicked(questionnaireType, itemType) + } + + override fun onProgrammingLanguageSelected(language: WelcomeOnboardingProgrammingLanguage) { + welcomeOnboardingViewModel.onLanguageSelected(language) + } + + @SuppressLint("InlinedApi") + override fun onTrackSelected(track: WelcomeOnboardingTrack) { + welcomeOnboardingViewModel.onTrackSelected( + track, + isNotificationPermissionGranted = ContextCompat.checkSelfPermission( + requireContext(), + android.Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) + } + + override fun onNotificationOnboardingCompleted() { + welcomeOnboardingViewModel.onNotificationPermissionCompleted() + } + + override fun onFinishScreenViewed() { + welcomeOnboardingViewModel.onFinishOnboardingShowed() + } + + override fun onFinishClicked() { + welcomeOnboardingViewModel.onFinishClicked() + } + + override fun onStartOnboardingViewed() { + welcomeOnboardingViewModel.onStartOnboardingViewed() + } + + override fun onUserQuestionnaireViewed(questionnaireType: WelcomeQuestionnaireType) { + welcomeOnboardingViewModel.onUserQuestionnaireViewed(questionnaireType) + } + + override fun onSelectProgrammingLanguageViewed() { + welcomeOnboardingViewModel.onSelectProgrammingLanguageViewed() + } + + private fun onAction(action: ViewAction) { + when (action) { + ViewAction.NavigateTo.StartScreen -> + router.newRootScreen(WelcomeOnboardingEntryPointScreen) + is ViewAction.NavigateTo.WelcomeOnboardingQuestionnaire -> + router.newRootScreen(WelcomeQuestionnaireScreen(action.type)) + ViewAction.NavigateTo.ChooseProgrammingLanguage -> + router.newRootScreen(WelcomeOnboardingChooseProgrammingLanguageScreen) + is ViewAction.NavigateTo.TrackDetails -> + router.navigateTo(WelcomeOnboardingTrackDetailsScreen(action.track)) + ViewAction.NavigateTo.NotificationOnboarding -> + router.newRootScreen(NotificationsOnboardingScreen) + is ViewAction.NavigateTo.OnboardingFinish -> + router.newRootScreen(WelcomeOnboardingFinishScreen(action.selectedTrack)) + is ViewAction.CompleteWelcomeOnboarding -> { + val stepRoute = action.stepRoute + requireAppRouter().sendResult( + WELCOME_ONBOARDING_COMPLETED, + if (stepRoute == null) { + WelcomeOnboardingCompleteResult.Empty + } else { + WelcomeOnboardingCompleteResult.StepRoute(stepRoute) + } + ) + } + } + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/model/WelcomeOnboardingCompleteResult.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/model/WelcomeOnboardingCompleteResult.kt new file mode 100644 index 0000000000..8a0deef62c --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/model/WelcomeOnboardingCompleteResult.kt @@ -0,0 +1,8 @@ +package org.hyperskill.app.android.welcome_onbaording.root.model + +sealed interface WelcomeOnboardingCompleteResult { + object Empty : WelcomeOnboardingCompleteResult + data class StepRoute( + val stepRoute: org.hyperskill.app.step.domain.model.StepRoute + ) : WelcomeOnboardingCompleteResult +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/model/WelcomeOnboardingHost.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/model/WelcomeOnboardingHost.kt new file mode 100644 index 0000000000..96e05266f9 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/model/WelcomeOnboardingHost.kt @@ -0,0 +1,23 @@ +package org.hyperskill.app.android.welcome_onbaording.root.model + +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireType +import org.hyperskill.app.welcome_onboarding.root.model.WelcomeOnboardingProgrammingLanguage + +interface WelcomeOnboardingHost { + fun onStartClick() + fun onQuestionnaireItemClicked( + questionnaireType: WelcomeQuestionnaireType, + itemType: WelcomeQuestionnaireItemType + ) + fun onProgrammingLanguageSelected(language: WelcomeOnboardingProgrammingLanguage) + fun onTrackSelected(track: WelcomeOnboardingTrack) + fun onNotificationOnboardingCompleted() + fun onFinishClicked() + + fun onStartOnboardingViewed() + fun onUserQuestionnaireViewed(questionnaireType: WelcomeQuestionnaireType) + fun onSelectProgrammingLanguageViewed() + fun onFinishScreenViewed() +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/navigation/WelcomeOnboardingScreen.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/navigation/WelcomeOnboardingScreen.kt new file mode 100644 index 0000000000..e2378e8945 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/navigation/WelcomeOnboardingScreen.kt @@ -0,0 +1,12 @@ +package org.hyperskill.app.android.welcome_onbaording.root.navigation + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import com.github.terrakok.cicerone.androidx.FragmentScreen +import org.hyperskill.app.android.welcome_onbaording.root.fragment.WelcomeOnboardingFragment +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingFeatureParams + +class WelcomeOnboardingScreen(private val params: WelcomeOnboardingFeatureParams) : FragmentScreen { + override fun createFragment(factory: FragmentFactory): Fragment = + WelcomeOnboardingFragment.newInstance(params) +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/ui/WelcomeOnboardingDefault.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/ui/WelcomeOnboardingDefault.kt new file mode 100644 index 0000000000..3c27a1d1a7 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/root/ui/WelcomeOnboardingDefault.kt @@ -0,0 +1,23 @@ +package org.hyperskill.app.android.welcome_onbaording.root.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.hyperskill.app.R + +object WelcomeOnboardingDefault { + val horizontalPadding: Dp = 20.dp + val buttonBottomPadding: Dp = 32.dp + + val titleTextStyle: TextStyle + @Composable + get() = TextStyle( + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + color = colorResource(id = R.color.text_primary) + ) +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/fragment/WelcomeOnboardingTrackDetailsFragment.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/fragment/WelcomeOnboardingTrackDetailsFragment.kt new file mode 100644 index 0000000000..978e0a8995 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/fragment/WelcomeOnboardingTrackDetailsFragment.kt @@ -0,0 +1,78 @@ +package org.hyperskill.app.android.welcome_onbaording.track.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModelProvider +import org.hyperskill.app.android.HyperskillApp +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.android.welcome_onbaording.root.model.WelcomeOnboardingHost +import org.hyperskill.app.android.welcome_onbaording.track.ui.WelcomeOnboardingTrackDetails +import org.hyperskill.app.core.view.handleActions +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Action.ViewAction +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsViewModel +import ru.nobird.android.view.base.ui.extension.argument +import ru.nobird.android.view.base.ui.extension.snackbar + +class WelcomeOnboardingTrackDetailsFragment : Fragment() { + companion object { + fun newInstance( + track: WelcomeOnboardingTrack + ): WelcomeOnboardingTrackDetailsFragment = + WelcomeOnboardingTrackDetailsFragment().apply { + this.track = track + } + } + + private var track: WelcomeOnboardingTrack by argument() + + private var viewModelProvider: ViewModelProvider.Factory? = null + private val welcomeOnboardingTrackDetailsViewModel: WelcomeOnboardingTrackDetailsViewModel by viewModels { + requireNotNull(viewModelProvider) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + injectComponent() + welcomeOnboardingTrackDetailsViewModel.handleActions(this, ::onAction) + } + + private fun injectComponent() { + viewModelProvider = + HyperskillApp + .graph() + .buildPlatformWelcomeOnboardingTrackDetailsComponent(track) + .reduxViewModelFactory + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = + ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnLifecycleDestroyed(viewLifecycleOwner)) + setContent { + HyperskillTheme { + WelcomeOnboardingTrackDetails(welcomeOnboardingTrackDetailsViewModel) + } + } + } + + private fun onAction(action: ViewAction) { + when (action) { + ViewAction.NotifyTrackSelected -> { + (parentFragment as? WelcomeOnboardingHost)?.onTrackSelected(track) + } + ViewAction.ShowTrackSelectionError -> { + requireView().snackbar(org.hyperskill.app.R.string.common_error) + } + } + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/navigation/WelcomeOnboardingTrackDetailsScreen.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/navigation/WelcomeOnboardingTrackDetailsScreen.kt new file mode 100644 index 0000000000..5e13dacd38 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/navigation/WelcomeOnboardingTrackDetailsScreen.kt @@ -0,0 +1,14 @@ +package org.hyperskill.app.android.welcome_onbaording.track.navigation + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentFactory +import com.github.terrakok.cicerone.androidx.FragmentScreen +import org.hyperskill.app.android.welcome_onbaording.track.fragment.WelcomeOnboardingTrackDetailsFragment +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack + +class WelcomeOnboardingTrackDetailsScreen( + private val track: WelcomeOnboardingTrack +) : FragmentScreen { + override fun createFragment(factory: FragmentFactory): Fragment = + WelcomeOnboardingTrackDetailsFragment.newInstance(track) +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackCard.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackCard.kt new file mode 100644 index 0000000000..b3b7c28145 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackCard.kt @@ -0,0 +1,83 @@ +package org.hyperskill.app.android.welcome_onbaording.track.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.hyperskill.app.android.R +import org.hyperskill.app.android.core.view.ui.widget.compose.HtmlText +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillCard +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.R as SharedR + +@Composable +fun WelcomeOnboardingTrackCard( + imagePainter: Painter, + trackTitle: String, + trackDescription: String, + modifier: Modifier = Modifier +) { + HyperskillCard(modifier) { + Column(modifier = Modifier.padding(bottom = 12.dp)) { + Image( + painter = imagePainter, + contentDescription = null, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = trackTitle, + style = MaterialTheme.typography.h4, + fontWeight = FontWeight.Bold, + fontSize = 28.sp, + color = colorResource(id = SharedR.color.color_on_surface_alpha_87), + textAlign = TextAlign.Center, + modifier = Modifier + .padding(horizontal = 12.dp) + .align(Alignment.CenterHorizontally) + ) + Spacer(modifier = Modifier.height(12.dp)) + HtmlText(text = trackDescription) { annotatedString -> + Text( + text = annotatedString, + style = MaterialTheme.typography.body1, + color = colorResource(id = SharedR.color.color_on_surface_alpha_60), + fontSize = 14.sp, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(12.dp) + .align(Alignment.CenterHorizontally) + ) + } + } + } +} + +@Suppress("MaxLineLength") +@Composable +@Preview(showSystemUi = true) +private fun WelcomeOnboardingTrackCardPreview() { + HyperskillTheme { + WelcomeOnboardingTrackCard( + imagePainter = painterResource(id = R.drawable.img_onboarding_choose_track_kotlin), + trackTitle = stringResource(id = SharedR.string.welcome_onboarding_track_details_kotlin_title), + trackDescription = stringResource(id = SharedR.string.welcome_onboarding_track_details_kotlin_description), + modifier = Modifier.padding(20.dp) + ) + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackDetails.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackDetails.kt new file mode 100644 index 0000000000..284f7faf7b --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackDetails.kt @@ -0,0 +1,123 @@ +package org.hyperskill.app.android.welcome_onbaording.track.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import org.hyperskill.app.R +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillButton +import org.hyperskill.app.android.core.view.ui.widget.compose.HyperskillTheme +import org.hyperskill.app.android.core.view.ui.widget.compose.OnComposableShownFirstTime +import org.hyperskill.app.android.core.view.ui.widget.compose.shimmer +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsViewModel + +@Composable +fun WelcomeOnboardingTrackDetails( + viewModel: WelcomeOnboardingTrackDetailsViewModel, + modifier: Modifier = Modifier +) { + OnComposableShownFirstTime(key = Unit, block = viewModel::onShow) + val viewState by viewModel.state.collectAsStateWithLifecycle() + WelcomeOnboardingTrackDetails( + viewState = viewState, + onSelectClick = viewModel::onContinueClick, + modifier = modifier + ) +} + +@Composable +fun WelcomeOnboardingTrackDetails( + viewState: WelcomeOnboardingTrackDetailsFeature.ViewState, + onSelectClick: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.padding(WelcomeOnboardingTrackDetailsDefaults.padding) + ) { + Column( + modifier = Modifier + .weight(1f) + .verticalScroll(rememberScrollState()) + ) { + Text( + text = viewState.title, + style = MaterialTheme.typography.h5, + fontSize = 24.sp, + fontWeight = FontWeight.SemiBold, + color = colorResource(id = R.color.text_primary), + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + Spacer(modifier = Modifier.height(24.dp)) + Box(modifier = Modifier.weight(1f)) { + Column(modifier = Modifier.align(Alignment.Center)) { + WelcomeOnboardingTrackCard( + imagePainter = viewState.track.imagePainter, + trackTitle = viewState.trackTitle, + trackDescription = viewState.trackDescriptionHtml + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = viewState.changeText, + style = MaterialTheme.typography.body2, + color = colorResource(id = R.color.text_secondary), + fontSize = 12.sp, + textAlign = TextAlign.Center, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + } + } + } + HyperskillButton( + onClick = onSelectClick, + enabled = !viewState.isLoadingShowed, + modifier = Modifier + .fillMaxWidth() + .shimmer(isLoading = viewState.isLoadingShowed) + ) { + Text(text = viewState.buttonText) + } + } +} + +@Suppress("MaxLineLength") +@Composable +@Preview(showSystemUi = true) +private fun WelcomeOnboardingTrackDetailsPreview() { + HyperskillTheme { + WelcomeOnboardingTrackDetails( + WelcomeOnboardingTrackDetailsFeature.ViewState( + track = WelcomeOnboardingTrack.KOTLIN, + title = stringResource(id = R.string.welcome_onboarding_track_details_title), + trackTitle = stringResource(id = R.string.welcome_onboarding_track_details_kotlin_title), + trackDescriptionHtml = stringResource( + id = R.string.welcome_onboarding_track_details_kotlin_description + ), + changeText = stringResource(id = R.string.welcome_onboarding_track_details_change_text), + buttonText = stringResource(id = R.string.welcome_onboarding_track_details_continue_btn), + isLoadingShowed = false + ), + onSelectClick = {} + ) + } +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackDetailsDefaults.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackDetailsDefaults.kt new file mode 100644 index 0000000000..58eea563c7 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackDetailsDefaults.kt @@ -0,0 +1,14 @@ +package org.hyperskill.app.android.welcome_onbaording.track.ui + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.ui.unit.dp +import org.hyperskill.app.android.welcome_onbaording.root.ui.WelcomeOnboardingDefault + +object WelcomeOnboardingTrackDetailsDefaults { + val padding: PaddingValues = PaddingValues( + top = 16.dp, + bottom = WelcomeOnboardingDefault.buttonBottomPadding, + start = WelcomeOnboardingDefault.horizontalPadding, + end = WelcomeOnboardingDefault.horizontalPadding + ) +} \ No newline at end of file diff --git a/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackImage.kt b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackImage.kt new file mode 100644 index 0000000000..9282208495 --- /dev/null +++ b/androidHyperskillApp/src/main/java/org/hyperskill/app/android/welcome_onbaording/track/ui/WelcomeOnboardingTrackImage.kt @@ -0,0 +1,20 @@ +package org.hyperskill.app.android.welcome_onbaording.track.ui + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource +import org.hyperskill.app.android.R +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack + +val WelcomeOnboardingTrack.imagePainter: Painter + @Composable + get() { + val res = when (this) { + WelcomeOnboardingTrack.JAVA -> R.drawable.img_onboarding_choose_track_java + WelcomeOnboardingTrack.JAVA_SCRIPT -> R.drawable.img_onboarding_choose_track_js + WelcomeOnboardingTrack.KOTLIN -> R.drawable.img_onboarding_choose_track_kotlin + WelcomeOnboardingTrack.PYTHON -> R.drawable.img_onboarding_choose_track_python + WelcomeOnboardingTrack.SQL -> R.drawable.img_onboarding_choose_track_sql + } + return painterResource(id = res) + } \ No newline at end of file diff --git a/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_java.webp b/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_java.webp new file mode 100644 index 0000000000..7ebd8a0cd8 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_java.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_js.webp b/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_js.webp new file mode 100644 index 0000000000..8c670af8cd Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_js.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_kotlin.webp b/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_kotlin.webp new file mode 100644 index 0000000000..6e3f487791 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_kotlin.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_python.webp b/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_python.webp new file mode 100644 index 0000000000..2e0043f73a Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_python.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_sql.webp b/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_sql.webp new file mode 100644 index 0000000000..890c54dddb Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-hdpi/img_onboarding_choose_track_sql.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-hdpi/img_welcome_onboarding_entry_point.webp b/androidHyperskillApp/src/main/res/drawable-hdpi/img_welcome_onboarding_entry_point.webp new file mode 100644 index 0000000000..907910cbba Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-hdpi/img_welcome_onboarding_entry_point.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-hdpi/img_welcome_onboarding_finish.webp b/androidHyperskillApp/src/main/res/drawable-hdpi/img_welcome_onboarding_finish.webp new file mode 100644 index 0000000000..e9e0f923e2 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-hdpi/img_welcome_onboarding_finish.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_java.webp b/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_java.webp new file mode 100644 index 0000000000..65dd169bd7 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_java.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_js.webp b/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_js.webp new file mode 100644 index 0000000000..13059607ae Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_js.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_kotlin.webp b/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_kotlin.webp new file mode 100644 index 0000000000..a50b7491df Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_kotlin.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_python.webp b/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_python.webp new file mode 100644 index 0000000000..427d470d45 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_python.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_sql.webp b/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_sql.webp new file mode 100644 index 0000000000..dfe4a2ad2e Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-mdpi/img_onboarding_choose_track_sql.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-mdpi/img_welcome_onboarding_entry_point.webp b/androidHyperskillApp/src/main/res/drawable-mdpi/img_welcome_onboarding_entry_point.webp new file mode 100644 index 0000000000..cda03e0f63 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-mdpi/img_welcome_onboarding_entry_point.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-mdpi/img_welcome_onboarding_finish.webp b/androidHyperskillApp/src/main/res/drawable-mdpi/img_welcome_onboarding_finish.webp new file mode 100644 index 0000000000..bdb9136afa Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-mdpi/img_welcome_onboarding_finish.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-night/ic_welcome_questionnaire_tiktok.xml b/androidHyperskillApp/src/main/res/drawable-night/ic_welcome_questionnaire_tiktok.xml new file mode 100644 index 0000000000..946989ad05 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable-night/ic_welcome_questionnaire_tiktok.xml @@ -0,0 +1,9 @@ + + + diff --git a/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_java.webp b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_java.webp new file mode 100644 index 0000000000..b7b1ccc0ee Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_java.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_js.webp b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_js.webp new file mode 100644 index 0000000000..416c368146 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_js.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_kotlin.webp b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_kotlin.webp new file mode 100644 index 0000000000..ceeeb09363 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_kotlin.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_python.webp b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_python.webp new file mode 100644 index 0000000000..29bdd180cd Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_python.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_sql.webp b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_sql.webp new file mode 100644 index 0000000000..6e481f20ba Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_onboarding_choose_track_sql.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xhdpi/img_welcome_onboarding_entry_point.webp b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_welcome_onboarding_entry_point.webp new file mode 100644 index 0000000000..bb1b80938a Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_welcome_onboarding_entry_point.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xhdpi/img_welcome_onboarding_finish.webp b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_welcome_onboarding_finish.webp new file mode 100644 index 0000000000..fdfbc76b42 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xhdpi/img_welcome_onboarding_finish.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_java.webp b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_java.webp new file mode 100644 index 0000000000..0298339b43 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_java.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_js.webp b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_js.webp new file mode 100644 index 0000000000..2137295250 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_js.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_kotlin.webp b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_kotlin.webp new file mode 100644 index 0000000000..61e3cfcddd Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_kotlin.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_python.webp b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_python.webp new file mode 100644 index 0000000000..26a43f35cb Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_python.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_sql.webp b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_sql.webp new file mode 100644 index 0000000000..d388fd446b Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_onboarding_choose_track_sql.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_welcome_onboarding_entry_point.webp b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_welcome_onboarding_entry_point.webp new file mode 100644 index 0000000000..53c077c4c3 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_welcome_onboarding_entry_point.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_welcome_onboarding_finish.webp b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_welcome_onboarding_finish.webp new file mode 100644 index 0000000000..0e393f02da Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxhdpi/img_welcome_onboarding_finish.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_java.webp b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_java.webp new file mode 100644 index 0000000000..376fb7f44f Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_java.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_js.webp b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_js.webp new file mode 100644 index 0000000000..87e6abdd8c Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_js.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_kotlin.webp b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_kotlin.webp new file mode 100644 index 0000000000..ee170b1da1 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_kotlin.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_python.webp b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_python.webp new file mode 100644 index 0000000000..df4092d59b Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_python.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_sql.webp b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_sql.webp new file mode 100644 index 0000000000..d411483cb5 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_onboarding_choose_track_sql.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_welcome_onboarding_entry_point.webp b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_welcome_onboarding_entry_point.webp new file mode 100644 index 0000000000..4d92187c7c Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_welcome_onboarding_entry_point.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_welcome_onboarding_finish.webp b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_welcome_onboarding_finish.webp new file mode 100644 index 0000000000..4e34667ec1 Binary files /dev/null and b/androidHyperskillApp/src/main/res/drawable-xxxhdpi/img_welcome_onboarding_finish.webp differ diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_java.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_java.xml new file mode 100644 index 0000000000..b6925b19d3 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_java.xml @@ -0,0 +1,9 @@ + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_js.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_js.xml new file mode 100644 index 0000000000..e67707059d --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_js.xml @@ -0,0 +1,9 @@ + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_kotlin.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_kotlin.xml new file mode 100644 index 0000000000..3e3f316e8d --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_kotlin.xml @@ -0,0 +1,18 @@ + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_python.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_python.xml new file mode 100644 index 0000000000..6f41ab21bd --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_python.xml @@ -0,0 +1,9 @@ + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_sql.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_sql.xml new file mode 100644 index 0000000000..09254cbfbc --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_onboarding_lang_sql.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_basic_experience.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_basic_experience.xml new file mode 100644 index 0000000000..a3cc32e265 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_basic_experience.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_change_stack.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_change_stack.xml new file mode 100644 index 0000000000..181e878b96 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_change_stack.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_current_job.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_current_job.xml new file mode 100644 index 0000000000..1bf6c7bc63 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_current_job.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_facebook.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_facebook.xml new file mode 100644 index 0000000000..c2ee166ca0 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_facebook.xml @@ -0,0 +1,10 @@ + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_friends.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_friends.xml new file mode 100644 index 0000000000..fd57314154 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_friends.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_fun.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_fun.xml new file mode 100644 index 0000000000..0760cb1ec0 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_fun.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_google.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_google.xml new file mode 100644 index 0000000000..f94fd880ca --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_google.xml @@ -0,0 +1,22 @@ + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_medium_experience.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_medium_experience.xml new file mode 100644 index 0000000000..a024640074 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_medium_experience.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_news.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_news.xml new file mode 100644 index 0000000000..3186c554c6 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_news.xml @@ -0,0 +1,280 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_no_experience.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_no_experience.xml new file mode 100644 index 0000000000..6aa67ec603 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_no_experience.xml @@ -0,0 +1,12 @@ + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_other.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_other.xml new file mode 100644 index 0000000000..b924f2a6bc --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_other.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_other_learning_goal.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_other_learning_goal.xml new file mode 100644 index 0000000000..5ca74f490b --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_other_learning_goal.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_play_store.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_play_store.xml new file mode 100644 index 0000000000..a7ce4489da --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_play_store.xml @@ -0,0 +1,30 @@ + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_pro.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_pro.xml new file mode 100644 index 0000000000..8ed5a2de09 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_pro.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_start_career.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_start_career.xml new file mode 100644 index 0000000000..31c9ced33b --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_start_career.xml @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_studies.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_studies.xml new file mode 100644 index 0000000000..e1644964e0 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_studies.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_tiktok.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_tiktok.xml new file mode 100644 index 0000000000..2e909f9c72 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_tiktok.xml @@ -0,0 +1,16 @@ + + + + + + diff --git a/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_youtube.xml b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_youtube.xml new file mode 100644 index 0000000000..5f972e2831 --- /dev/null +++ b/androidHyperskillApp/src/main/res/drawable/ic_welcome_questionnaire_youtube.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/androidHyperskillApp/src/test/java/org/hyperskill/app/android/util/AppFeatureStateSerializationTest.kt b/androidHyperskillApp/src/test/java/org/hyperskill/app/android/util/AppFeatureStateSerializationTest.kt index 4e42bef269..1b3f3c3a52 100644 --- a/androidHyperskillApp/src/test/java/org/hyperskill/app/android/util/AppFeatureStateSerializationTest.kt +++ b/androidHyperskillApp/src/test/java/org/hyperskill/app/android/util/AppFeatureStateSerializationTest.kt @@ -21,7 +21,7 @@ class AppFeatureStateSerializationTest { isMobileOnlySubscriptionEnabled = true, canMakePayments = true ) - else -> throw IllegalStateException("Unknown state class: $stateClass. Please add it to the test.") + else -> error("Unknown state class: $stateClass. Please add it to the test.") } val json = Json.encodeToString(AppFeature.State.serializer(), state) val decodedState = Json.decodeFromString(AppFeature.State.serializer(), json) diff --git a/config/detekt/baseline.xml b/config/detekt/baseline.xml index 2b8641214e..6529a2e327 100644 --- a/config/detekt/baseline.xml +++ b/config/detekt/baseline.xml @@ -56,6 +56,7 @@ LongMethod:ChallengeCard.kt$@Composable fun ChallengeCard( viewState: ChallengeWidgetViewState, onNewMessage: (Message) -> Unit ) LongMethod:DefaultStepQuizFragment.kt$DefaultStepQuizFragment$override fun onAction(action: StepQuizFeature.Action.ViewAction) LongMethod:LeaderboardSkeletonItem.kt$@Composable fun LeaderboardSkeletonItem( placeNumber: Int?, modifier: Modifier = Modifier ) + LongMethod:LegacyAppReducer.kt$LegacyAppReducer$private fun handleFetchAppStartupConfigSuccess( state: State, message: Message.FetchAppStartupConfigSuccess ): ReducerResult LongMethod:LoadingView.kt$LoadingView$override fun onDraw(canvas: Canvas) LongMethod:MainActivity.kt$MainActivity$override fun onAction(action: AppFeature.Action.ViewAction) LongMethod:MainStepCompletionActionDispatcher.kt$MainStepCompletionActionDispatcher$private suspend fun handleStepSolved(stepId: Long) @@ -176,7 +177,6 @@ MaxLineLength:ExpandableTextView.kt$ExpandableTextView$* MaxLineLength:HtmlText.kt$text MaxLineLength:LinearProgressIndicator.kt$* - MaxLineLength:MainComponentImpl.kt$MainComponentImpl$notificationClickHandlingActionDispatcher = clickedNotificationComponent.notificationClickHandlingActionDispatcher MaxLineLength:ProjectSelectionDetailsClickedSelectThisProjectHyperskillAnalyticEvent.kt$ProjectSelectionDetailsClickedSelectThisProjectHyperskillAnalyticEvent$* MaxLineLength:RepositoryCacheProxy.kt$RepositoryCacheProxy$* MaxLineLength:StepComponentImpl.kt$StepComponentImpl$nextLearningActivityStateRepository = appGraph.stateRepositoriesComponent.nextLearningActivityStateRepository @@ -199,7 +199,6 @@ ModifierMissing:RequestReviewDialog.kt$RequestReviewDialog ModifierNotUsedAtRoot:LeaderboardPlaceInfo.kt$modifier = modifier .requiredSize(24.dp) .align(Alignment.CenterVertically) ModifierNotUsedAtRoot:NotificationsOnboardingScreen.kt$modifier.align(Alignment.Center) - ModifierNotUsedAtRoot:UsersQuestionnaireOnboardingScreen.kt$modifier = modifier .fillMaxSize() .verticalScroll(rememberScrollState()) .padding(padding + UsersQuestionnaireOnboardingDefaults.ContentPadding) ModifierReused:LeaderboardPlaceInfo.kt$Image( painter = painterResource( id = when (placeNumber) { 1 -> org.hyperskill.app.android.R.drawable.ic_leaderboard_first_place 2 -> org.hyperskill.app.android.R.drawable.ic_leaderboard_second_place 3 -> org.hyperskill.app.android.R.drawable.ic_leaderboard_third_place else -> error("Place icon should not be visible for the place number $placeNumber") } ), contentDescription = null, modifier = modifier .requiredSize(24.dp) .align(Alignment.CenterVertically) ) ModifierReused:LeaderboardPlaceInfo.kt$Row( modifier = modifier, horizontalArrangement = Arrangement.SpaceBetween ) { Text( text = placeNumber.toString(), style = MaterialTheme.typography.body2, color = colorResource(id = R.color.color_on_surface_alpha_60), modifier = Modifier.align(Alignment.CenterVertically) ) if (placeNumber in 1..3) { Image( painter = painterResource( id = when (placeNumber) { 1 -> org.hyperskill.app.android.R.drawable.ic_leaderboard_first_place 2 -> org.hyperskill.app.android.R.drawable.ic_leaderboard_second_place 3 -> org.hyperskill.app.android.R.drawable.ic_leaderboard_third_place else -> error("Place icon should not be visible for the place number $placeNumber") } ), contentDescription = null, modifier = modifier .requiredSize(24.dp) .align(Alignment.CenterVertically) ) } } ModifierReused:NotificationsOnboardingScreen.kt$Box( modifier = modifier .clip(RoundedCornerShape(dimensionResource(id = org.hyperskill.app.android.R.dimen.corner_radius))) ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center, modifier = Modifier .clickable( interactionSource = remember { MutableInteractionSource() }, indication = rememberRipple(), onClick = onTimeClick ) .padding(vertical = 4.dp, horizontal = 8.dp) ) { Text(text = stringResource(id = R.string.notifications_onboarding_daily_study_reminders_interval_prefix)) Spacer(modifier = Modifier.width(8.dp)) Text(text = formattedInterval) Spacer(modifier = Modifier.width(4.dp)) Box(modifier = Modifier.size(24.dp)) { Image( painter = painterResource( id = org.hyperskill.app.android.R.drawable.ic_notification_onboarding_arrow ), contentDescription = null, modifier.align(Alignment.Center) ) } } } @@ -298,14 +297,14 @@ UnstableCollections:LeaderboardList.kt$List<LeaderboardWidgetListItem> UnstableCollections:ServerSwitcher.kt$List<EndpointConfigType> UnstableCollections:TopicSearchResultContent.kt$List<Item> - UnstableCollections:UsersQuestionnaireOptionsList.kt$List<String> + UnstableCollections:WelcomeOnboardingChooseProgrammingLanguage.kt$List<WelcomeOnboardingProgrammingLanguage> + UnstableCollections:WelcomeQuestionnaire.kt$List<WelcomeQuestionnaireItem> UnusedParameter:CodeAnalyzer.kt$CodeAnalyzer$count: Int UnusedParameter:DebugToolsHelper.kt$DebugToolsHelper$app: Application UnusedPrivateMember:StepQuizResolverTest.kt$StepQuizResolverTest$@Suppress("UNUSED_EXPRESSION") private fun marker( stepRoute: StepRoute, stepQuizState: StepQuizFeature.StepQuizState ) UnusedPrivateProperty:MainActivity.kt$MainActivity$val splashScreen = installSplashScreen() UnusedPrivateProperty:TrackSelectionDetailsFragment.kt$TrackSelectionDetailsFragment$private val mainScreenRouter: MainScreenRouter by lazy(LazyThreadSafetyMode.NONE) { HyperskillApp.graph().navigationComponent.mainScreenCicerone.router } UseCheckOrError:AndroidStepQuizTest.kt$AndroidStepQuizTest$throw IllegalStateException( "Unknown step route class: $concreteStepRouteClass. Please add it to the test." ) - UseCheckOrError:AppFeatureStateSerializationTest.kt$AppFeatureStateSerializationTest$throw IllegalStateException("Unknown state class: $stateClass. Please add it to the test.") UseCheckOrError:CollectionToJsonElement.kt$throw IllegalStateException("Can't serialize unknown type: $this") UseCheckOrError:ContextExtensions.kt$throw IllegalStateException("Can't find Activity in a given context") UseCheckOrError:FragmentSerializableArgumentDelegate.kt$BundleDelegate$throw IllegalStateException("Property ${property.name} could not be read") diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/sharedSwift/Extensions/AppFeatureStateKsExtensions.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/sharedSwift/Extensions/AppFeatureStateKsExtensions.swift index 02db45188e..2b9e21ece6 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/sharedSwift/Extensions/AppFeatureStateKsExtensions.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Frameworks/sharedSwift/Extensions/AppFeatureStateKsExtensions.swift @@ -1,8 +1,8 @@ import Foundation import shared -extension AppFeatureStateKs: Equatable { - public static func == (lhs: AppFeatureStateKs, rhs: AppFeatureStateKs) -> Bool { +extension LegacyAppFeatureStateKs: Equatable { + public static func == (lhs: LegacyAppFeatureStateKs, rhs: LegacyAppFeatureStateKs) -> Bool { switch (lhs, rhs) { case (.idle, .idle): return true diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppAssembly.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppAssembly.swift index abd7daa8a3..ac70404f39 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppAssembly.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppAssembly.swift @@ -9,7 +9,7 @@ final class AppAssembly: UIKitAssembly { } func makeModule() -> UIViewController { - let feature = AppGraphBridge.sharedAppGraph.mainComponent.appFeature() + let feature = AppGraphBridge.sharedAppGraph.legacyMainComponent.legacyAppFeature() let viewModel = AppViewModel( pushNotificationData: pushNotificationData, diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppView.swift index 12b7991796..fdf83bf6ce 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppView.swift @@ -53,7 +53,7 @@ final class AppView: UIView { fatalError("init(coder:) has not been implemented") } - func renderState(_ state: AppFeatureStateKs) { + func renderState(_ state: LegacyAppFeatureStateKs) { switch state { case .idle, .loading: loadingIndicator.isHidden = false diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppViewModel.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppViewModel.swift index 49b60e01b5..d68a751ecf 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppViewModel.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppViewModel.swift @@ -4,7 +4,7 @@ import Foundation import shared import SwiftUI -final class AppViewModel: FeatureViewModel { +final class AppViewModel: FeatureViewModel { weak var viewController: AppViewControllerProtocol? private var pushNotificationData: PushNotificationData? @@ -29,7 +29,7 @@ final class AppViewModel: FeatureViewModel Bool { - AppFeatureStateKs(oldState) != AppFeatureStateKs(newState) + override func shouldNotifyStateDidChange(oldState: LegacyAppFeatureState, newState: LegacyAppFeatureState) -> Bool { + LegacyAppFeatureStateKs(oldState) != LegacyAppFeatureStateKs(newState) } func doLoadApp(forceUpdate: Bool = false) { - onNewMessage(AppFeatureMessageInitialize(pushNotificationData: pushNotificationData, forceUpdate: forceUpdate)) + onNewMessage(LegacyAppFeatureMessageInitialize(pushNotificationData: pushNotificationData, forceUpdate: forceUpdate)) } // MARK: Private API @@ -97,7 +97,7 @@ extension AppViewModel: AuthOutputProtocol { await MainActor.run { onNewMessage( - AppFeatureMessageUserAuthorized( + LegacyAppFeatureMessageUserAuthorized( profile: profile, isNotificationPermissionGranted: currentAuthorizationStatus.isRegistered ) @@ -111,14 +111,14 @@ extension AppViewModel: AuthOutputProtocol { extension AppViewModel: WelcomeOutputProtocol { func handleWelcomeSignInRequested() { - onViewAction?(AppFeatureActionViewActionNavigateToAuthScreen(isInSignUpMode: false)) + onViewAction?(LegacyAppFeatureActionViewActionNavigateToAuthScreen(isInSignUpMode: false)) } func handleWelcomeSignUpRequested(isInSignUpMode: Bool) { if isInSignUpMode { - onViewAction?(AppFeatureActionViewActionNavigateToAuthScreen(isInSignUpMode: isInSignUpMode)) + onViewAction?(LegacyAppFeatureActionViewActionNavigateToAuthScreen(isInSignUpMode: isInSignUpMode)) } else { - onViewAction?(AppFeatureActionViewActionNavigateToTrackSelectionScreen()) + onViewAction?(LegacyAppFeatureActionViewActionNavigateToTrackSelectionScreen()) } } } @@ -128,8 +128,8 @@ extension AppViewModel: WelcomeOutputProtocol { extension AppViewModel: NotificationsOnboardingOutputProtocol { func handleNotificationsOnboardingCompleted() { onNewMessage( - AppFeatureMessageWelcomeOnboardingMessage( - message: WelcomeOnboardingFeatureMessageNotificationOnboardingCompleted() + LegacyAppFeatureMessageWelcomeOnboardingMessage( + message: LegacyWelcomeOnboardingFeatureMessageNotificationOnboardingCompleted() ) ) } @@ -140,8 +140,8 @@ extension AppViewModel: NotificationsOnboardingOutputProtocol { extension AppViewModel: UsersQuestionnaireOnboardingOutputProtocol { func handleUsersQuestionnaireOnboardingCompleted() { onNewMessage( - AppFeatureMessageWelcomeOnboardingMessage( - message: WelcomeOnboardingFeatureMessageUsersQuestionnaireOnboardingCompleted() + LegacyAppFeatureMessageWelcomeOnboardingMessage( + message: LegacyWelcomeOnboardingFeatureMessageUsersQuestionnaireOnboardingCompleted() ) ) } @@ -152,8 +152,8 @@ extension AppViewModel: UsersQuestionnaireOnboardingOutputProtocol { extension AppViewModel: FirstProblemOnboardingOutputProtocol { func handleFirstProblemOnboardingCompleted(stepRoute: StepRoute?) { onNewMessage( - AppFeatureMessageWelcomeOnboardingMessage( - message: WelcomeOnboardingFeatureMessageFirstProblemOnboardingCompleted( + LegacyAppFeatureMessageWelcomeOnboardingMessage( + message: LegacyWelcomeOnboardingFeatureMessageFirstProblemOnboardingCompleted( firstProblemStepRoute: stepRoute ) ) @@ -230,14 +230,14 @@ private extension AppViewModel { @objc func handleProjectSelectionDetailsDidRequestNavigateToStudyPlanAsNewRootScreen() { - onViewAction?(AppFeatureActionViewActionNavigateToStudyPlan()) + onViewAction?(LegacyAppFeatureActionViewActionNavigateToStudyPlan()) } @objc func handleTrackSelectionDetailsDidRequestNavigateToFirstProblemOnboarding() { onViewAction?( - AppFeatureActionViewActionWelcomeOnboardingViewAction( - viewAction: WelcomeOnboardingFeatureActionViewActionNavigateToFirstProblemOnboardingScreen( + LegacyAppFeatureActionViewActionWelcomeOnboardingViewAction( + viewAction: LegacyWelcomeOnboardingFeatureActionViewActionNavigateToFirstProblemOnboardingScreen( isNewUserMode: true ) ) @@ -259,7 +259,7 @@ AppViewModel: \(#function) PushNotificationData not found in userInfo = \(String return } - onNewMessage(AppFeatureMessageNotificationClicked(notificationData: pushNotificationData)) + onNewMessage(LegacyAppFeatureMessageNotificationClicked(notificationData: pushNotificationData)) } @objc @@ -289,7 +289,7 @@ AppViewModel: \(#function) PushNotificationData not found in userInfo = \(String @objc private func handleApplicationWillEnterForeground() { - onNewMessage(AppFeatureMessageAppBecomesActive()) + onNewMessage(LegacyAppFeatureMessageAppBecomesActive()) } @objc @@ -307,7 +307,7 @@ AppViewModel: \(#function) isPaywallShown not found in userInfo = \(String(descr return } - onNewMessage(AppFeatureMessageIsPaywallShownChanged(isPaywallShown: isPaywallShown)) + onNewMessage(LegacyAppFeatureMessageIsPaywallShownChanged(isPaywallShown: isPaywallShown)) } } @@ -316,7 +316,7 @@ AppViewModel: \(#function) isPaywallShown not found in userInfo = \(String(descr extension AppViewModel: StreakRecoveryModalViewControllerDelegate { func streakRecoveryModalViewControllerDidTapRestoreStreakButton() { onNewMessage( - AppFeatureMessageStreakRecoveryMessage( + LegacyAppFeatureMessageStreakRecoveryMessage( message: StreakRecoveryFeatureMessageRestoreStreakClicked() ) ) @@ -324,7 +324,7 @@ extension AppViewModel: StreakRecoveryModalViewControllerDelegate { func streakRecoveryModalViewControllerDidTapNoThanksButton() { onNewMessage( - AppFeatureMessageStreakRecoveryMessage( + LegacyAppFeatureMessageStreakRecoveryMessage( message: StreakRecoveryFeatureMessageNoThanksClicked() ) ) @@ -332,7 +332,7 @@ extension AppViewModel: StreakRecoveryModalViewControllerDelegate { func streakRecoveryModalViewControllerDidAppear(_ viewController: StreakRecoveryModalViewController) { onNewMessage( - AppFeatureMessageStreakRecoveryMessage( + LegacyAppFeatureMessageStreakRecoveryMessage( message: StreakRecoveryFeatureMessageStreakRecoveryModalShownEventMessage() ) ) @@ -340,7 +340,7 @@ extension AppViewModel: StreakRecoveryModalViewControllerDelegate { func streakRecoveryModalViewControllerDidDisappear(_ viewController: StreakRecoveryModalViewController) { onNewMessage( - AppFeatureMessageStreakRecoveryMessage( + LegacyAppFeatureMessageStreakRecoveryMessage( message: StreakRecoveryFeatureMessageStreakRecoveryModalHiddenEventMessage() ) ) @@ -354,7 +354,7 @@ extension AppViewModel: BadgeEarnedModalViewControllerDelegate { _ viewController: BadgeEarnedModalViewController, badgeKind: BadgeKind ) { onNewMessage( - AppFeatureMessageNotificationClickHandlingMessage( + LegacyAppFeatureMessageNotificationClickHandlingMessage( message: NotificationClickHandlingFeatureMessageEarnedBadgeModalShownEventMessage(badgeKind: badgeKind) ) ) @@ -364,7 +364,7 @@ extension AppViewModel: BadgeEarnedModalViewControllerDelegate { _ viewController: BadgeEarnedModalViewController, badgeKind: BadgeKind ) { onNewMessage( - AppFeatureMessageNotificationClickHandlingMessage( + LegacyAppFeatureMessageNotificationClickHandlingMessage( message: NotificationClickHandlingFeatureMessageEarnedBadgeModalHiddenEventMessage(badgeKind: badgeKind) ) ) diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/ViewControllers/AppViewController.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/ViewControllers/AppViewController.swift index 2d61ca0b68..20767b7e09 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/ViewControllers/AppViewController.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/ViewControllers/AppViewController.swift @@ -4,8 +4,8 @@ import SwiftUI import UIKit protocol AppViewControllerProtocol: AnyObject { - func displayState(_ state: AppFeatureStateKs) - func displayViewAction(_ viewAction: AppFeatureActionViewActionKs) + func displayState(_ state: LegacyAppFeatureStateKs) + func displayViewAction(_ viewAction: LegacyAppFeatureActionViewActionKs) } extension AppViewController { @@ -56,7 +56,7 @@ final class AppViewController: UIViewController { // MARK: - AppViewController: AppViewControllerProtocol - extension AppViewController: AppViewControllerProtocol { - func displayState(_ state: AppFeatureStateKs) { + func displayState(_ state: LegacyAppFeatureStateKs) { if case .ready(let data) = state { AppTabItemsAvailabilityService.shared.setIsMobileLeaderboardsEnabled(data.isMobileLeaderboardsEnabled) } @@ -64,11 +64,11 @@ extension AppViewController: AppViewControllerProtocol { appView?.renderState(state) } - func displayViewAction(_ viewAction: AppFeatureActionViewActionKs) { + func displayViewAction(_ viewAction: LegacyAppFeatureActionViewActionKs) { switch viewAction { case .navigateTo(let navigateToViewAction): handleNavigateToViewAction( - AppFeatureActionViewActionNavigateToKs(navigateToViewAction) + LegacyAppFeatureActionViewActionNavigateToKs(navigateToViewAction) ) case .streakRecoveryViewAction(let streakRecoveryViewAction): handleStreakRecoveryViewAction( @@ -80,12 +80,12 @@ extension AppViewController: AppViewControllerProtocol { ) case .welcomeOnboardingViewAction(let welcomeOnboardingViewAction): handleWelcomeOnboardingViewAction( - WelcomeOnboardingFeatureActionViewActionKs(welcomeOnboardingViewAction.viewAction) + LegacyWelcomeOnboardingFeatureActionViewActionKs(welcomeOnboardingViewAction.viewAction) ) } } - private func handleNavigateToViewAction(_ viewAction: AppFeatureActionViewActionNavigateToKs) { + private func handleNavigateToViewAction(_ viewAction: LegacyAppFeatureActionViewActionNavigateToKs) { switch viewAction { case .authScreen(let data): router.route(.auth(isInSignUpMode: data.isInSignUpMode, moduleOutput: viewModel)) @@ -202,11 +202,11 @@ extension AppViewController: AppViewControllerProtocol { } private func handleWelcomeOnboardingViewAction( - _ viewAction: WelcomeOnboardingFeatureActionViewActionKs + _ viewAction: LegacyWelcomeOnboardingFeatureActionViewActionKs ) { switch viewAction { case .navigateTo(let navigateToViewAction): - switch WelcomeOnboardingFeatureActionViewActionNavigateToKs(navigateToViewAction) { + switch LegacyWelcomeOnboardingFeatureActionViewActionNavigateToKs(navigateToViewAction) { case .firstProblemOnboardingScreen(let data): router.route(.firstProblemOnboarding(isNewUserMode: data.isNewUserMode, moduleOutput: viewModel)) case .notificationOnboardingScreen: diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/UsersQuestionnaireOnboardingAssembly.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/UsersQuestionnaireOnboardingAssembly.swift index f1a60c0acb..d87493c258 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/UsersQuestionnaireOnboardingAssembly.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/UsersQuestionnaireOnboardingAssembly.swift @@ -10,10 +10,10 @@ final class UsersQuestionnaireOnboardingAssembly: UIKitAssembly { func makeModule() -> UIViewController { let usersQuestionnaireOnboardingComponent = - AppGraphBridge.sharedAppGraph.buildUsersQuestionnaireOnboardingComponent() + AppGraphBridge.sharedAppGraph.buildLegacyUsersQuestionnaireOnboardingComponent() let usersQuestionnaireOnboardingViewModel = UsersQuestionnaireOnboardingViewModel( - feature: usersQuestionnaireOnboardingComponent.usersQuestionnaireOnboardingFeature + feature: usersQuestionnaireOnboardingComponent.legacyUsersQuestionnaireOnboardingFeature ) usersQuestionnaireOnboardingViewModel.moduleOutput = moduleOutput diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/UsersQuestionnaireOnboardingViewModel.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/UsersQuestionnaireOnboardingViewModel.swift index 1449dd1e69..68e9790b77 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/UsersQuestionnaireOnboardingViewModel.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/UsersQuestionnaireOnboardingViewModel.swift @@ -2,33 +2,33 @@ import Foundation import shared final class UsersQuestionnaireOnboardingViewModel: FeatureViewModel< - UsersQuestionnaireOnboardingFeature.ViewState, - UsersQuestionnaireOnboardingFeatureMessage, - UsersQuestionnaireOnboardingFeatureActionViewAction + LegacyUsersQuestionnaireOnboardingFeature.ViewState, + LegacyUsersQuestionnaireOnboardingFeatureMessage, + LegacyUsersQuestionnaireOnboardingFeatureActionViewAction > { weak var moduleOutput: UsersQuestionnaireOnboardingOutputProtocol? override func shouldNotifyStateDidChange( - oldState: UsersQuestionnaireOnboardingFeature.ViewState, - newState: UsersQuestionnaireOnboardingFeature.ViewState + oldState: LegacyUsersQuestionnaireOnboardingFeature.ViewState, + newState: LegacyUsersQuestionnaireOnboardingFeature.ViewState ) -> Bool { !oldState.isEqual(newState) } func selectChoice(_ choice: String) { - onNewMessage(UsersQuestionnaireOnboardingFeatureMessageClickedChoice(choice: choice)) + onNewMessage(LegacyUsersQuestionnaireOnboardingFeatureMessageClickedChoice(choice: choice)) } func doTextInputValueChanged(_ value: String) { - onNewMessage(UsersQuestionnaireOnboardingFeatureMessageTextInputValueChanged(text: value)) + onNewMessage(LegacyUsersQuestionnaireOnboardingFeatureMessageTextInputValueChanged(text: value)) } func doSend() { - onNewMessage(UsersQuestionnaireOnboardingFeatureMessageSendButtonClicked()) + onNewMessage(LegacyUsersQuestionnaireOnboardingFeatureMessageSendButtonClicked()) } func doSkip() { - onNewMessage(UsersQuestionnaireOnboardingFeatureMessageSkipButtonClicked()) + onNewMessage(LegacyUsersQuestionnaireOnboardingFeatureMessageSkipButtonClicked()) } func doCompleteOnboarding() { @@ -36,6 +36,6 @@ final class UsersQuestionnaireOnboardingViewModel: FeatureViewModel< } func logViewedEvent() { - onNewMessage(UsersQuestionnaireOnboardingFeatureMessageViewedEventMessage()) + onNewMessage(LegacyUsersQuestionnaireOnboardingFeatureMessageViewedEventMessage()) } } diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/Views/UsersQuestionnaireOnboardingView.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/Views/UsersQuestionnaireOnboardingView.swift index e30714f0d2..b4d37f6085 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/Views/UsersQuestionnaireOnboardingView.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/UsersQuestionnaireOnboarding/Views/UsersQuestionnaireOnboardingView.swift @@ -49,9 +49,9 @@ struct UsersQuestionnaireOnboardingView: View { private extension UsersQuestionnaireOnboardingView { func handleViewAction( - _ viewAction: UsersQuestionnaireOnboardingFeatureActionViewAction + _ viewAction: LegacyUsersQuestionnaireOnboardingFeatureActionViewAction ) { - switch UsersQuestionnaireOnboardingFeatureActionViewActionKs(viewAction) { + switch LegacyUsersQuestionnaireOnboardingFeatureActionViewActionKs(viewAction) { case .showSendSuccessMessage(let showSendSuccessMessageViewAction): ProgressHUD.showSuccess(status: showSendSuccessMessageViewAction.message) case .completeUsersQuestionnaireOnboarding: diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraph.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraph.kt index ae25f8f554..cb4f6226c4 100644 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraph.kt +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraph.kt @@ -46,9 +46,12 @@ import org.hyperskill.app.track_selection.details.injection.PlatformTrackSelecti import org.hyperskill.app.track_selection.details.injection.TrackSelectionDetailsParams import org.hyperskill.app.track_selection.list.injection.PlatformTrackSelectionListComponent import org.hyperskill.app.track_selection.list.injection.TrackSelectionListParams -import org.hyperskill.app.users_questionnaire_onboarding.onboarding.injection.PlatformUsersQuestionnaireOnboardingComponent import org.hyperskill.app.welcome.injection.PlatformWelcomeComponent import org.hyperskill.app.welcome.injection.WelcomeComponent +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingFeatureParams +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.root.injection.PlatformWelcomeOnboardingComponent +import org.hyperskill.app.welcome_onboarding.track_details.injection.PlatformWelcomeOnboardingTrackDetailsComponent interface CommonAndroidAppGraph : AppGraph { val application: Application @@ -119,8 +122,6 @@ interface CommonAndroidAppGraph : AppGraph { stepRoute: StepRoute ): PlatformRequestReviewComponent - fun buildPlatformUsersQuestionnaireOnboardingComponent(): PlatformUsersQuestionnaireOnboardingComponent - fun buildPlatformPaywallComponent(paywallTransitionSource: PaywallTransitionSource): PlatformPaywallComponent fun buildPlatformManageSubscriptionComponent(): PlatformManageSubscriptionComponent @@ -132,4 +133,12 @@ interface CommonAndroidAppGraph : AppGraph { fun buildPlatformTopicCompletedModalComponent( params: TopicCompletedModalFeatureParams ): PlatformTopicCompletedModalComponent + + fun buildPlatformWelcomeOnboardingComponent( + params: WelcomeOnboardingFeatureParams + ): PlatformWelcomeOnboardingComponent + + fun buildPlatformWelcomeOnboardingTrackDetailsComponent( + track: WelcomeOnboardingTrack + ): PlatformWelcomeOnboardingTrackDetailsComponent } \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraphImpl.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraphImpl.kt index 906233892a..ae89fbc070 100644 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraphImpl.kt +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/core/injection/CommonAndroidAppGraphImpl.kt @@ -81,11 +81,15 @@ import org.hyperskill.app.track_selection.details.injection.TrackSelectionDetail import org.hyperskill.app.track_selection.list.injection.PlatformTrackSelectionListComponent import org.hyperskill.app.track_selection.list.injection.PlatformTrackSelectionListComponentImpl import org.hyperskill.app.track_selection.list.injection.TrackSelectionListParams -import org.hyperskill.app.users_questionnaire_onboarding.onboarding.injection.PlatformUsersQuestionnaireOnboardingComponent -import org.hyperskill.app.users_questionnaire_onboarding.onboarding.injection.PlatformUsersQuestionnaireOnboardingComponentImpl import org.hyperskill.app.welcome.injection.PlatformWelcomeComponent import org.hyperskill.app.welcome.injection.PlatformWelcomeComponentImpl import org.hyperskill.app.welcome.injection.WelcomeComponent +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingFeatureParams +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.root.injection.PlatformWelcomeOnboardingComponent +import org.hyperskill.app.welcome_onboarding.root.injection.PlatformWelcomeOnboardingComponentImpl +import org.hyperskill.app.welcome_onboarding.track_details.injection.PlatformWelcomeOnboardingTrackDetailsComponent +import org.hyperskill.app.welcome_onboarding.track_details.injection.PlatformWelcomeOnboardingTrackDetailsComponentImpl abstract class CommonAndroidAppGraphImpl : CommonAndroidAppGraph, BaseAppGraph() { @@ -296,11 +300,6 @@ abstract class CommonAndroidAppGraphImpl : CommonAndroidAppGraph, BaseAppGraph() requestReviewComponent = buildRequestReviewModalComponent(stepRoute) ) - override fun buildPlatformUsersQuestionnaireOnboardingComponent(): PlatformUsersQuestionnaireOnboardingComponent = - PlatformUsersQuestionnaireOnboardingComponentImpl( - usersQuestionnaireOnboardingComponent = buildUsersQuestionnaireOnboardingComponent() - ) - override fun buildPlatformPaywallComponent( paywallTransitionSource: PaywallTransitionSource ): PlatformPaywallComponent = @@ -326,4 +325,18 @@ abstract class CommonAndroidAppGraphImpl : CommonAndroidAppGraph, BaseAppGraph() PlatformTopicCompletedModalComponentImpl( topicCompletedModalComponent = buildTopicCompletedModalComponent(params) ) + + override fun buildPlatformWelcomeOnboardingComponent( + params: WelcomeOnboardingFeatureParams + ): PlatformWelcomeOnboardingComponent = + PlatformWelcomeOnboardingComponentImpl( + welcomeOnboardingComponent = buildWelcomeOnboardingComponent(params) + ) + + override fun buildPlatformWelcomeOnboardingTrackDetailsComponent( + track: WelcomeOnboardingTrack + ): PlatformWelcomeOnboardingTrackDetailsComponent = + PlatformWelcomeOnboardingTrackDetailsComponentImpl( + welcomeOnboardingTrackDetailsComponent = buildWelcomeOnboardingTrackDetailsComponent(track) + ) } \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/main/presentation/MainViewModel.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/main/presentation/MainViewModel.kt index f532e46b7b..a3f96761bc 100644 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/main/presentation/MainViewModel.kt +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/main/presentation/MainViewModel.kt @@ -12,7 +12,6 @@ import org.hyperskill.app.main.injection.PlatformMainComponentImpl import org.hyperskill.app.main.presentation.AppFeature.Message import org.hyperskill.app.main.presentation.AppFeature.State import org.hyperskill.app.notification.remote.domain.model.PushNotificationData -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature import ru.nobird.android.view.redux.viewmodel.ReduxViewModel import ru.nobird.app.core.model.Cancellable import ru.nobird.app.presentation.redux.container.ReduxViewContainer @@ -78,8 +77,4 @@ class MainViewModel( fun logScreenOrientation(screenOrientation: ScreenOrientation) { analyticInteractor.setScreenOrientation(screenOrientation) } - - fun onNewMessage(message: WelcomeOnboardingFeature.Message) { - onNewMessage(Message.WelcomeOnboardingMessage(message)) - } } \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/onboarding/injection/PlatformUsersQuestionnaireOnboardingComponent.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/onboarding/injection/PlatformUsersQuestionnaireOnboardingComponent.kt deleted file mode 100644 index ea83f82003..0000000000 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/onboarding/injection/PlatformUsersQuestionnaireOnboardingComponent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package org.hyperskill.app.users_questionnaire_onboarding.onboarding.injection - -import org.hyperskill.app.core.injection.ReduxViewModelFactory - -interface PlatformUsersQuestionnaireOnboardingComponent { - val reduxViewModelFactory: ReduxViewModelFactory -} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/onboarding/injection/PlatformUsersQuestionnaireOnboardingComponentImpl.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/onboarding/injection/PlatformUsersQuestionnaireOnboardingComponentImpl.kt deleted file mode 100644 index e0a4d026c1..0000000000 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/onboarding/injection/PlatformUsersQuestionnaireOnboardingComponentImpl.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.hyperskill.app.users_questionnaire_onboarding.onboarding.injection - -import org.hyperskill.app.core.flowredux.presentation.wrapWithFlowView -import org.hyperskill.app.core.injection.ReduxViewModelFactory -import org.hyperskill.app.users_questionnaire_onboarding.injection.UsersQuestionnaireOnboardingComponent -import org.hyperskill.app.users_questionnaire_onboarding.onboarding.presentation.UsersQuestionnaireOnboardingViewModel - -internal class PlatformUsersQuestionnaireOnboardingComponentImpl( - private val usersQuestionnaireOnboardingComponent: UsersQuestionnaireOnboardingComponent -) : PlatformUsersQuestionnaireOnboardingComponent { - override val reduxViewModelFactory: ReduxViewModelFactory - get() = ReduxViewModelFactory( - mapOf( - UsersQuestionnaireOnboardingViewModel::class.java to { - UsersQuestionnaireOnboardingViewModel( - usersQuestionnaireOnboardingComponent.usersQuestionnaireOnboardingFeature.wrapWithFlowView() - ) - } - ) - ) -} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/onboarding/presentation/UsersQuestionnaireOnboardingViewModel.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/onboarding/presentation/UsersQuestionnaireOnboardingViewModel.kt deleted file mode 100644 index 50ba06514f..0000000000 --- a/shared/src/androidMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/onboarding/presentation/UsersQuestionnaireOnboardingViewModel.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.hyperskill.app.users_questionnaire_onboarding.onboarding.presentation - -import org.hyperskill.app.core.flowredux.presentation.FlowView -import org.hyperskill.app.core.flowredux.presentation.ReduxFlowViewModel -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.Action.ViewAction -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.Message -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.ViewState - -class UsersQuestionnaireOnboardingViewModel( - viewContainer: FlowView -) : ReduxFlowViewModel(viewContainer) { - fun onChoiceClicked(choice: String) { - onNewMessage(Message.ClickedChoice(choice)) - } - - fun onTextInputChanged(text: String) { - onNewMessage(Message.TextInputValueChanged(text)) - } - - fun onSendButtonClick() { - onNewMessage(Message.SendButtonClicked) - } - - fun onSkipButtonClick() { - onNewMessage(Message.SkipButtonClicked) - } -} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/PlatformWelcomeOnboardingComponent.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/PlatformWelcomeOnboardingComponent.kt new file mode 100644 index 0000000000..32cd36da30 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/PlatformWelcomeOnboardingComponent.kt @@ -0,0 +1,7 @@ +package org.hyperskill.app.welcome_onboarding.root.injection + +import org.hyperskill.app.core.injection.ReduxViewModelFactory + +interface PlatformWelcomeOnboardingComponent { + val reduxViewModelFactory: ReduxViewModelFactory +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/PlatformWelcomeOnboardingComponentImpl.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/PlatformWelcomeOnboardingComponentImpl.kt new file mode 100644 index 0000000000..d248642fe4 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/PlatformWelcomeOnboardingComponentImpl.kt @@ -0,0 +1,20 @@ +package org.hyperskill.app.welcome_onboarding.root.injection + +import org.hyperskill.app.core.flowredux.presentation.wrapWithFlowView +import org.hyperskill.app.core.injection.ReduxViewModelFactory +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingViewModel + +class PlatformWelcomeOnboardingComponentImpl( + private val welcomeOnboardingComponent: WelcomeOnboardingComponent +) : PlatformWelcomeOnboardingComponent { + override val reduxViewModelFactory: ReduxViewModelFactory + get() = ReduxViewModelFactory( + mapOf( + WelcomeOnboardingViewModel::class.java to { + WelcomeOnboardingViewModel( + welcomeOnboardingComponent.welcomeOnboardingFeature.wrapWithFlowView() + ) + } + ) + ) +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingViewModel.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingViewModel.kt new file mode 100644 index 0000000000..39426194ac --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingViewModel.kt @@ -0,0 +1,63 @@ +package org.hyperskill.app.welcome_onboarding.root.presentation + +import org.hyperskill.app.core.flowredux.presentation.FlowView +import org.hyperskill.app.core.flowredux.presentation.ReduxFlowViewModel +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireType +import org.hyperskill.app.welcome_onboarding.root.model.WelcomeOnboardingProgrammingLanguage +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Action.ViewAction +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Message +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.State + +class WelcomeOnboardingViewModel( + flowView: FlowView +) : ReduxFlowViewModel(flowView) { + + init { + onNewMessage(Message.Initialize) + } + + fun onStartClick() { + onNewMessage(Message.StartJourneyClicked) + } + + fun onQuestionnaireItemClicked( + questionnaireType: WelcomeQuestionnaireType, + itemType: WelcomeQuestionnaireItemType + ) { + onNewMessage(Message.QuestionnaireItemClicked(questionnaireType, itemType)) + } + + fun onLanguageSelected(language: WelcomeOnboardingProgrammingLanguage) { + onNewMessage(Message.ProgrammingLanguageSelected(language)) + } + + fun onTrackSelected(track: WelcomeOnboardingTrack, isNotificationPermissionGranted: Boolean) { + onNewMessage(Message.TrackSelected(track, isNotificationPermissionGranted)) + } + + fun onNotificationPermissionCompleted() { + onNewMessage(Message.NotificationPermissionOnboardingCompleted) + } + + fun onFinishOnboardingShowed() { + onNewMessage(Message.FinishOnboardingViewed) + } + + fun onStartOnboardingViewed() { + onNewMessage(Message.StartOnboardingViewed) + } + + fun onUserQuestionnaireViewed(questionnaireType: WelcomeQuestionnaireType) { + onNewMessage(Message.UserQuestionnaireViewed(questionnaireType)) + } + + fun onSelectProgrammingLanguageViewed() { + onNewMessage(Message.SelectProgrammingLanguageViewed) + } + + fun onFinishClicked() { + onNewMessage(Message.FinishClicked) + } +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/PlatformWelcomeOnboardingTrackDetailsComponent.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/PlatformWelcomeOnboardingTrackDetailsComponent.kt new file mode 100644 index 0000000000..46844d3487 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/PlatformWelcomeOnboardingTrackDetailsComponent.kt @@ -0,0 +1,7 @@ +package org.hyperskill.app.welcome_onboarding.track_details.injection + +import org.hyperskill.app.core.injection.ReduxViewModelFactory + +interface PlatformWelcomeOnboardingTrackDetailsComponent { + val reduxViewModelFactory: ReduxViewModelFactory +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/PlatformWelcomeOnboardingTrackDetailsComponentImpl.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/PlatformWelcomeOnboardingTrackDetailsComponentImpl.kt new file mode 100644 index 0000000000..798b3e5a03 --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/PlatformWelcomeOnboardingTrackDetailsComponentImpl.kt @@ -0,0 +1,20 @@ +package org.hyperskill.app.welcome_onboarding.track_details.injection + +import org.hyperskill.app.core.flowredux.presentation.wrapWithFlowView +import org.hyperskill.app.core.injection.ReduxViewModelFactory +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsViewModel + +class PlatformWelcomeOnboardingTrackDetailsComponentImpl( + private val welcomeOnboardingTrackDetailsComponent: WelcomeOnboardingTrackDetailsComponent +) : PlatformWelcomeOnboardingTrackDetailsComponent { + override val reduxViewModelFactory: ReduxViewModelFactory + get() = ReduxViewModelFactory( + mapOf( + WelcomeOnboardingTrackDetailsViewModel::class.java to { + WelcomeOnboardingTrackDetailsViewModel( + welcomeOnboardingTrackDetailsComponent.welcomeOnboardingTrackDetails.wrapWithFlowView() + ) + } + ) + ) +} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsViewModel.kt b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsViewModel.kt new file mode 100644 index 0000000000..4eb336be9c --- /dev/null +++ b/shared/src/androidMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsViewModel.kt @@ -0,0 +1,20 @@ +package org.hyperskill.app.welcome_onboarding.track_details.presentation + +import org.hyperskill.app.core.flowredux.presentation.FlowView +import org.hyperskill.app.core.flowredux.presentation.ReduxFlowViewModel +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Action.ViewAction +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Message +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.ViewState + +class WelcomeOnboardingTrackDetailsViewModel( + flowView: FlowView +) : ReduxFlowViewModel(flowView) { + + fun onShow() { + onNewMessage(Message.ViewedEventMessage) + } + + fun onContinueClick() { + onNewMessage(Message.ContinueClicked) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticRoute.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticRoute.kt index 1b4596fa64..78d740311e 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticRoute.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticRoute.kt @@ -20,6 +20,21 @@ sealed class HyperskillAnalyticRoute { override val path: String get() = "${super.path}/questionnaire" } + + object ProgrammingLanguageQuestionnaire : Onboarding() { + override val path: String + get() = "${super.path}/programming-language-select" + } + + object TrackSelection : Onboarding() { + override val path: String + get() = "${super.path}/track-select" + } + + object Finish : Onboarding() { + override val path: String + get() = "${super.path}/finish" + } } open class Login : HyperskillAnalyticRoute() { diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt index a9d123f84b..7574385e60 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticTarget.kt @@ -131,5 +131,8 @@ enum class HyperskillAnalyticTarget(val targetName: String) { QUESTION_MARK("question_mark"), SPACEBOT("spacebot"), SCREENSHOT("screenshot"), - OPEN_IN_WEB("open_in_web") + OPEN_IN_WEB("open_in_web"), + START_MY_JOURNEY("start_my_journey"), + QUESTIONNAIRE_ITEM("questionnaire_item"), + PROGRAMMING_LANGUAGE("programming_language") } \ No newline at end of file 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 e37711c4f4..06fac014f3 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 @@ -21,11 +21,13 @@ import org.hyperskill.app.leaderboard.injection.LeaderboardDataComponent import org.hyperskill.app.leaderboard.screen.injection.LeaderboardScreenComponent import org.hyperskill.app.leaderboard.widget.injection.LeaderboardWidgetComponent import org.hyperskill.app.learning_activities.injection.LearningActivitiesDataComponent +import org.hyperskill.app.legacy_welcome_onboarding.injection.LegacyWelcomeOnboardingComponent import org.hyperskill.app.likes.injection.LikesDataComponent import org.hyperskill.app.logging.injection.LoggerComponent import org.hyperskill.app.magic_links.injection.MagicLinksDataComponent import org.hyperskill.app.main.injection.MainComponent import org.hyperskill.app.main.injection.MainDataComponent +import org.hyperskill.app.main_legacy.injection.LegacyMainComponent import org.hyperskill.app.manage_subscription.injection.ManageSubscriptionComponent import org.hyperskill.app.network.injection.NetworkComponent import org.hyperskill.app.notification.click_handling.injection.NotificationClickHandlingComponent @@ -88,10 +90,15 @@ import org.hyperskill.app.track_selection.details.injection.TrackSelectionDetail import org.hyperskill.app.track_selection.list.injection.TrackSelectionListComponent import org.hyperskill.app.user_storage.injection.UserStorageComponent import org.hyperskill.app.users_interview_widget.injection.UsersInterviewWidgetComponent -import org.hyperskill.app.users_questionnaire_onboarding.injection.UsersQuestionnaireOnboardingComponent +import org.hyperskill.app.users_questionnaire_onboarding_legacy.injection.LegacyUsersQuestionnaireOnboardingComponent import org.hyperskill.app.welcome.injection.WelcomeComponent import org.hyperskill.app.welcome.injection.WelcomeDataComponent -import org.hyperskill.app.welcome_onboarding.injection.WelcomeOnboardingComponent +import org.hyperskill.app.welcome_onboarding.finish.injection.WelcomeOnboardingFinishComponent +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingFeatureParams +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.questionnaire.injection.WelcomeQuestionnaireComponent +import org.hyperskill.app.welcome_onboarding.root.injection.WelcomeOnboardingComponent +import org.hyperskill.app.welcome_onboarding.track_details.injection.WelcomeOnboardingTrackDetailsComponent interface AppGraph { val commonComponent: CommonComponent @@ -99,6 +106,8 @@ interface AppGraph { val loggerComponent: LoggerComponent val authComponent: AuthComponent val mainComponent: MainComponent + @Deprecated("Should be removed in ALTAPPS-1276") + val legacyMainComponent: LegacyMainComponent val analyticComponent: AnalyticComponent val sentryComponent: SentryComponent val streakFlowDataComponent: StreakFlowDataComponent @@ -188,13 +197,21 @@ interface AppGraph { fun buildLeaderboardWidgetComponent(): LeaderboardWidgetComponent fun buildSearchResultsDataComponent(): SearchResultsDataComponent fun buildSearchComponent(): SearchComponent - fun buildWelcomeOnboardingComponent(): WelcomeOnboardingComponent + @Deprecated("Should be removed in ALTAPPS-1276") + fun buildLegacyWelcomeOnboardingComponent(): LegacyWelcomeOnboardingComponent + fun buildWelcomeOnboardingComponent(params: WelcomeOnboardingFeatureParams): WelcomeOnboardingComponent + fun buildWelcomeQuestionnaireComponent(): WelcomeQuestionnaireComponent + fun buildWelcomeOnboardingTrackDetailsComponent( + track: WelcomeOnboardingTrack + ): WelcomeOnboardingTrackDetailsComponent + fun buildWelcomeOnboardingFinishComponent(): WelcomeOnboardingFinishComponent fun buildRequestReviewDataComponent(): RequestReviewDataComponent fun buildRequestReviewModalComponent(stepRoute: StepRoute): RequestReviewModalComponent fun buildPaywallComponent(paywallTransitionSource: PaywallTransitionSource): PaywallComponent fun buildManageSubscriptionComponent(): ManageSubscriptionComponent fun buildUsersInterviewWidgetComponent(): UsersInterviewWidgetComponent - fun buildUsersQuestionnaireOnboardingComponent(): UsersQuestionnaireOnboardingComponent + @Deprecated("Should be removed in ALTAPPS-1276") + fun buildLegacyUsersQuestionnaireOnboardingComponent(): LegacyUsersQuestionnaireOnboardingComponent fun buildProblemsLimitInfoModalComponent( params: ProblemsLimitInfoModalFeatureParams ): ProblemsLimitInfoModalComponent diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/BaseAppGraph.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/BaseAppGraph.kt index 25952058b7..f1e8cc4425 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/BaseAppGraph.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/core/injection/BaseAppGraph.kt @@ -39,6 +39,8 @@ import org.hyperskill.app.leaderboard.widget.injection.LeaderboardWidgetComponen import org.hyperskill.app.leaderboard.widget.injection.LeaderboardWidgetComponentImpl import org.hyperskill.app.learning_activities.injection.LearningActivitiesDataComponent import org.hyperskill.app.learning_activities.injection.LearningActivitiesDataComponentImpl +import org.hyperskill.app.legacy_welcome_onboarding.injection.LegacyWelcomeOnboardingComponent +import org.hyperskill.app.legacy_welcome_onboarding.injection.LegacyWelcomeOnboardingComponentImpl import org.hyperskill.app.likes.injection.LikesDataComponent import org.hyperskill.app.likes.injection.LikesDataComponentImpl import org.hyperskill.app.logging.injection.LoggerComponent @@ -49,6 +51,8 @@ import org.hyperskill.app.main.injection.MainComponent import org.hyperskill.app.main.injection.MainComponentImpl import org.hyperskill.app.main.injection.MainDataComponent import org.hyperskill.app.main.injection.MainDataComponentImpl +import org.hyperskill.app.main_legacy.injection.LegacyMainComponent +import org.hyperskill.app.main_legacy.injection.LegacyMainComponentImpl import org.hyperskill.app.manage_subscription.injection.ManageSubscriptionComponent import org.hyperskill.app.manage_subscription.injection.ManageSubscriptionComponentImpl import org.hyperskill.app.network.injection.NetworkComponent @@ -163,14 +167,22 @@ import org.hyperskill.app.user_storage.injection.UserStorageComponent import org.hyperskill.app.user_storage.injection.UserStorageComponentImpl import org.hyperskill.app.users_interview_widget.injection.UsersInterviewWidgetComponent import org.hyperskill.app.users_interview_widget.injection.UsersInterviewWidgetComponentImpl -import org.hyperskill.app.users_questionnaire_onboarding.injection.UsersQuestionnaireOnboardingComponent -import org.hyperskill.app.users_questionnaire_onboarding.injection.UsersQuestionnaireOnboardingComponentImpl +import org.hyperskill.app.users_questionnaire_onboarding_legacy.injection.LegacyUsersQuestionnaireOnboardingComponent +import org.hyperskill.app.users_questionnaire_onboarding_legacy.injection.LegacyUsersQuestionnaireOnboardingComponentImpl import org.hyperskill.app.welcome.injection.WelcomeComponent import org.hyperskill.app.welcome.injection.WelcomeComponentImpl import org.hyperskill.app.welcome.injection.WelcomeDataComponent import org.hyperskill.app.welcome.injection.WelcomeDataComponentImpl -import org.hyperskill.app.welcome_onboarding.injection.WelcomeOnboardingComponent -import org.hyperskill.app.welcome_onboarding.injection.WelcomeOnboardingComponentImpl +import org.hyperskill.app.welcome_onboarding.finish.injection.WelcomeOnboardingFinishComponent +import org.hyperskill.app.welcome_onboarding.finish.injection.WelcomeOnboardingFinishComponentImpl +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingFeatureParams +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.questionnaire.injection.WelcomeQuestionnaireComponent +import org.hyperskill.app.welcome_onboarding.questionnaire.injection.WelcomeQuestionnaireComponentImpl +import org.hyperskill.app.welcome_onboarding.root.injection.WelcomeOnboardingComponent +import org.hyperskill.app.welcome_onboarding.root.injection.WelcomeOnboardingComponentImpl +import org.hyperskill.app.welcome_onboarding.track_details.injection.WelcomeOnboardingTrackDetailsComponent +import org.hyperskill.app.welcome_onboarding.track_details.injection.WelcomeOnboardingTrackDetailsComponentImpl abstract class BaseAppGraph : AppGraph { @@ -178,6 +190,10 @@ abstract class BaseAppGraph : AppGraph { MainComponentImpl(this) } + override val legacyMainComponent: LegacyMainComponent by lazy { + LegacyMainComponentImpl(this) + } + override val networkComponent: NetworkComponent by lazy { NetworkComponentImpl(this) } @@ -492,8 +508,22 @@ abstract class BaseAppGraph : AppGraph { override fun buildSearchComponent(): SearchComponent = SearchComponentImpl(this) - override fun buildWelcomeOnboardingComponent(): WelcomeOnboardingComponent = - WelcomeOnboardingComponentImpl(this) + override fun buildLegacyWelcomeOnboardingComponent(): LegacyWelcomeOnboardingComponent = + LegacyWelcomeOnboardingComponentImpl(this) + + override fun buildWelcomeOnboardingComponent(params: WelcomeOnboardingFeatureParams): WelcomeOnboardingComponent = + WelcomeOnboardingComponentImpl(this, params) + + override fun buildWelcomeQuestionnaireComponent(): WelcomeQuestionnaireComponent = + WelcomeQuestionnaireComponentImpl(this) + + override fun buildWelcomeOnboardingTrackDetailsComponent( + track: WelcomeOnboardingTrack + ): WelcomeOnboardingTrackDetailsComponent = + WelcomeOnboardingTrackDetailsComponentImpl(track, this) + + override fun buildWelcomeOnboardingFinishComponent(): WelcomeOnboardingFinishComponent = + WelcomeOnboardingFinishComponentImpl(this) override fun buildRequestReviewDataComponent(): RequestReviewDataComponent = RequestReviewDataComponentImpl(this) @@ -512,8 +542,8 @@ abstract class BaseAppGraph : AppGraph { override fun buildUsersInterviewWidgetComponent(): UsersInterviewWidgetComponent = UsersInterviewWidgetComponentImpl(this) - override fun buildUsersQuestionnaireOnboardingComponent(): UsersQuestionnaireOnboardingComponent = - UsersQuestionnaireOnboardingComponentImpl(this) + override fun buildLegacyUsersQuestionnaireOnboardingComponent(): LegacyUsersQuestionnaireOnboardingComponent = + LegacyUsersQuestionnaireOnboardingComponentImpl(this) override fun buildProblemsLimitInfoModalComponent( params: ProblemsLimitInfoModalFeatureParams diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/first_problem_onboarding/domain/analytic/OnboardingCompletionAppsFlyerAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/first_problem_onboarding/domain/analytic/OnboardingCompletionAppsFlyerAnalyticEvent.kt index f7fb23994e..eee88ab41c 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/first_problem_onboarding/domain/analytic/OnboardingCompletionAppsFlyerAnalyticEvent.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/first_problem_onboarding/domain/analytic/OnboardingCompletionAppsFlyerAnalyticEvent.kt @@ -5,17 +5,8 @@ import org.hyperskill.app.analytic.domain.model.apps_flyer.AppsFlyerAnalyticEven /** * Represents onboarding completion analytic event. * - * @param target can be "start_learning" or "keep_learning" - * * @see AppsFlyerAnalyticEvent */ -class OnboardingCompletionAppsFlyerAnalyticEvent( - target: String -) : AppsFlyerAnalyticEvent( - name = "af_onb_completion", - params = mapOf(PARAM_TARGET to target) -) { - companion object { - private const val PARAM_TARGET = "af_target" - } -} \ No newline at end of file +object OnboardingCompletionAppsFlyerAnalyticEvent : AppsFlyerAnalyticEvent( + name = "af_onb_completion" +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/first_problem_onboarding/presentation/FirstProblemOnboardingReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/first_problem_onboarding/presentation/FirstProblemOnboardingReducer.kt index cb1225134a..d893d10fc6 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/first_problem_onboarding/presentation/FirstProblemOnboardingReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/first_problem_onboarding/presentation/FirstProblemOnboardingReducer.kt @@ -140,9 +140,7 @@ internal class FirstProblemOnboardingReducer : StateReducer(config.createConfig()) { diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/presentation/WelcomeOnboardingFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/legacy_welcome_onboarding/presentation/LegacyWelcomeOnboardingFeature.kt similarity index 86% rename from shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/presentation/WelcomeOnboardingFeature.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/legacy_welcome_onboarding/presentation/LegacyWelcomeOnboardingFeature.kt index 61c980a988..57da9ecf51 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/presentation/WelcomeOnboardingFeature.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/legacy_welcome_onboarding/presentation/LegacyWelcomeOnboardingFeature.kt @@ -1,11 +1,12 @@ -package org.hyperskill.app.welcome_onboarding.presentation +package org.hyperskill.app.legacy_welcome_onboarding.presentation import kotlinx.serialization.Serializable +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingFeature.Action import org.hyperskill.app.profile.domain.model.Profile import org.hyperskill.app.step.domain.model.StepRoute -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature.Action -object WelcomeOnboardingFeature { +@Deprecated("Should be removed in ALTAPPS-1276") +object LegacyWelcomeOnboardingFeature { @Serializable data class State(val profile: Profile? = null) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/presentation/WelcomeOnboardingReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/legacy_welcome_onboarding/presentation/LegacyWelcomeOnboardingReducer.kt similarity index 78% rename from shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/presentation/WelcomeOnboardingReducer.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/legacy_welcome_onboarding/presentation/LegacyWelcomeOnboardingReducer.kt index 8ac1ad743e..e09feef5a4 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/presentation/WelcomeOnboardingReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/legacy_welcome_onboarding/presentation/LegacyWelcomeOnboardingReducer.kt @@ -1,17 +1,18 @@ -package org.hyperskill.app.welcome_onboarding.presentation +package org.hyperskill.app.legacy_welcome_onboarding.presentation +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingFeature.Action +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingFeature.Action.ViewAction +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingFeature.InternalAction +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingFeature.InternalMessage +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingFeature.Message +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingFeature.State import org.hyperskill.app.profile.domain.model.isNewUser -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature.Action -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature.Action.ViewAction -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature.InternalAction -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature.InternalMessage -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature.Message -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature.State import ru.nobird.app.presentation.redux.reducer.StateReducer private typealias ReducerResult = Pair> -class WelcomeOnboardingReducer : StateReducer { +@Deprecated("Should be removed in ALTAPPS-1276") +class LegacyWelcomeOnboardingReducer : StateReducer { override fun reduce(state: State, message: Message): ReducerResult = when (message) { is InternalMessage.OnboardingFlowRequested -> 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 568853b35d..1b4b2ef0a7 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 @@ -22,8 +22,6 @@ import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryActionDispa import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryReducer import org.hyperskill.app.subscriptions.domain.interactor.SubscriptionsInteractor import org.hyperskill.app.subscriptions.domain.repository.CurrentSubscriptionStateRepository -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingActionDispatcher -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingReducer import ru.nobird.app.core.model.safeCast import ru.nobird.app.presentation.redux.dispatcher.transform import ru.nobird.app.presentation.redux.dispatcher.wrapWithActionDispatcher @@ -45,8 +43,6 @@ internal object AppFeatureBuilder { notificationClickHandlingActionDispatcher: NotificationClickHandlingActionDispatcher, notificationsInteractor: NotificationInteractor, pushNotificationsInteractor: PushNotificationsInteractor, - welcomeOnboardingReducer: WelcomeOnboardingReducer, - welcomeOnboardingActionDispatcher: WelcomeOnboardingActionDispatcher, purchaseInteractor: PurchaseInteractor, currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, subscriptionsInteractor: SubscriptionsInteractor, @@ -55,8 +51,7 @@ internal object AppFeatureBuilder { ): Feature { val appReducer = AppReducer( streakRecoveryReducer = streakRecoveryReducer, - notificationClickHandlingReducer = clickedNotificationReducer, - welcomeOnboardingReducer = welcomeOnboardingReducer + notificationClickHandlingReducer = clickedNotificationReducer ).wrapWithLogger(buildVariant, logger, LOG_TAG) val appActionDispatcher = AppActionDispatcher( @@ -87,11 +82,5 @@ internal object AppFeatureBuilder { transformMessage = Message::NotificationClickHandlingMessage ) ) - .wrapWithActionDispatcher( - welcomeOnboardingActionDispatcher.transform( - transformAction = { it.safeCast()?.action }, - transformMessage = Message::WelcomeOnboardingMessage - ) - ) } } \ No newline at end of file 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 a8457d3e72..00239a6f6b 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 @@ -4,7 +4,6 @@ import org.hyperskill.app.core.injection.AppGraph import org.hyperskill.app.main.presentation.AppFeature import org.hyperskill.app.notification.click_handling.injection.NotificationClickHandlingComponent import org.hyperskill.app.streak_recovery.injection.StreakRecoveryComponent -import org.hyperskill.app.welcome_onboarding.injection.WelcomeOnboardingComponent import ru.nobird.app.presentation.redux.feature.Feature internal class MainComponentImpl(private val appGraph: AppGraph) : MainComponent { @@ -14,10 +13,8 @@ internal class MainComponentImpl(private val appGraph: AppGraph) : MainComponent private val clickedNotificationComponent: NotificationClickHandlingComponent = appGraph.buildClickedNotificationComponent() - private val welcomeOnboardingComponent: WelcomeOnboardingComponent = - appGraph.buildWelcomeOnboardingComponent() - /*ktlint-disable*/ + @Suppress("MaxLineLength") override fun appFeature( initialState: AppFeature.State? ): Feature = @@ -33,8 +30,6 @@ internal class MainComponentImpl(private val appGraph: AppGraph) : MainComponent notificationClickHandlingActionDispatcher = clickedNotificationComponent.notificationClickHandlingActionDispatcher, notificationsInteractor = appGraph.buildNotificationComponent().notificationInteractor, pushNotificationsInteractor = appGraph.buildPushNotificationsComponent().pushNotificationsInteractor, - welcomeOnboardingReducer = welcomeOnboardingComponent.welcomeOnboardingReducer, - welcomeOnboardingActionDispatcher = welcomeOnboardingComponent.welcomeOnboardingActionDispatcher, purchaseInteractor = appGraph.buildPurchaseComponent().purchaseInteractor, currentSubscriptionStateRepository = appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository, subscriptionsInteractor = appGraph.subscriptionDataComponent.subscriptionsInteractor, diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppFeature.kt index bcafd7fb23..650afd81f3 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppFeature.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppFeature.kt @@ -6,9 +6,9 @@ import org.hyperskill.app.notification.click_handling.presentation.NotificationC import org.hyperskill.app.notification.remote.domain.model.PushNotificationData import org.hyperskill.app.paywall.domain.model.PaywallTransitionSource import org.hyperskill.app.profile.domain.model.Profile +import org.hyperskill.app.step.domain.model.StepRoute import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryFeature import org.hyperskill.app.subscriptions.domain.model.Subscription -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature object AppFeature { @@ -28,7 +28,6 @@ object AppFeature { val isAuthorized: Boolean, val isMobileLeaderboardsEnabled: Boolean, internal val streakRecoveryState: StreakRecoveryFeature.State = StreakRecoveryFeature.State(), - internal val welcomeOnboardingState: WelcomeOnboardingFeature.State = WelcomeOnboardingFeature.State(), internal val isMobileOnlySubscriptionEnabled: Boolean, internal val canMakePayments: Boolean, internal val subscription: Subscription? = null, @@ -78,6 +77,8 @@ object AppFeature { val isPaywallShown: Boolean ) : Message + data class WelcomeOnboardingCompleted(val stepRoute: StepRoute?) : Message + /** * Message Wrappers */ @@ -86,10 +87,6 @@ object AppFeature { data class NotificationClickHandlingMessage( val message: NotificationClickHandlingFeature.Message ) : Message - - data class WelcomeOnboardingMessage( - val message: WelcomeOnboardingFeature.Message - ) : Message } internal sealed interface InternalMessage : Message { @@ -116,10 +113,6 @@ object AppFeature { val action: NotificationClickHandlingFeature.Action ) : Action - data class WelcomeOnboardingAction( - val action: WelcomeOnboardingFeature.Action - ) : Action - /** * Sentry */ @@ -132,10 +125,12 @@ object AppFeature { object TrackSelectionScreen : NavigateTo object WelcomeScreen : NavigateTo object StudyPlan : NavigateTo + data class Step(val stepRoute: StepRoute) : NavigateTo data class Paywall(val paywallTransitionSource: PaywallTransitionSource) : NavigateTo data class StudyPlanWithPaywall( val paywallTransitionSource: PaywallTransitionSource ) : NavigateTo + data class WelcomeOnboarding(val profile: Profile) : NavigateTo } /** @@ -146,10 +141,6 @@ object AppFeature { data class ClickedNotificationViewAction( val viewAction: NotificationClickHandlingFeature.Action.ViewAction ) : ViewAction - - data class WelcomeOnboardingViewAction( - val viewAction: WelcomeOnboardingFeature.Action.ViewAction - ) : ViewAction } } diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppReducer.kt index db07f95c20..eccbd00f8a 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppReducer.kt @@ -2,6 +2,7 @@ package org.hyperskill.app.main.presentation import org.hyperskill.app.auth.domain.model.UserDeauthorized import org.hyperskill.app.main.presentation.AppFeature.Action +import org.hyperskill.app.main.presentation.AppFeature.Action.ViewAction.NavigateTo import org.hyperskill.app.main.presentation.AppFeature.InternalAction import org.hyperskill.app.main.presentation.AppFeature.InternalMessage import org.hyperskill.app.main.presentation.AppFeature.Message @@ -12,22 +13,18 @@ import org.hyperskill.app.paywall.domain.model.PaywallTransitionSource import org.hyperskill.app.profile.domain.model.Profile import org.hyperskill.app.profile.domain.model.isMobileLeaderboardsEnabled import org.hyperskill.app.profile.domain.model.isMobileOnlySubscriptionEnabled -import org.hyperskill.app.profile.domain.model.isNewUser import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryFeature import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryReducer import org.hyperskill.app.subscriptions.domain.model.Subscription import org.hyperskill.app.subscriptions.domain.model.isFreemium -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingFeature -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingReducer -import org.hyperskill.app.welcome_onboarding.presentation.getFinishAction +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature import ru.nobird.app.presentation.redux.reducer.StateReducer private typealias ReducerResult = Pair> internal class AppReducer( private val streakRecoveryReducer: StreakRecoveryReducer, - private val notificationClickHandlingReducer: NotificationClickHandlingReducer, - private val welcomeOnboardingReducer: WelcomeOnboardingReducer + private val notificationClickHandlingReducer: NotificationClickHandlingReducer ) : StateReducer { companion object { @@ -64,9 +61,9 @@ internal class AppReducer( if (state is State.Ready && state.isAuthorized) { val navigateToViewAction = when (message.reason) { UserDeauthorized.Reason.TOKEN_REFRESH_FAILURE -> - Action.ViewAction.NavigateTo.WelcomeScreen + NavigateTo.WelcomeScreen UserDeauthorized.Reason.SIGN_OUT -> - Action.ViewAction.NavigateTo.AuthScreen() + NavigateTo.AuthScreen() } State.Ready( @@ -79,9 +76,9 @@ internal class AppReducer( null } is Message.OpenAuthScreen -> - state to setOf(Action.ViewAction.NavigateTo.AuthScreen()) + state to setOf(NavigateTo.AuthScreen()) is Message.OpenNewUserScreen -> - state to setOf(Action.ViewAction.NavigateTo.TrackSelectionScreen) + state to setOf(NavigateTo.TrackSelectionScreen) is Message.StreakRecoveryMessage -> if (state is State.Ready) { val (streakRecoveryState, streakRecoveryActions) = @@ -94,13 +91,12 @@ internal class AppReducer( handleNotificationClicked(state, message) is Message.NotificationClickHandlingMessage -> state to reduceNotificationClickHandlingMessage(message.message) - is Message.WelcomeOnboardingMessage -> - reduceWelcomeOnboardingMessage(state, message.message) is InternalMessage.SubscriptionChanged -> handleSubscriptionChanged(state, message) is Message.IsPaywallShownChanged -> handleIsPaywallShownChanged(state, message) is InternalMessage.PaymentAbilityResult -> handlePaymentAbilityResult(state, message) + is Message.WelcomeOnboardingCompleted -> handleWelcomeOnboardingCompleted(state, message) } ?: (state to emptySet()) private fun handleFetchAppStartupConfigSuccess( @@ -144,16 +140,16 @@ internal class AppReducer( ) ) ) - message.profile.isNewUser -> - add(Action.ViewAction.NavigateTo.TrackSelectionScreen) + WelcomeOnboardingFeature.shouldBeLaunchedOnStartup(message.profile) -> + add(NavigateTo.WelcomeOnboarding(message.profile)) shouldShowPaywall(readyState) -> add( - Action.ViewAction.NavigateTo.StudyPlanWithPaywall( + NavigateTo.StudyPlanWithPaywall( PaywallTransitionSource.APP_BECOMES_ACTIVE ) ) else -> - add(Action.ViewAction.NavigateTo.StudyPlan) + add(NavigateTo.StudyPlan) } addAll( getOnAuthorizedAppStartUpActions( @@ -174,7 +170,7 @@ internal class AppReducer( ) } addAll(getNotAuthorizedAppStartUpActions()) - add(Action.ViewAction.NavigateTo.WelcomeScreen) + add(NavigateTo.WelcomeScreen) } addAll(streakRecoveryActions) } @@ -195,15 +191,7 @@ internal class AppReducer( isMobileOnlySubscriptionEnabled = message.profile.features.isMobileOnlySubscriptionEnabled, canMakePayments = false ) - val (onboardingState, onboardingActions) = reduceWelcomeOnboardingMessage( - WelcomeOnboardingFeature.State(), - WelcomeOnboardingFeature.InternalMessage.OnboardingFlowRequested( - message.profile, - message.isNotificationPermissionGranted - ) - ) - authState.copy(welcomeOnboardingState = onboardingState) to - getAuthorizedUserActions(message.profile) + onboardingActions + authState to getAuthorizedUserActions(message.profile, message.isNotificationPermissionGranted) } else { state to emptySet() } @@ -213,7 +201,7 @@ internal class AppReducer( state.incrementAppShowsCount() to buildSet { when { shouldShowPaywall(state) -> - add(Action.ViewAction.NavigateTo.Paywall(PaywallTransitionSource.APP_BECOMES_ACTIVE)) + add(NavigateTo.Paywall(PaywallTransitionSource.APP_BECOMES_ACTIVE)) // Fetch actual payment ability state if it's not possible to make payment at the moment !state.canMakePayments && state.subscription?.isFreemium == true -> add(InternalAction.FetchPaymentAbility) @@ -283,50 +271,6 @@ internal class AppReducer( }.toSet() } - private fun reduceWelcomeOnboardingMessage( - state: State, - message: WelcomeOnboardingFeature.Message - ): ReducerResult = - if (state is State.Ready) { - val (onboardingState, actions) = - reduceWelcomeOnboardingMessage(state.welcomeOnboardingState, message) - state.copy(welcomeOnboardingState = onboardingState) to actions - } else { - state to emptySet() - } - - private fun reduceWelcomeOnboardingMessage( - state: WelcomeOnboardingFeature.State, - message: WelcomeOnboardingFeature.Message - ): Pair> { - val (onboardingState, onboardingActions) = - welcomeOnboardingReducer.reduce(state, message) - val finishAction = onboardingActions.getFinishAction() - return if (finishAction != null) { - onboardingState to handleWelcomeOnboardingFinishAction(finishAction) - } else { - onboardingState to - onboardingActions.map { - if (it is WelcomeOnboardingFeature.Action.ViewAction) { - Action.ViewAction.WelcomeOnboardingViewAction(it) - } else { - Action.WelcomeOnboardingAction(it) - } - }.toSet() - } - } - - private fun handleWelcomeOnboardingFinishAction( - finishAction: WelcomeOnboardingFeature.Action.OnboardingFlowFinished - ): Set = - setOf( - if (finishAction.profile?.isNewUser == true) { - Action.ViewAction.NavigateTo.TrackSelectionScreen - } else { - Action.ViewAction.NavigateTo.StudyPlan - } - ) - private fun getOnAuthorizedAppStartUpActions( profileId: Long, subscription: Subscription? @@ -341,8 +285,13 @@ internal class AppReducer( private fun getNotAuthorizedAppStartUpActions(): Set = setOf(Action.ClearUserInSentry) - private fun getAuthorizedUserActions(profile: Profile): Set = + private fun getAuthorizedUserActions(profile: Profile, isNotificationPermissionGranted: Boolean): Set = setOf( + if (WelcomeOnboardingFeature.shouldBeLaunchedAfterAuthorization(profile, isNotificationPermissionGranted)) { + NavigateTo.WelcomeOnboarding(profile) + } else { + NavigateTo.StudyPlan + }, InternalAction.FetchSubscription(forceUpdate = false), InternalAction.IdentifyUserInPurchaseSdk(userId = profile.id), Action.IdentifyUserInSentry(userId = profile.id), @@ -387,4 +336,16 @@ internal class AppReducer( } else { state to emptySet() } + + private fun handleWelcomeOnboardingCompleted( + state: State, + message: Message.WelcomeOnboardingCompleted + ): ReducerResult = + state to setOf( + if (message.stepRoute != null) { + NavigateTo.Step(message.stepRoute) + } else { + NavigateTo.StudyPlan + } + ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/injection/LegacyAppFeatureBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/injection/LegacyAppFeatureBuilder.kt new file mode 100644 index 0000000000..aa1fbd90ae --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/injection/LegacyAppFeatureBuilder.kt @@ -0,0 +1,98 @@ +package org.hyperskill.app.main_legacy.injection + +import co.touchlab.kermit.Logger +import org.hyperskill.app.auth.domain.interactor.AuthInteractor +import org.hyperskill.app.core.domain.BuildVariant +import org.hyperskill.app.core.injection.StateRepositoriesComponent +import org.hyperskill.app.core.presentation.ActionDispatcherOptions +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingActionDispatcher +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingReducer +import org.hyperskill.app.logging.presentation.wrapWithLogger +import org.hyperskill.app.main.domain.interactor.AppInteractor +import org.hyperskill.app.main_legacy.presentation.LegacyAppActionDispatcher +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.Action +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.Message +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.State +import org.hyperskill.app.main_legacy.presentation.LegacyAppReducer +import org.hyperskill.app.notification.click_handling.presentation.NotificationClickHandlingActionDispatcher +import org.hyperskill.app.notification.click_handling.presentation.NotificationClickHandlingReducer +import org.hyperskill.app.notification.local.domain.interactor.NotificationInteractor +import org.hyperskill.app.notification.remote.domain.interactor.PushNotificationsInteractor +import org.hyperskill.app.purchases.domain.interactor.PurchaseInteractor +import org.hyperskill.app.sentry.domain.interactor.SentryInteractor +import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryActionDispatcher +import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryReducer +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 +import ru.nobird.app.presentation.redux.feature.Feature +import ru.nobird.app.presentation.redux.feature.ReduxFeature + +@Deprecated("Should be removed in ALTAPPS-1276") +internal object LegacyAppFeatureBuilder { + private const val LOG_TAG = "AppFeature" + + fun build( + initialState: State?, + appInteractor: AppInteractor, + authInteractor: AuthInteractor, + sentryInteractor: SentryInteractor, + stateRepositoriesComponent: StateRepositoriesComponent, + streakRecoveryReducer: StreakRecoveryReducer, + streakRecoveryActionDispatcher: StreakRecoveryActionDispatcher, + clickedNotificationReducer: NotificationClickHandlingReducer, + notificationClickHandlingActionDispatcher: NotificationClickHandlingActionDispatcher, + notificationsInteractor: NotificationInteractor, + pushNotificationsInteractor: PushNotificationsInteractor, + legacyWelcomeOnboardingReducer: LegacyWelcomeOnboardingReducer, + legacyWelcomeOnboardingActionDispatcher: LegacyWelcomeOnboardingActionDispatcher, + purchaseInteractor: PurchaseInteractor, + currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, + subscriptionsInteractor: SubscriptionsInteractor, + logger: Logger, + buildVariant: BuildVariant + ): Feature { + val legacyAppReducer = LegacyAppReducer( + streakRecoveryReducer = streakRecoveryReducer, + notificationClickHandlingReducer = clickedNotificationReducer, + legacyWelcomeOnboardingReducer = legacyWelcomeOnboardingReducer + ).wrapWithLogger(buildVariant, logger, LOG_TAG) + + val legacyAppActionDispatcher = LegacyAppActionDispatcher( + config = ActionDispatcherOptions(), + appInteractor = appInteractor, + authInteractor = authInteractor, + sentryInteractor = sentryInteractor, + stateRepositoriesComponent = stateRepositoriesComponent, + notificationsInteractor = notificationsInteractor, + pushNotificationsInteractor = pushNotificationsInteractor, + purchaseInteractor = purchaseInteractor, + currentSubscriptionStateRepository = currentSubscriptionStateRepository, + subscriptionsInteractor = subscriptionsInteractor, + logger.withTag(LOG_TAG) + ) + + return ReduxFeature(initialState ?: State.Idle, legacyAppReducer) + .wrapWithActionDispatcher(legacyAppActionDispatcher) + .wrapWithActionDispatcher( + streakRecoveryActionDispatcher.transform( + transformAction = { it.safeCast()?.action }, + transformMessage = Message::StreakRecoveryMessage + ) + ) + .wrapWithActionDispatcher( + notificationClickHandlingActionDispatcher.transform( + transformAction = { it.safeCast()?.action }, + transformMessage = Message::NotificationClickHandlingMessage + ) + ) + .wrapWithActionDispatcher( + legacyWelcomeOnboardingActionDispatcher.transform( + transformAction = { it.safeCast()?.action }, + transformMessage = Message::WelcomeOnboardingMessage + ) + ) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/injection/LegacyMainComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/injection/LegacyMainComponent.kt new file mode 100644 index 0000000000..f3a14a3d65 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/injection/LegacyMainComponent.kt @@ -0,0 +1,14 @@ +package org.hyperskill.app.main_legacy.injection + +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature +import ru.nobird.app.presentation.redux.feature.Feature + +@Deprecated("Should be removed in ALTAPPS-1276") +interface LegacyMainComponent { + + /** + * Special method for iOS platform to create [LegacyAppFeature] without initial state. + * Internally calls [legacyAppFeature] with null as initialState. + */ + fun legacyAppFeature(): Feature +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/injection/LegacyMainComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/injection/LegacyMainComponentImpl.kt new file mode 100644 index 0000000000..951cc402d8 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/injection/LegacyMainComponentImpl.kt @@ -0,0 +1,46 @@ +package org.hyperskill.app.main_legacy.injection + +import org.hyperskill.app.core.injection.AppGraph +import org.hyperskill.app.legacy_welcome_onboarding.injection.LegacyWelcomeOnboardingComponent +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.Action +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.Message +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.State +import org.hyperskill.app.notification.click_handling.injection.NotificationClickHandlingComponent +import org.hyperskill.app.streak_recovery.injection.StreakRecoveryComponent +import ru.nobird.app.presentation.redux.feature.Feature + +@Deprecated("Should be removed in ALTAPPS-1276") +internal class LegacyMainComponentImpl(private val appGraph: AppGraph) : LegacyMainComponent { + private val streakRecoveryComponent: StreakRecoveryComponent = + appGraph.buildStreakRecoveryComponent() + + private val clickedNotificationComponent: NotificationClickHandlingComponent = + appGraph.buildClickedNotificationComponent() + + private val legacyWelcomeOnboardingComponent: LegacyWelcomeOnboardingComponent = + appGraph.buildLegacyWelcomeOnboardingComponent() + + /*ktlint-disable*/ + @Suppress("MaxLineLength") + override fun legacyAppFeature(): Feature = + LegacyAppFeatureBuilder.build( + initialState = null, + appInteractor = appGraph.buildMainDataComponent().appInteractor, + authInteractor = appGraph.authComponent.authInteractor, + sentryInteractor = appGraph.sentryComponent.sentryInteractor, + stateRepositoriesComponent = appGraph.stateRepositoriesComponent, + streakRecoveryReducer = streakRecoveryComponent.streakRecoveryReducer, + streakRecoveryActionDispatcher = streakRecoveryComponent.streakRecoveryActionDispatcher, + clickedNotificationReducer = clickedNotificationComponent.notificationClickHandlingReducer, + notificationClickHandlingActionDispatcher = clickedNotificationComponent.notificationClickHandlingActionDispatcher, + notificationsInteractor = appGraph.buildNotificationComponent().notificationInteractor, + pushNotificationsInteractor = appGraph.buildPushNotificationsComponent().pushNotificationsInteractor, + legacyWelcomeOnboardingReducer = legacyWelcomeOnboardingComponent.legacyWelcomeOnboardingReducer, + legacyWelcomeOnboardingActionDispatcher = legacyWelcomeOnboardingComponent.legacyWelcomeOnboardingActionDispatcher, + purchaseInteractor = appGraph.buildPurchaseComponent().purchaseInteractor, + currentSubscriptionStateRepository = appGraph.stateRepositoriesComponent.currentSubscriptionStateRepository, + subscriptionsInteractor = appGraph.subscriptionDataComponent.subscriptionsInteractor, + logger = appGraph.loggerComponent.logger, + buildVariant = appGraph.commonComponent.buildKonfig.buildVariant + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/presentation/LegacyAppActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/presentation/LegacyAppActionDispatcher.kt new file mode 100644 index 0000000000..a74ed62ee5 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/presentation/LegacyAppActionDispatcher.kt @@ -0,0 +1,238 @@ +package org.hyperskill.app.main_legacy.presentation + +import co.touchlab.kermit.Logger +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +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_legacy.presentation.LegacyAppFeature.Action +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.InternalAction +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.InternalMessage +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.Message +import org.hyperskill.app.notification.local.domain.interactor.NotificationInteractor +import org.hyperskill.app.notification.remote.domain.interactor.PushNotificationsInteractor +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 +import org.hyperskill.app.sentry.domain.model.transaction.HyperskillSentryTransactionBuilder +import org.hyperskill.app.sentry.domain.withTransaction +import org.hyperskill.app.subscriptions.domain.interactor.SubscriptionsInteractor +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.repository.CurrentSubscriptionStateRepository +import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher + +@Deprecated("Should be removed in ALTAPPS-1276") +internal class LegacyAppActionDispatcher( + config: ActionDispatcherOptions, + private val appInteractor: AppInteractor, + private val authInteractor: AuthInteractor, + private val sentryInteractor: SentryInteractor, + private val stateRepositoriesComponent: StateRepositoriesComponent, + private val notificationsInteractor: NotificationInteractor, + private val pushNotificationsInteractor: PushNotificationsInteractor, + private val purchaseInteractor: PurchaseInteractor, + private val currentSubscriptionStateRepository: CurrentSubscriptionStateRepository, + private val subscriptionsInteractor: SubscriptionsInteractor, + private val logger: Logger +) : CoroutineActionDispatcher(config.createConfig()) { + + init { + authInteractor + .observeUserDeauthorization() + .onEach { + when (it.reason) { + UserDeauthorized.Reason.TOKEN_REFRESH_FAILURE -> { + authInteractor.clearCache() + } + UserDeauthorized.Reason.SIGN_OUT -> { + appInteractor.doCurrentUserSignedOutCleanUp() + } + } + + stateRepositoriesComponent.resetRepositories() + + sentryInteractor.addBreadcrumb(HyperskillSentryBreadcrumbBuilder.buildAppUserDeauthorized(it.reason)) + + onNewMessage(Message.UserDeauthorized(it.reason)) + } + .launchIn(actionScope) + + currentSubscriptionStateRepository + .changes + .distinctUntilChanged() + .onEach { subscription -> + onNewMessage(InternalMessage.SubscriptionChanged(subscription)) + } + .launchIn(actionScope) + } + + override suspend fun doSuspendableAction(action: Action) { + when (action) { + is InternalAction.FetchAppStartupConfig -> + handleFetchAppStartupConfig(action, ::onNewMessage) + is Action.IdentifyUserInSentry -> + sentryInteractor.setUsedId(action.userId) + is Action.ClearUserInSentry -> + sentryInteractor.clearCurrentUser() + is Action.UpdateDailyLearningNotificationTime -> + handleUpdateDailyLearningNotificationTime() + is Action.SendPushNotificationsToken -> + pushNotificationsInteractor.renewFCMToken() + is InternalAction.IdentifyUserInPurchaseSdk -> + identifyUserInPurchaseSDK(action.userId) + is InternalAction.FetchPaymentAbility -> + handleFetchPaymentAbility(::onNewMessage) + is Action.LogAppLaunchFirstTimeAnalyticEventIfNeeded -> + appInteractor.logAppLaunchFirstTimeAnalyticEventIfNeeded() + is InternalAction.FetchSubscription -> + handleFetchSubscription(action, ::onNewMessage) + is InternalAction.RefreshSubscriptionOnExpiration -> + subscriptionsInteractor.refreshSubscriptionOnExpirationIfNeeded(action.subscription) + is InternalAction.CancelSubscriptionRefresh -> + subscriptionsInteractor.cancelSubscriptionRefresh() + else -> {} + } + } + + private suspend fun handleFetchAppStartupConfig( + action: InternalAction.FetchAppStartupConfig, + onNewMessage: (Message) -> Unit + ) { + val isAuthorized = + authInteractor.isAuthorized().getOrDefault(false) + + sentryInteractor.withTransaction( + transaction = HyperskillSentryTransactionBuilder.buildAppScreenRemoteDataLoading(isAuthorized), + onError = { e -> + sentryInteractor.addBreadcrumb( + HyperskillSentryBreadcrumbBuilder.buildAppDetermineUserAccountStatusError(e) + ) + Message.FetchAppStartupConfigError + } + ) { + coroutineScope { + sentryInteractor.addBreadcrumb(HyperskillSentryBreadcrumbBuilder.buildAppDetermineUserAccountStatus()) + + val profileDeferred = async { appInteractor.fetchProfile(isAuthorized) } + val subscriptionDeferred = async { fetchSubscription(isAuthorized = isAuthorized) } + + val profile = profileDeferred.await().getOrThrow() + val subscription = subscriptionDeferred.await() + + val canMakePayments = if (isAuthorized) { + // Identify user in the Purchase SDK if user is already authorized. + // Otherwise user will be identified later after authorization. + identifyUserInPurchaseSDK(profile.id) + .fold( + onSuccess = { + purchaseInteractor + .canMakePayments() + .getOrDefault(false) + }, + onFailure = { false } + ) + } else { + false + } + + sentryInteractor.addBreadcrumb( + HyperskillSentryBreadcrumbBuilder.buildAppDetermineUserAccountStatusSuccess() + ) + + Message.FetchAppStartupConfigSuccess( + profile = profile, + subscription = subscription, + notificationData = action.pushNotificationData, + canMakePayments = canMakePayments + ) + } + }.let(onNewMessage) + } + + private suspend fun fetchSubscription( + isAuthorized: Boolean = true, + forceUpdate: Boolean = false + ): Subscription? = + if (isAuthorized) { + currentSubscriptionStateRepository + .getStateWithSource(forceUpdate = forceUpdate) + .fold( + onSuccess = { (subscription, usedDataSourceType) -> + // Fetch subscription from remote + // if the user has the mobile-only subscription + // and its valid time is passed + val shouldFetchSubscriptionFromRemote = + usedDataSourceType == DataSourceType.CACHE && + subscription.type == SubscriptionType.MOBILE_ONLY && + (subscription.isExpired || subscription.isValidTillPassed()) + if (shouldFetchSubscriptionFromRemote) { + currentSubscriptionStateRepository + .getState(forceUpdate = true) + .getOrNull() + } else { + subscription + } + }, + onFailure = { + currentSubscriptionStateRepository + .getState(forceUpdate = true) + .onFailure { e -> + logger.e(e) { "Failed to fetch subscription" } + } + .getOrNull() + } + ) + } else { + null + } + + private suspend fun handleUpdateDailyLearningNotificationTime() { + notificationsInteractor + .updateTimeZone() + .onFailure { + sentryInteractor.captureErrorMessage( + "AppActionDispatcher: failed to update timezone\n$it" + ) + } + } + + private suspend fun handleFetchPaymentAbility(onNewMessage: (Message) -> Unit) { + onNewMessage( + InternalMessage.PaymentAbilityResult( + canMakePayments = purchaseInteractor + .canMakePayments() + .getOrDefault(false) + ) + ) + } + + private suspend fun identifyUserInPurchaseSDK(userId: Long): Result = + purchaseInteractor + .login(userId) + .onFailure { + logger.e(it) { + "Failed to login user in the purchase sdk" + } + } + + private suspend fun handleFetchSubscription( + action: InternalAction.FetchSubscription, + onNewMessage: (Message) -> Unit + ) { + fetchSubscription(forceUpdate = action.forceUpdate)?.let { + onNewMessage( + InternalMessage.SubscriptionChanged(it) + ) + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/presentation/LegacyAppFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/presentation/LegacyAppFeature.kt new file mode 100644 index 0000000000..89225f5938 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/presentation/LegacyAppFeature.kt @@ -0,0 +1,179 @@ +package org.hyperskill.app.main_legacy.presentation + +import kotlinx.serialization.Serializable +import org.hyperskill.app.auth.domain.model.UserDeauthorized.Reason +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingFeature +import org.hyperskill.app.notification.click_handling.presentation.NotificationClickHandlingFeature +import org.hyperskill.app.notification.remote.domain.model.PushNotificationData +import org.hyperskill.app.paywall.domain.model.PaywallTransitionSource +import org.hyperskill.app.profile.domain.model.Profile +import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryFeature +import org.hyperskill.app.subscriptions.domain.model.Subscription + +@Deprecated("Should be removed in ALTAPPS-1276") +object LegacyAppFeature { + + @Serializable + sealed interface State { + @Serializable + object Idle : State + + @Serializable + object Loading : State + + @Serializable + object NetworkError : State + + @Serializable + data class Ready( + val isAuthorized: Boolean, + val isMobileLeaderboardsEnabled: Boolean, + internal val streakRecoveryState: StreakRecoveryFeature.State = StreakRecoveryFeature.State(), + internal val welcomeOnboardingState: LegacyWelcomeOnboardingFeature.State = + LegacyWelcomeOnboardingFeature.State(), + internal val isMobileOnlySubscriptionEnabled: Boolean, + internal val canMakePayments: Boolean, + internal val subscription: Subscription? = null, + internal val appShowsCount: Int = 1, + internal val isPaywallShown: Boolean = false + ) : State { + internal fun incrementAppShowsCount(): Ready = + // ALTAPPS-1151: Fix redundant paywall shows -> increment app shows count only if paywall is not shown + if (!isPaywallShown) { + copy(appShowsCount = appShowsCount + 1) + } else { + this + } + } + } + + sealed interface Message { + data class Initialize( + val pushNotificationData: PushNotificationData?, + val forceUpdate: Boolean = false + ) : Message + + object AppBecomesActive : Message + + data class FetchAppStartupConfigSuccess( + val profile: Profile, + val subscription: Subscription?, + val notificationData: PushNotificationData?, + val canMakePayments: Boolean + ) : Message + object FetchAppStartupConfigError : Message + + data class UserAuthorized( + val profile: Profile, + val isNotificationPermissionGranted: Boolean + ) : Message + data class UserDeauthorized(val reason: Reason) : Message + + object OpenAuthScreen : Message + object OpenNewUserScreen : Message + + data class NotificationClicked( + val notificationData: PushNotificationData + ) : Message + + data class IsPaywallShownChanged( + val isPaywallShown: Boolean + ) : Message + + /** + * Message Wrappers + */ + data class StreakRecoveryMessage(val message: StreakRecoveryFeature.Message) : Message + + data class NotificationClickHandlingMessage( + val message: NotificationClickHandlingFeature.Message + ) : Message + + data class WelcomeOnboardingMessage( + val message: LegacyWelcomeOnboardingFeature.Message + ) : Message + } + + internal sealed interface InternalMessage : Message { + data class PaymentAbilityResult(val canMakePayments: Boolean) : InternalMessage + + data class SubscriptionChanged( + val subscription: Subscription + ) : InternalMessage + } + + sealed interface Action { + object UpdateDailyLearningNotificationTime : Action + + object SendPushNotificationsToken : Action + + object LogAppLaunchFirstTimeAnalyticEventIfNeeded : Action + + /** + * Action Wrappers + */ + data class StreakRecoveryAction(val action: StreakRecoveryFeature.Action) : Action + + data class ClickedNotificationAction( + val action: NotificationClickHandlingFeature.Action + ) : Action + + data class WelcomeOnboardingAction( + val action: LegacyWelcomeOnboardingFeature.Action + ) : Action + + /** + * Sentry + */ + data class IdentifyUserInSentry(val userId: Long) : Action + object ClearUserInSentry : Action + + sealed interface ViewAction : Action { + sealed interface NavigateTo : ViewAction { + data class AuthScreen(val isInSignUpMode: Boolean = false) : NavigateTo + object TrackSelectionScreen : NavigateTo + object WelcomeScreen : NavigateTo + object StudyPlan : NavigateTo + data class Paywall(val paywallTransitionSource: PaywallTransitionSource) : NavigateTo + data class StudyPlanWithPaywall( + val paywallTransitionSource: PaywallTransitionSource + ) : NavigateTo + } + + /** + * ViewAction Wrappers + */ + data class StreakRecoveryViewAction(val viewAction: StreakRecoveryFeature.Action.ViewAction) : ViewAction + + data class ClickedNotificationViewAction( + val viewAction: NotificationClickHandlingFeature.Action.ViewAction + ) : ViewAction + + data class WelcomeOnboardingViewAction( + val viewAction: LegacyWelcomeOnboardingFeature.Action.ViewAction + ) : ViewAction + } + } + + internal sealed interface InternalAction : Action { + /** + * Fetch data required for the App startup + * and identify user in Purchase SDK if the user has already authorized. + */ + data class FetchAppStartupConfig(val pushNotificationData: PushNotificationData?) : InternalAction + + data class FetchSubscription(val forceUpdate: Boolean) : InternalAction + + data class IdentifyUserInPurchaseSdk(val userId: Long) : InternalAction + + /** + * Check whether it's possible to make payment from user's device and account. + * @see [InternalMessage.PaymentAbilityResult] + */ + object FetchPaymentAbility : InternalAction + + data class RefreshSubscriptionOnExpiration(val subscription: Subscription) : InternalAction + + object CancelSubscriptionRefresh : InternalAction + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/presentation/LegacyAppReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/presentation/LegacyAppReducer.kt new file mode 100644 index 0000000000..bcdcfdebae --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main_legacy/presentation/LegacyAppReducer.kt @@ -0,0 +1,391 @@ +package org.hyperskill.app.main_legacy.presentation + +import org.hyperskill.app.auth.domain.model.UserDeauthorized +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingFeature +import org.hyperskill.app.legacy_welcome_onboarding.presentation.LegacyWelcomeOnboardingReducer +import org.hyperskill.app.legacy_welcome_onboarding.presentation.getFinishAction +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.Action +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.InternalAction +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.InternalMessage +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.Message +import org.hyperskill.app.main_legacy.presentation.LegacyAppFeature.State +import org.hyperskill.app.notification.click_handling.presentation.NotificationClickHandlingFeature +import org.hyperskill.app.notification.click_handling.presentation.NotificationClickHandlingReducer +import org.hyperskill.app.paywall.domain.model.PaywallTransitionSource +import org.hyperskill.app.profile.domain.model.Profile +import org.hyperskill.app.profile.domain.model.isMobileLeaderboardsEnabled +import org.hyperskill.app.profile.domain.model.isMobileOnlySubscriptionEnabled +import org.hyperskill.app.profile.domain.model.isNewUser +import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryFeature +import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryReducer +import org.hyperskill.app.subscriptions.domain.model.Subscription +import org.hyperskill.app.subscriptions.domain.model.isFreemium +import ru.nobird.app.presentation.redux.reducer.StateReducer + +private typealias ReducerResult = Pair> + +@Deprecated("Should be removed in ALTAPPS-1276") +internal class LegacyAppReducer( + private val streakRecoveryReducer: StreakRecoveryReducer, + private val notificationClickHandlingReducer: NotificationClickHandlingReducer, + private val legacyWelcomeOnboardingReducer: LegacyWelcomeOnboardingReducer +) : StateReducer { + + companion object { + internal const val APP_SHOWS_COUNT_TILL_PAYWALL = 3 + } + + override fun reduce( + state: State, + message: Message + ): ReducerResult = + when (message) { + is Message.Initialize -> { + if (state is State.Idle || (state is State.NetworkError && message.forceUpdate)) { + State.Loading to setOf( + InternalAction.FetchAppStartupConfig(message.pushNotificationData), + Action.LogAppLaunchFirstTimeAnalyticEventIfNeeded + ) + } else { + null + } + } + is Message.FetchAppStartupConfigSuccess -> + handleFetchAppStartupConfigSuccess(state, message) + is Message.FetchAppStartupConfigError -> + if (state is State.Loading) { + State.NetworkError to emptySet() + } else { + null + } + is Message.AppBecomesActive -> handleAppBecomesActive(state) + is Message.UserAuthorized -> + handleUserAuthorized(state, message) + is Message.UserDeauthorized -> + if (state is State.Ready && state.isAuthorized) { + val navigateToViewAction = when (message.reason) { + UserDeauthorized.Reason.TOKEN_REFRESH_FAILURE -> + Action.ViewAction.NavigateTo.WelcomeScreen + UserDeauthorized.Reason.SIGN_OUT -> + Action.ViewAction.NavigateTo.AuthScreen() + } + + State.Ready( + isAuthorized = false, + isMobileLeaderboardsEnabled = false, + isMobileOnlySubscriptionEnabled = false, + canMakePayments = false + ) to getDeauthorizedUserActions() + setOf(navigateToViewAction) + } else { + null + } + is Message.OpenAuthScreen -> + state to setOf(Action.ViewAction.NavigateTo.AuthScreen()) + is Message.OpenNewUserScreen -> + state to setOf(Action.ViewAction.NavigateTo.TrackSelectionScreen) + is Message.StreakRecoveryMessage -> + if (state is State.Ready) { + val (streakRecoveryState, streakRecoveryActions) = + reduceStreakRecoveryMessage(state.streakRecoveryState, message.message) + state.copy(streakRecoveryState = streakRecoveryState) to streakRecoveryActions + } else { + null + } + is Message.NotificationClicked -> + handleNotificationClicked(state, message) + is Message.NotificationClickHandlingMessage -> + state to reduceNotificationClickHandlingMessage(message.message) + is Message.WelcomeOnboardingMessage -> + reduceWelcomeOnboardingMessage(state, message.message) + is InternalMessage.SubscriptionChanged -> + handleSubscriptionChanged(state, message) + is Message.IsPaywallShownChanged -> + handleIsPaywallShownChanged(state, message) + is InternalMessage.PaymentAbilityResult -> handlePaymentAbilityResult(state, message) + } ?: (state to emptySet()) + + private fun handleFetchAppStartupConfigSuccess( + state: State, + message: Message.FetchAppStartupConfigSuccess + ): ReducerResult = + if (state is State.Loading) { + val isAuthorized = !message.profile.isGuest + + val (streakRecoveryState, streakRecoveryActions) = + if (isAuthorized && message.notificationData == null) { + reduceStreakRecoveryMessage( + StreakRecoveryFeature.State(), + StreakRecoveryFeature.Message.Initialize + ) + } else { + StreakRecoveryFeature.State() to emptySet() + } + + val readyState = State.Ready( + isAuthorized = isAuthorized, + isMobileLeaderboardsEnabled = message.profile.features.isMobileLeaderboardsEnabled, + streakRecoveryState = streakRecoveryState, + appShowsCount = 0, // This is a hack to show paywall on the first app start + subscription = message.subscription, + isMobileOnlySubscriptionEnabled = message.profile.features.isMobileOnlySubscriptionEnabled, + canMakePayments = message.canMakePayments + ) + + val actions: Set = + buildSet { + if (isAuthorized) { + when { + message.notificationData != null -> + addAll( + reduceNotificationClickHandlingMessage( + NotificationClickHandlingFeature.Message.NotificationClicked( + message.notificationData, + isUserAuthorized = true, + notificationLaunchedApp = true + ) + ) + ) + message.profile.isNewUser -> + add(Action.ViewAction.NavigateTo.TrackSelectionScreen) + shouldShowPaywall(readyState) -> + add( + Action.ViewAction.NavigateTo.StudyPlanWithPaywall( + PaywallTransitionSource.APP_BECOMES_ACTIVE + ) + ) + else -> + add(Action.ViewAction.NavigateTo.StudyPlan) + } + addAll( + getOnAuthorizedAppStartUpActions( + profileId = message.profile.id, + subscription = message.subscription + ) + ) + } else { + if (message.notificationData != null) { + addAll( + reduceNotificationClickHandlingMessage( + NotificationClickHandlingFeature.Message.NotificationClicked( + message.notificationData, + isUserAuthorized = false, + notificationLaunchedApp = true + ) + ) + ) + } + addAll(getNotAuthorizedAppStartUpActions()) + add(Action.ViewAction.NavigateTo.WelcomeScreen) + } + addAll(streakRecoveryActions) + } + + readyState.incrementAppShowsCount() to actions + } else { + state to emptySet() + } + + private fun handleUserAuthorized( + state: State, + message: Message.UserAuthorized + ): ReducerResult = + if (state is State.Ready && !state.isAuthorized) { + val authState = State.Ready( + isAuthorized = true, + isMobileLeaderboardsEnabled = message.profile.features.isMobileLeaderboardsEnabled, + isMobileOnlySubscriptionEnabled = message.profile.features.isMobileOnlySubscriptionEnabled, + canMakePayments = false + ) + val (onboardingState, onboardingActions) = reduceWelcomeOnboardingMessage( + LegacyWelcomeOnboardingFeature.State(), + LegacyWelcomeOnboardingFeature.InternalMessage.OnboardingFlowRequested( + message.profile, + message.isNotificationPermissionGranted + ) + ) + authState.copy(welcomeOnboardingState = onboardingState) to + getAuthorizedUserActions(message.profile) + onboardingActions + } else { + state to emptySet() + } + + private fun handleAppBecomesActive(state: State): ReducerResult = + if (state is State.Ready) { + state.incrementAppShowsCount() to buildSet { + when { + shouldShowPaywall(state) -> + add(Action.ViewAction.NavigateTo.Paywall(PaywallTransitionSource.APP_BECOMES_ACTIVE)) + // Fetch actual payment ability state if it's not possible to make payment at the moment + !state.canMakePayments && state.subscription?.isFreemium == true -> + add(InternalAction.FetchPaymentAbility) + } + if (state.isAuthorized) { + add(InternalAction.FetchSubscription(forceUpdate = true)) + } + } + } else { + state to emptySet() + } + + private fun shouldShowPaywall(state: State.Ready): Boolean = + state.isAuthorized && + state.isMobileOnlySubscriptionEnabled && + state.canMakePayments && + state.subscription?.isFreemium == true && + !state.isPaywallShown && + state.appShowsCount % APP_SHOWS_COUNT_TILL_PAYWALL == 0 + + private fun reduceStreakRecoveryMessage( + state: StreakRecoveryFeature.State, + message: StreakRecoveryFeature.Message + ): Pair> { + val (streakRecoveryState, streakRecoveryActions) = streakRecoveryReducer.reduce(state, message) + + val actions = streakRecoveryActions + .map { + if (it is StreakRecoveryFeature.Action.ViewAction) { + Action.ViewAction.StreakRecoveryViewAction(it) + } else { + Action.StreakRecoveryAction(it) + } + } + .toSet() + + return streakRecoveryState to actions + } + + private fun handleNotificationClicked( + state: State, + message: Message.NotificationClicked + ): ReducerResult = + if (state is State.Ready) { + state to reduceNotificationClickHandlingMessage( + NotificationClickHandlingFeature.Message.NotificationClicked( + notificationData = message.notificationData, + isUserAuthorized = state.isAuthorized, + notificationLaunchedApp = false + ) + ) + } else { + state to emptySet() + } + + private fun reduceNotificationClickHandlingMessage( + message: NotificationClickHandlingFeature.Message + ): Set { + val (_, notificationClickedHandlingActions) = + notificationClickHandlingReducer.reduce(NotificationClickHandlingFeature.State, message) + return notificationClickedHandlingActions.map { + if (it is NotificationClickHandlingFeature.Action.ViewAction) { + Action.ViewAction.ClickedNotificationViewAction(it) + } else { + Action.ClickedNotificationAction(it) + } + }.toSet() + } + + private fun reduceWelcomeOnboardingMessage( + state: State, + message: LegacyWelcomeOnboardingFeature.Message + ): ReducerResult = + if (state is State.Ready) { + val (onboardingState, actions) = + reduceWelcomeOnboardingMessage(state.welcomeOnboardingState, message) + state.copy(welcomeOnboardingState = onboardingState) to actions + } else { + state to emptySet() + } + + private fun reduceWelcomeOnboardingMessage( + state: LegacyWelcomeOnboardingFeature.State, + message: LegacyWelcomeOnboardingFeature.Message + ): Pair> { + val (onboardingState, onboardingActions) = + legacyWelcomeOnboardingReducer.reduce(state, message) + val finishAction = onboardingActions.getFinishAction() + return if (finishAction != null) { + onboardingState to handleWelcomeOnboardingFinishAction(finishAction) + } else { + onboardingState to + onboardingActions.map { + if (it is LegacyWelcomeOnboardingFeature.Action.ViewAction) { + Action.ViewAction.WelcomeOnboardingViewAction(it) + } else { + Action.WelcomeOnboardingAction(it) + } + }.toSet() + } + } + + private fun handleWelcomeOnboardingFinishAction( + finishAction: LegacyWelcomeOnboardingFeature.Action.OnboardingFlowFinished + ): Set = + setOf( + if (finishAction.profile?.isNewUser == true) { + Action.ViewAction.NavigateTo.TrackSelectionScreen + } else { + Action.ViewAction.NavigateTo.StudyPlan + } + ) + + private fun getOnAuthorizedAppStartUpActions( + profileId: Long, + subscription: Subscription? + ): Set = + setOfNotNull( + Action.IdentifyUserInSentry(userId = profileId), + subscription?.let(InternalAction::RefreshSubscriptionOnExpiration), + Action.UpdateDailyLearningNotificationTime, + Action.SendPushNotificationsToken + ) + + private fun getNotAuthorizedAppStartUpActions(): Set = + setOf(Action.ClearUserInSentry) + + private fun getAuthorizedUserActions(profile: Profile): Set = + setOf( + InternalAction.FetchSubscription(forceUpdate = false), + InternalAction.IdentifyUserInPurchaseSdk(userId = profile.id), + Action.IdentifyUserInSentry(userId = profile.id), + Action.UpdateDailyLearningNotificationTime, + Action.SendPushNotificationsToken + ) + + private fun getDeauthorizedUserActions(): Set = + setOf( + Action.ClearUserInSentry, + InternalAction.CancelSubscriptionRefresh + ) + + private fun handleSubscriptionChanged( + state: State, + message: InternalMessage.SubscriptionChanged + ): ReducerResult = + if (state is State.Ready) { + state.copy(subscription = message.subscription) to setOf( + InternalAction.RefreshSubscriptionOnExpiration(message.subscription) + ) + } else { + state to emptySet() + } + + private fun handleIsPaywallShownChanged( + state: State, + message: Message.IsPaywallShownChanged + ): ReducerResult = + if (state is State.Ready) { + state.copy(isPaywallShown = message.isPaywallShown) to emptySet() + } else { + state to emptySet() + } + + private fun handlePaymentAbilityResult( + state: State, + message: InternalMessage.PaymentAbilityResult + ): ReducerResult = + if (state is State.Ready) { + state.copy(canMakePayments = message.canMakePayments) to emptySet() + } else { + state to emptySet() + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/model/transaction/HyperskillSentryTransactionBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/model/transaction/HyperskillSentryTransactionBuilder.kt index 3a3458a1d2..9a99842cc4 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/model/transaction/HyperskillSentryTransactionBuilder.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/sentry/domain/model/transaction/HyperskillSentryTransactionBuilder.kt @@ -316,6 +316,12 @@ object HyperskillSentryTransactionBuilder { operation = HyperskillSentryTransactionOperation.API_LOAD ) + fun buildWelcomeOnboardingFeatureFetchNextLearningActivity(): HyperskillSentryTransaction = + HyperskillSentryTransaction( + name = "welcome-onboarding-feature-fetch-next-learning-activity", + operation = HyperskillSentryTransactionOperation.API_LOAD + ) + /** * ChallengeWidgetFeature */ diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/injection/UsersQuestionnaireOnboardingComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/injection/UsersQuestionnaireOnboardingComponent.kt deleted file mode 100644 index 222dac9bc2..0000000000 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/injection/UsersQuestionnaireOnboardingComponent.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.hyperskill.app.users_questionnaire_onboarding.injection - -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.Action -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.Message -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.ViewState -import ru.nobird.app.presentation.redux.feature.Feature - -interface UsersQuestionnaireOnboardingComponent { - val usersQuestionnaireOnboardingFeature: Feature -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/injection/UsersQuestionnaireOnboardingComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/injection/UsersQuestionnaireOnboardingComponentImpl.kt deleted file mode 100644 index 5eb39d8f08..0000000000 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/injection/UsersQuestionnaireOnboardingComponentImpl.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.hyperskill.app.users_questionnaire_onboarding.injection - -import org.hyperskill.app.core.injection.AppGraph -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.Action -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.Message -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.ViewState -import ru.nobird.app.presentation.redux.feature.Feature - -internal class UsersQuestionnaireOnboardingComponentImpl( - private val appGraph: AppGraph -) : UsersQuestionnaireOnboardingComponent { - override val usersQuestionnaireOnboardingFeature: Feature - get() = UsersQuestionnaireOnboardingFeatureBuilder.build( - analyticInteractor = appGraph.analyticComponent.analyticInteractor, - buildVariant = appGraph.commonComponent.buildKonfig.buildVariant, - logger = appGraph.loggerComponent.logger, - platform = appGraph.commonComponent.platform, - resourceProvider = appGraph.commonComponent.resourceProvider - ) -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingAnalyticParams.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingAnalyticParams.kt similarity index 62% rename from shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingAnalyticParams.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingAnalyticParams.kt index 1928dee96e..cf93a7293b 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingAnalyticParams.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingAnalyticParams.kt @@ -1,4 +1,4 @@ -package org.hyperskill.app.users_questionnaire_onboarding.domain.analytic +package org.hyperskill.app.users_questionnaire_onboarding_legacy.domain.analytic internal object UsersQuestionnaireOnboardingAnalyticParams { const val PARAM_SOURCE = "source" diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingClickedChoiceHyperskillAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingClickedChoiceHyperskillAnalyticEvent.kt similarity index 93% rename from shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingClickedChoiceHyperskillAnalyticEvent.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingClickedChoiceHyperskillAnalyticEvent.kt index 18f5535726..3c94968ef0 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingClickedChoiceHyperskillAnalyticEvent.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingClickedChoiceHyperskillAnalyticEvent.kt @@ -1,4 +1,4 @@ -package org.hyperskill.app.users_questionnaire_onboarding.domain.analytic +package org.hyperskill.app.users_questionnaire_onboarding_legacy.domain.analytic import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingClickedSendHyperskillAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingClickedSendHyperskillAnalyticEvent.kt similarity index 94% rename from shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingClickedSendHyperskillAnalyticEvent.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingClickedSendHyperskillAnalyticEvent.kt index 4068c8ab32..d1e9e41d8f 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingClickedSendHyperskillAnalyticEvent.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingClickedSendHyperskillAnalyticEvent.kt @@ -1,4 +1,4 @@ -package org.hyperskill.app.users_questionnaire_onboarding.domain.analytic +package org.hyperskill.app.users_questionnaire_onboarding_legacy.domain.analytic import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingClickedSkipHyperskillAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingClickedSkipHyperskillAnalyticEvent.kt similarity index 92% rename from shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingClickedSkipHyperskillAnalyticEvent.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingClickedSkipHyperskillAnalyticEvent.kt index 58fa3aa498..0d22ced8b1 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingClickedSkipHyperskillAnalyticEvent.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingClickedSkipHyperskillAnalyticEvent.kt @@ -1,4 +1,4 @@ -package org.hyperskill.app.users_questionnaire_onboarding.domain.analytic +package org.hyperskill.app.users_questionnaire_onboarding_legacy.domain.analytic import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingViewedHyperskillAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingViewedHyperskillAnalyticEvent.kt similarity index 88% rename from shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingViewedHyperskillAnalyticEvent.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingViewedHyperskillAnalyticEvent.kt index fec5c40d79..4770de96d5 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/domain/analytic/UsersQuestionnaireOnboardingViewedHyperskillAnalyticEvent.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/domain/analytic/UsersQuestionnaireOnboardingViewedHyperskillAnalyticEvent.kt @@ -1,4 +1,4 @@ -package org.hyperskill.app.users_questionnaire_onboarding.domain.analytic +package org.hyperskill.app.users_questionnaire_onboarding_legacy.domain.analytic import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/injection/LegacyUsersQuestionnaireOnboardingComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/injection/LegacyUsersQuestionnaireOnboardingComponent.kt new file mode 100644 index 0000000000..6451dbab13 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/injection/LegacyUsersQuestionnaireOnboardingComponent.kt @@ -0,0 +1,11 @@ +package org.hyperskill.app.users_questionnaire_onboarding_legacy.injection + +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.Action +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.Message +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.ViewState +import ru.nobird.app.presentation.redux.feature.Feature + +@Deprecated("Should be removed in ALTAPPS-1276") +interface LegacyUsersQuestionnaireOnboardingComponent { + val legacyUsersQuestionnaireOnboardingFeature: Feature +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/injection/LegacyUsersQuestionnaireOnboardingComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/injection/LegacyUsersQuestionnaireOnboardingComponentImpl.kt new file mode 100644 index 0000000000..3ecca02ef0 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/injection/LegacyUsersQuestionnaireOnboardingComponentImpl.kt @@ -0,0 +1,21 @@ +package org.hyperskill.app.users_questionnaire_onboarding_legacy.injection + +import org.hyperskill.app.core.injection.AppGraph +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.Action +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.Message +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.ViewState +import ru.nobird.app.presentation.redux.feature.Feature + +@Deprecated("Should be removed in ALTAPPS-1276") +internal class LegacyUsersQuestionnaireOnboardingComponentImpl( + private val appGraph: AppGraph +) : LegacyUsersQuestionnaireOnboardingComponent { + override val legacyUsersQuestionnaireOnboardingFeature: Feature + get() = LegacyUsersQuestionnaireOnboardingFeatureBuilder.build( + analyticInteractor = appGraph.analyticComponent.analyticInteractor, + buildVariant = appGraph.commonComponent.buildKonfig.buildVariant, + logger = appGraph.loggerComponent.logger, + platform = appGraph.commonComponent.platform, + resourceProvider = appGraph.commonComponent.resourceProvider + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/injection/UsersQuestionnaireOnboardingFeatureBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/injection/LegacyUsersQuestionnaireOnboardingFeatureBuilder.kt similarity index 53% rename from shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/injection/UsersQuestionnaireOnboardingFeatureBuilder.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/injection/LegacyUsersQuestionnaireOnboardingFeatureBuilder.kt index 8977af316a..604e643879 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/injection/UsersQuestionnaireOnboardingFeatureBuilder.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/injection/LegacyUsersQuestionnaireOnboardingFeatureBuilder.kt @@ -1,4 +1,4 @@ -package org.hyperskill.app.users_questionnaire_onboarding.injection +package org.hyperskill.app.users_questionnaire_onboarding_legacy.injection import co.touchlab.kermit.Logger import org.hyperskill.app.analytic.domain.interactor.AnalyticInteractor @@ -8,17 +8,18 @@ import org.hyperskill.app.core.domain.platform.Platform import org.hyperskill.app.core.presentation.transformState import org.hyperskill.app.core.view.mapper.ResourceProvider import org.hyperskill.app.logging.presentation.wrapWithLogger -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.Action -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.InternalAction -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.Message -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.ViewState -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingReducer -import org.hyperskill.app.users_questionnaire_onboarding.view.mapper.UsersQuestionnaireOnboardingViewStateMapper +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.Action +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.InternalAction +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.Message +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.ViewState +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingReducer +import org.hyperskill.app.users_questionnaire_onboarding_legacy.view.mapper.UsersQuestionnaireOnboardingViewStateMapper import ru.nobird.app.presentation.redux.feature.Feature import ru.nobird.app.presentation.redux.feature.ReduxFeature -internal object UsersQuestionnaireOnboardingFeatureBuilder { +@Deprecated("Should be removed in ALTAPPS-1276") +internal object LegacyUsersQuestionnaireOnboardingFeatureBuilder { private const val LOG_TAG = "UsersQuestionnaireOnboardingFeature" fun build( @@ -28,7 +29,7 @@ internal object UsersQuestionnaireOnboardingFeatureBuilder { platform: Platform, resourceProvider: ResourceProvider ): Feature { - val reducer = UsersQuestionnaireOnboardingReducer(resourceProvider) + val reducer = LegacyUsersQuestionnaireOnboardingReducer(resourceProvider) .wrapWithLogger(buildVariant, logger, LOG_TAG) val viewStateMapper = UsersQuestionnaireOnboardingViewStateMapper( @@ -37,7 +38,7 @@ internal object UsersQuestionnaireOnboardingFeatureBuilder { ) return ReduxFeature( - initialState = UsersQuestionnaireOnboardingFeature.State(), + initialState = LegacyUsersQuestionnaireOnboardingFeature.State(), reducer = reducer ) .transformState(viewStateMapper::mapState) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/presentation/UsersQuestionnaireOnboardingFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/presentation/LegacyUsersQuestionnaireOnboardingFeature.kt similarity index 87% rename from shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/presentation/UsersQuestionnaireOnboardingFeature.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/presentation/LegacyUsersQuestionnaireOnboardingFeature.kt index 9223345a99..c662a35ede 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/presentation/UsersQuestionnaireOnboardingFeature.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/presentation/LegacyUsersQuestionnaireOnboardingFeature.kt @@ -1,8 +1,9 @@ -package org.hyperskill.app.users_questionnaire_onboarding.presentation +package org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation import org.hyperskill.app.analytic.domain.model.AnalyticEvent -object UsersQuestionnaireOnboardingFeature { +@Deprecated("Should be removed in ALTAPPS-1276") +object LegacyUsersQuestionnaireOnboardingFeature { internal data class State( val selectedChoice: String? = null, val textInputValue: String? = null diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/presentation/UsersQuestionnaireOnboardingReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/presentation/LegacyUsersQuestionnaireOnboardingReducer.kt similarity index 68% rename from shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/presentation/UsersQuestionnaireOnboardingReducer.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/presentation/LegacyUsersQuestionnaireOnboardingReducer.kt index 97eb698bbe..9834d50e24 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/presentation/UsersQuestionnaireOnboardingReducer.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/presentation/LegacyUsersQuestionnaireOnboardingReducer.kt @@ -1,20 +1,21 @@ -package org.hyperskill.app.users_questionnaire_onboarding.presentation +package org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation import org.hyperskill.app.SharedResources import org.hyperskill.app.core.view.mapper.ResourceProvider -import org.hyperskill.app.users_questionnaire_onboarding.domain.analytic.UsersQuestionnaireOnboardingClickedChoiceHyperskillAnalyticEvent -import org.hyperskill.app.users_questionnaire_onboarding.domain.analytic.UsersQuestionnaireOnboardingClickedSendHyperskillAnalyticEvent -import org.hyperskill.app.users_questionnaire_onboarding.domain.analytic.UsersQuestionnaireOnboardingClickedSkipHyperskillAnalyticEvent -import org.hyperskill.app.users_questionnaire_onboarding.domain.analytic.UsersQuestionnaireOnboardingViewedHyperskillAnalyticEvent -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.Action -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.InternalAction -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.Message -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.State +import org.hyperskill.app.users_questionnaire_onboarding_legacy.domain.analytic.UsersQuestionnaireOnboardingClickedChoiceHyperskillAnalyticEvent +import org.hyperskill.app.users_questionnaire_onboarding_legacy.domain.analytic.UsersQuestionnaireOnboardingClickedSendHyperskillAnalyticEvent +import org.hyperskill.app.users_questionnaire_onboarding_legacy.domain.analytic.UsersQuestionnaireOnboardingClickedSkipHyperskillAnalyticEvent +import org.hyperskill.app.users_questionnaire_onboarding_legacy.domain.analytic.UsersQuestionnaireOnboardingViewedHyperskillAnalyticEvent +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.Action +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.InternalAction +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.Message +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.State import ru.nobird.app.presentation.redux.reducer.StateReducer private typealias ReducerResult = Pair> -internal class UsersQuestionnaireOnboardingReducer( +@Deprecated("Should be removed in ALTAPPS-1276") +internal class LegacyUsersQuestionnaireOnboardingReducer( private val resourceProvider: ResourceProvider ) : StateReducer { override fun reduce(state: State, message: Message): ReducerResult = diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/view/mapper/UsersQuestionnaireOnboardingViewStateMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/view/mapper/UsersQuestionnaireOnboardingViewStateMapper.kt similarity index 85% rename from shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/view/mapper/UsersQuestionnaireOnboardingViewStateMapper.kt rename to shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/view/mapper/UsersQuestionnaireOnboardingViewStateMapper.kt index 230651f89b..a746fb59be 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding/view/mapper/UsersQuestionnaireOnboardingViewStateMapper.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/users_questionnaire_onboarding_legacy/view/mapper/UsersQuestionnaireOnboardingViewStateMapper.kt @@ -1,12 +1,13 @@ -package org.hyperskill.app.users_questionnaire_onboarding.view.mapper +package org.hyperskill.app.users_questionnaire_onboarding_legacy.view.mapper import org.hyperskill.app.SharedResources import org.hyperskill.app.core.domain.platform.Platform import org.hyperskill.app.core.domain.platform.PlatformType import org.hyperskill.app.core.view.mapper.ResourceProvider -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.State -import org.hyperskill.app.users_questionnaire_onboarding.presentation.UsersQuestionnaireOnboardingFeature.ViewState +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.State +import org.hyperskill.app.users_questionnaire_onboarding_legacy.presentation.LegacyUsersQuestionnaireOnboardingFeature.ViewState +@Deprecated("Should be removed in ALTAPPS-1276") internal class UsersQuestionnaireOnboardingViewStateMapper( platform: Platform, resourceProvider: ResourceProvider diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/injection/WelcomeOnboardingFinishComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/injection/WelcomeOnboardingFinishComponent.kt new file mode 100644 index 0000000000..9d74cec085 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/injection/WelcomeOnboardingFinishComponent.kt @@ -0,0 +1,7 @@ +package org.hyperskill.app.welcome_onboarding.finish.injection + +import org.hyperskill.app.welcome_onboarding.finish.view.WelcomeOnboardingFinishViewStateMapper + +interface WelcomeOnboardingFinishComponent { + val welcomeOnboardingFinishViewStateMapper: WelcomeOnboardingFinishViewStateMapper +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/injection/WelcomeOnboardingFinishComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/injection/WelcomeOnboardingFinishComponentImpl.kt new file mode 100644 index 0000000000..8cde487838 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/injection/WelcomeOnboardingFinishComponentImpl.kt @@ -0,0 +1,11 @@ +package org.hyperskill.app.welcome_onboarding.finish.injection + +import org.hyperskill.app.core.injection.AppGraph +import org.hyperskill.app.welcome_onboarding.finish.view.WelcomeOnboardingFinishViewStateMapper + +class WelcomeOnboardingFinishComponentImpl( + private val appGraph: AppGraph +) : WelcomeOnboardingFinishComponent { + override val welcomeOnboardingFinishViewStateMapper: WelcomeOnboardingFinishViewStateMapper + get() = WelcomeOnboardingFinishViewStateMapper(appGraph.commonComponent.resourceProvider) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/view/WelcomeOnboardingFinishViewState.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/view/WelcomeOnboardingFinishViewState.kt new file mode 100644 index 0000000000..a72a04b679 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/view/WelcomeOnboardingFinishViewState.kt @@ -0,0 +1,7 @@ +package org.hyperskill.app.welcome_onboarding.finish.view + +data class WelcomeOnboardingFinishViewState( + val title: String, + val description: String, + val buttonText: String +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/view/WelcomeOnboardingFinishViewStateMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/view/WelcomeOnboardingFinishViewStateMapper.kt new file mode 100644 index 0000000000..ca2fe53746 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/finish/view/WelcomeOnboardingFinishViewStateMapper.kt @@ -0,0 +1,20 @@ +package org.hyperskill.app.welcome_onboarding.finish.view + +import org.hyperskill.app.SharedResources +import org.hyperskill.app.core.view.mapper.ResourceProvider +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.track_details.view.titleStringResource + +class WelcomeOnboardingFinishViewStateMapper( + private val resourceProvider: ResourceProvider +) { + fun map(track: WelcomeOnboardingTrack): WelcomeOnboardingFinishViewState = + WelcomeOnboardingFinishViewState( + title = resourceProvider.getString(SharedResources.strings.welcome_onboarding_finish_screen_title), + description = resourceProvider.getString( + SharedResources.strings.welcome_onboarding_finish_screen_description, + resourceProvider.getString(track.titleStringResource) + ), + buttonText = resourceProvider.getString(SharedResources.strings.welcome_onboarding_finish_btn) + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/injection/WelcomeOnboardingComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/injection/WelcomeOnboardingComponent.kt deleted file mode 100644 index 44a69ffd8b..0000000000 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/injection/WelcomeOnboardingComponent.kt +++ /dev/null @@ -1,9 +0,0 @@ -package org.hyperskill.app.welcome_onboarding.injection - -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingActionDispatcher -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingReducer - -interface WelcomeOnboardingComponent { - val welcomeOnboardingReducer: WelcomeOnboardingReducer - val welcomeOnboardingActionDispatcher: WelcomeOnboardingActionDispatcher -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/injection/WelcomeOnboardingComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/injection/WelcomeOnboardingComponentImpl.kt deleted file mode 100644 index 8059f25790..0000000000 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/injection/WelcomeOnboardingComponentImpl.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.hyperskill.app.welcome_onboarding.injection - -import org.hyperskill.app.core.injection.AppGraph -import org.hyperskill.app.core.presentation.ActionDispatcherOptions -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingActionDispatcher -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingReducer - -internal class WelcomeOnboardingComponentImpl( - private val appGraph: AppGraph -) : WelcomeOnboardingComponent { - override val welcomeOnboardingReducer: WelcomeOnboardingReducer - get() = WelcomeOnboardingReducer() - - override val welcomeOnboardingActionDispatcher: WelcomeOnboardingActionDispatcher - get() = WelcomeOnboardingActionDispatcher( - config = ActionDispatcherOptions(), - onboardingInteractor = appGraph.buildOnboardingDataComponent().onboardingInteractor - ) -} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/model/WelcomeOnboardingFeatureParams.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/model/WelcomeOnboardingFeatureParams.kt new file mode 100644 index 0000000000..c811114e72 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/model/WelcomeOnboardingFeatureParams.kt @@ -0,0 +1,10 @@ +package org.hyperskill.app.welcome_onboarding.model + +import kotlinx.serialization.Serializable +import org.hyperskill.app.profile.domain.model.Profile + +@Serializable +data class WelcomeOnboardingFeatureParams( + val profile: Profile, + val isNotificationPermissionGranted: Boolean +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/model/WelcomeOnboardingTrack.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/model/WelcomeOnboardingTrack.kt new file mode 100644 index 0000000000..9c43869824 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/model/WelcomeOnboardingTrack.kt @@ -0,0 +1,9 @@ +package org.hyperskill.app.welcome_onboarding.model + +enum class WelcomeOnboardingTrack { + JAVA, + JAVA_SCRIPT, + KOTLIN, + PYTHON, + SQL +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/injection/WelcomeQuestionnaireComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/injection/WelcomeQuestionnaireComponent.kt new file mode 100644 index 0000000000..36b8328e71 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/injection/WelcomeQuestionnaireComponent.kt @@ -0,0 +1,7 @@ +package org.hyperskill.app.welcome_onboarding.questionnaire.injection + +import org.hyperskill.app.welcome_onboarding.questionnaire.view.WelcomeQuestionnaireViewStateMapper + +interface WelcomeQuestionnaireComponent { + val welcomeQuestionnaireViewStateMapper: WelcomeQuestionnaireViewStateMapper +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/injection/WelcomeQuestionnaireComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/injection/WelcomeQuestionnaireComponentImpl.kt new file mode 100644 index 0000000000..a8fa3ef83d --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/injection/WelcomeQuestionnaireComponentImpl.kt @@ -0,0 +1,14 @@ +package org.hyperskill.app.welcome_onboarding.questionnaire.injection + +import org.hyperskill.app.core.injection.AppGraph +import org.hyperskill.app.welcome_onboarding.questionnaire.view.WelcomeQuestionnaireViewStateMapper + +class WelcomeQuestionnaireComponentImpl( + private val appGraph: AppGraph +) : WelcomeQuestionnaireComponent { + override val welcomeQuestionnaireViewStateMapper: WelcomeQuestionnaireViewStateMapper + get() = WelcomeQuestionnaireViewStateMapper( + resourceProvider = appGraph.commonComponent.resourceProvider, + platformType = appGraph.commonComponent.platform.platformType + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/model/WelcomeQuestionnaireItemType.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/model/WelcomeQuestionnaireItemType.kt new file mode 100644 index 0000000000..04063b2212 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/model/WelcomeQuestionnaireItemType.kt @@ -0,0 +1,30 @@ +package org.hyperskill.app.welcome_onboarding.questionnaire.model + +sealed interface WelcomeQuestionnaireItemType { + enum class ClientSource : WelcomeQuestionnaireItemType { + TIK_TOK, + GOOGLE_SEARCH, + NEWS, + APP_STORE, + FACEBOOK, + FRIENDS, + YOUTUBE, + OTHER + } + + enum class LearningGoal : WelcomeQuestionnaireItemType { + START_CAREER, + CURRENT_JOB, + CHANGE_STACK, + STUDIES, + FUN, + OTHER + } + + enum class CodingBackground : WelcomeQuestionnaireItemType { + NO_CODING_EXPERIENCE, + BASIC_UNDERSTANDING, + WRITTEN_SOME_PROJECTS, + WORKING_PROFESSIONALLY + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/model/WelcomeQuestionnaireType.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/model/WelcomeQuestionnaireType.kt new file mode 100644 index 0000000000..67445912f6 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/model/WelcomeQuestionnaireType.kt @@ -0,0 +1,7 @@ +package org.hyperskill.app.welcome_onboarding.questionnaire.model + +enum class WelcomeQuestionnaireType { + HOW_DID_YOU_HEAR_ABOUT_HYPERSKILL, + LEARNING_REASON, + CODING_EXPERIENCE +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireItem.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireItem.kt new file mode 100644 index 0000000000..a8425773d2 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireItem.kt @@ -0,0 +1,8 @@ +package org.hyperskill.app.welcome_onboarding.questionnaire.view + +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType + +data class WelcomeQuestionnaireItem( + val type: WelcomeQuestionnaireItemType, + val text: String +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireViewState.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireViewState.kt new file mode 100644 index 0000000000..94b331eafd --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireViewState.kt @@ -0,0 +1,6 @@ +package org.hyperskill.app.welcome_onboarding.questionnaire.view + +data class WelcomeQuestionnaireViewState( + val title: String, + val items: List +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireViewStateMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireViewStateMapper.kt new file mode 100644 index 0000000000..6e72cc4788 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/questionnaire/view/WelcomeQuestionnaireViewStateMapper.kt @@ -0,0 +1,150 @@ +package org.hyperskill.app.welcome_onboarding.questionnaire.view + +import org.hyperskill.app.SharedResources +import org.hyperskill.app.core.domain.platform.PlatformType +import org.hyperskill.app.core.view.mapper.ResourceProvider +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType.ClientSource +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType.CodingBackground +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType.LearningGoal +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireType + +class WelcomeQuestionnaireViewStateMapper( + private val resourceProvider: ResourceProvider, + private val platformType: PlatformType +) { + fun mapQuestionnaireTypeToViewState(type: WelcomeQuestionnaireType): WelcomeQuestionnaireViewState = + when (type) { + WelcomeQuestionnaireType.HOW_DID_YOU_HEAR_ABOUT_HYPERSKILL -> + getHowDidYouHearAboutHyperskillViewState() + WelcomeQuestionnaireType.LEARNING_REASON -> getLearningReasonViewState() + WelcomeQuestionnaireType.CODING_EXPERIENCE -> getCodingBackgroundViewState() + } + + private fun getHowDidYouHearAboutHyperskillViewState(): WelcomeQuestionnaireViewState = + WelcomeQuestionnaireViewState( + title = resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_how_did_you_hear_about_hyperskill_title + ), + items = listOf( + WelcomeQuestionnaireItem( + ClientSource.TIK_TOK, + resourceProvider.getString(SharedResources.strings.welcome_questionnaire_tiktok_item) + ), + WelcomeQuestionnaireItem( + ClientSource.GOOGLE_SEARCH, + resourceProvider.getString(SharedResources.strings.welcome_questionnaire_google_item) + ), + WelcomeQuestionnaireItem( + ClientSource.NEWS, + resourceProvider.getString(SharedResources.strings.welcome_questionnaire_news_item) + ), + WelcomeQuestionnaireItem( + ClientSource.APP_STORE, + resourceProvider.getString( + when (platformType) { + PlatformType.IOS -> + SharedResources.strings.welcome_questionnaire_app_store_item + PlatformType.ANDROID -> + SharedResources.strings.welcome_questionnaire_play_store_item + } + ) + ), + WelcomeQuestionnaireItem( + ClientSource.FACEBOOK, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_facebook_item + ) + ), + WelcomeQuestionnaireItem( + ClientSource.FRIENDS, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_friends_item + ) + ), + WelcomeQuestionnaireItem( + ClientSource.YOUTUBE, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_youtube_item + ) + ), + WelcomeQuestionnaireItem( + ClientSource.OTHER, + resourceProvider.getString(SharedResources.strings.welcome_questionnaire_other_item) + ) + ) + ) + + private fun getLearningReasonViewState(): WelcomeQuestionnaireViewState = + WelcomeQuestionnaireViewState( + title = resourceProvider.getString(SharedResources.strings.welcome_questionnaire_learning_goal_title), + items = listOf( + WelcomeQuestionnaireItem( + LearningGoal.START_CAREER, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_learning_goal_start_career_item + ) + ), + WelcomeQuestionnaireItem( + LearningGoal.CURRENT_JOB, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_learning_goal_current_job_item + ) + ), + WelcomeQuestionnaireItem( + LearningGoal.CHANGE_STACK, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_learning_goal_change_stack_item + ) + ), + WelcomeQuestionnaireItem( + LearningGoal.STUDIES, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_learning_goal_study_item + ) + ), + WelcomeQuestionnaireItem( + LearningGoal.FUN, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_learning_goal_fun_item + ) + ), + WelcomeQuestionnaireItem( + LearningGoal.OTHER, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_learning_goal_different_reason_item + ) + ) + ) + ) + + private fun getCodingBackgroundViewState(): WelcomeQuestionnaireViewState = + WelcomeQuestionnaireViewState( + title = resourceProvider.getString(SharedResources.strings.welcome_questionnaire_background_title), + items = listOf( + WelcomeQuestionnaireItem( + CodingBackground.NO_CODING_EXPERIENCE, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_background_no_experience_item + ) + ), + WelcomeQuestionnaireItem( + CodingBackground.BASIC_UNDERSTANDING, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_background_basic_item + ) + ), + WelcomeQuestionnaireItem( + CodingBackground.WRITTEN_SOME_PROJECTS, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_background_medium_item + ) + ), + WelcomeQuestionnaireItem( + CodingBackground.WORKING_PROFESSIONALLY, + resourceProvider.getString( + SharedResources.strings.welcome_questionnaire_background_pro_item + ) + ) + ) + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingFinishScreenStartClickedHSAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingFinishScreenStartClickedHSAnalyticEvent.kt new file mode 100644 index 0000000000..0db05cb123 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingFinishScreenStartClickedHSAnalyticEvent.kt @@ -0,0 +1,28 @@ +package org.hyperskill.app.welcome_onboarding.root.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget + +/** + * Represents click on the "Start my journey" button on the finish onboarding screen analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/onboarding/finish", + * "action": "click", + * "part": "main", + * "target": "start_my_journey" + * } + * ``` + * @see HyperskillAnalyticEvent + */ +object WelcomeOnboardingFinishScreenStartClickedHSAnalyticEvent : HyperskillAnalyticEvent( + route = HyperskillAnalyticRoute.Onboarding.Finish, + action = HyperskillAnalyticAction.CLICK, + part = HyperskillAnalyticPart.MAIN, + target = HyperskillAnalyticTarget.START_MY_JOURNEY +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingFinishScreenViewedHSAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingFinishScreenViewedHSAnalyticEvent.kt new file mode 100644 index 0000000000..2c06010f72 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingFinishScreenViewedHSAnalyticEvent.kt @@ -0,0 +1,23 @@ +package org.hyperskill.app.welcome_onboarding.root.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute + +/** + * Represents a view analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/onboarding/finish", + * "action": "view" + * } + * ``` + * + * @see HyperskillAnalyticEvent + */ +object WelcomeOnboardingFinishScreenViewedHSAnalyticEvent : HyperskillAnalyticEvent( + HyperskillAnalyticRoute.Onboarding.Finish, + HyperskillAnalyticAction.VIEW +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingProgrammingLanguageClickedHSAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingProgrammingLanguageClickedHSAnalyticEvent.kt new file mode 100644 index 0000000000..c09e43b377 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingProgrammingLanguageClickedHSAnalyticEvent.kt @@ -0,0 +1,41 @@ +package org.hyperskill.app.welcome_onboarding.root.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget +import org.hyperskill.app.welcome_onboarding.root.model.WelcomeOnboardingProgrammingLanguage + +/** + * Represents click on the programming language button analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/onboarding/programming-language-select", + * "action": "click", + * "part": "main", + * "target": "programming_language", + * "context": { + * "programming_language": "python" + * } + * } + * ``` + * @see HyperskillAnalyticEvent + */ +class WelcomeOnboardingProgrammingLanguageClickedHSAnalyticEvent( + programmingLanguage: WelcomeOnboardingProgrammingLanguage +) : HyperskillAnalyticEvent( + route = HyperskillAnalyticRoute.Onboarding.ProgrammingLanguageQuestionnaire, + action = HyperskillAnalyticAction.CLICK, + part = HyperskillAnalyticPart.MAIN, + target = HyperskillAnalyticTarget.PROGRAMMING_LANGUAGE, + context = mapOf( + PROGRAMMING_LANGUAGE_KEY to programmingLanguage.name.lowercase() + ) +) { + companion object { + private const val PROGRAMMING_LANGUAGE_KEY = "programming_language" + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingQuestionnaireItemClickedHSAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingQuestionnaireItemClickedHSAnalyticEvent.kt new file mode 100644 index 0000000000..ce0091ed54 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingQuestionnaireItemClickedHSAnalyticEvent.kt @@ -0,0 +1,50 @@ +package org.hyperskill.app.welcome_onboarding.root.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireType + +/** + * Represents click on the questionnaire item button analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/onboarding/questionnaire", + * "action": "click", + * "part": "main", + * "target": "questionnaire_item", + * "context": { + * "questionnaire_type": "how_did_you_hear_about_hyperskill" + * "option": "tik_tok" + * } + * } + * ``` + * @see HyperskillAnalyticEvent + */ +class WelcomeOnboardingQuestionnaireItemClickedHSAnalyticEvent( + questionnaireType: WelcomeQuestionnaireType, + questionnaireItemType: WelcomeQuestionnaireItemType +) : HyperskillAnalyticEvent( + route = HyperskillAnalyticRoute.Onboarding.UsersQuestionnaire, + action = HyperskillAnalyticAction.CLICK, + part = HyperskillAnalyticPart.MAIN, + target = HyperskillAnalyticTarget.QUESTIONNAIRE_ITEM, + context = mapOf( + QUESTIONNAIRE_TYPE_KEY to questionnaireType.name.lowercase(), + OPTION_KEY to when (questionnaireItemType) { + is WelcomeQuestionnaireItemType.ClientSource -> questionnaireItemType.name.lowercase() + is WelcomeQuestionnaireItemType.CodingBackground -> questionnaireItemType.name.lowercase() + is WelcomeQuestionnaireItemType.LearningGoal -> questionnaireItemType.name.lowercase() + } + ) +) { + companion object { + private const val QUESTIONNAIRE_TYPE_KEY = "questionnaire_type" + private const val OPTION_KEY = "option" + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingSelectProgrammingLanguageViewedHSAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingSelectProgrammingLanguageViewedHSAnalyticEvent.kt new file mode 100644 index 0000000000..954a201bac --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingSelectProgrammingLanguageViewedHSAnalyticEvent.kt @@ -0,0 +1,23 @@ +package org.hyperskill.app.welcome_onboarding.root.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute + +/** + * Represents a view analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/onboarding/programming-language-select", + * "action": "view" + * } + * ``` + * + * @see HyperskillAnalyticEvent + */ +object WelcomeOnboardingSelectProgrammingLanguageViewedHSAnalyticEvent : HyperskillAnalyticEvent( + HyperskillAnalyticRoute.Onboarding.ProgrammingLanguageQuestionnaire, + HyperskillAnalyticAction.VIEW +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingStartJourneyClickedHSAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingStartJourneyClickedHSAnalyticEvent.kt new file mode 100644 index 0000000000..1fe9b41c39 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingStartJourneyClickedHSAnalyticEvent.kt @@ -0,0 +1,28 @@ +package org.hyperskill.app.welcome_onboarding.root.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget + +/** + * Represents click on the "Start my journey" button analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/onboarding", + * "action": "click", + * "part": "main", + * "target": "start_my_journey" + * } + * ``` + * @see HyperskillAnalyticEvent + */ +object WelcomeOnboardingStartJourneyClickedHSAnalyticEvent : HyperskillAnalyticEvent( + HyperskillAnalyticRoute.Onboarding(), + HyperskillAnalyticAction.CLICK, + HyperskillAnalyticPart.MAIN, + HyperskillAnalyticTarget.START_MY_JOURNEY +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingStartScreenViewedHSAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingStartScreenViewedHSAnalyticEvent.kt new file mode 100644 index 0000000000..9986a6cb7b --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingStartScreenViewedHSAnalyticEvent.kt @@ -0,0 +1,23 @@ +package org.hyperskill.app.welcome_onboarding.root.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute + +/** + * Represents a view analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/onboarding", + * "action": "view" + * } + * ``` + * + * @see HyperskillAnalyticEvent + */ +object WelcomeOnboardingStartScreenViewedHSAnalyticEvent : HyperskillAnalyticEvent( + HyperskillAnalyticRoute.Onboarding(), + HyperskillAnalyticAction.VIEW +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingUserQuestionnaireViewedHSAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingUserQuestionnaireViewedHSAnalyticEvent.kt new file mode 100644 index 0000000000..9f1b34d93c --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/domain/analytic/WelcomeOnboardingUserQuestionnaireViewedHSAnalyticEvent.kt @@ -0,0 +1,35 @@ +package org.hyperskill.app.welcome_onboarding.root.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireType + +/** + * Represents a view analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/onboarding/questionnaire", + * "action": "view", + * "context": + * { + * "type": "how_did_you_hear_about_hyperskill" + * } + * } + * ``` + * + * @see HyperskillAnalyticEvent + */ +class WelcomeOnboardingUserQuestionnaireViewedHSAnalyticEvent( + questionnaireType: WelcomeQuestionnaireType +) : HyperskillAnalyticEvent( + HyperskillAnalyticRoute.Onboarding.UsersQuestionnaire, + HyperskillAnalyticAction.VIEW, + context = mapOf(TYPE_KEY to questionnaireType.name.lowercase()) +) { + companion object { + private const val TYPE_KEY = "type" + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/WelcomeOnboardingComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/WelcomeOnboardingComponent.kt new file mode 100644 index 0000000000..45990003cc --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/WelcomeOnboardingComponent.kt @@ -0,0 +1,10 @@ +package org.hyperskill.app.welcome_onboarding.root.injection + +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Action +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Message +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.State +import ru.nobird.app.presentation.redux.feature.Feature + +interface WelcomeOnboardingComponent { + val welcomeOnboardingFeature: Feature +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/WelcomeOnboardingComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/WelcomeOnboardingComponentImpl.kt new file mode 100644 index 0000000000..24ed5e4ed3 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/WelcomeOnboardingComponentImpl.kt @@ -0,0 +1,23 @@ +package org.hyperskill.app.welcome_onboarding.root.injection + +import org.hyperskill.app.core.injection.AppGraph +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingFeatureParams +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Action +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Message +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.State +import ru.nobird.app.presentation.redux.feature.Feature + +internal class WelcomeOnboardingComponentImpl( + private val appGraph: AppGraph, + private val params: WelcomeOnboardingFeatureParams +) : WelcomeOnboardingComponent { + override val welcomeOnboardingFeature: Feature + get() = WelcomeOnboardingFeatureBuilder.build( + analyticInteractor = appGraph.analyticComponent.analyticInteractor, + logger = appGraph.loggerComponent.logger, + buildVariant = appGraph.commonComponent.buildKonfig.buildVariant, + sentryInteractor = appGraph.sentryComponent.sentryInteractor, + learningActivityRepository = appGraph.buildLearningActivitiesDataComponent().learningActivitiesRepository, + params = params + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/WelcomeOnboardingFeatureBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/WelcomeOnboardingFeatureBuilder.kt new file mode 100644 index 0000000000..537081fb44 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/injection/WelcomeOnboardingFeatureBuilder.kt @@ -0,0 +1,53 @@ +package org.hyperskill.app.welcome_onboarding.root.injection + +import co.touchlab.kermit.Logger +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.learning_activities.domain.repository.LearningActivitiesRepository +import org.hyperskill.app.logging.presentation.wrapWithLogger +import org.hyperskill.app.sentry.domain.interactor.SentryInteractor +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingFeatureParams +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingActionDispatcher +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Action +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Message +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.State +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingReducer +import ru.nobird.app.presentation.redux.dispatcher.wrapWithActionDispatcher +import ru.nobird.app.presentation.redux.feature.Feature +import ru.nobird.app.presentation.redux.feature.ReduxFeature + +internal object WelcomeOnboardingFeatureBuilder { + private const val LOG_TAG = "WelcomeOnboardingFeature" + + fun build( + analyticInteractor: AnalyticInteractor, + logger: Logger, + buildVariant: BuildVariant, + sentryInteractor: SentryInteractor, + learningActivityRepository: LearningActivitiesRepository, + params: WelcomeOnboardingFeatureParams + ): Feature { + val welcomeOnboardingReducer = + WelcomeOnboardingReducer() + .wrapWithLogger(buildVariant, logger, LOG_TAG) + + val welcomeOnboardingActionDispatcher = WelcomeOnboardingActionDispatcher( + ActionDispatcherOptions(), + sentryInteractor = sentryInteractor, + learningActivityRepository = learningActivityRepository, + logger = logger.withTag(LOG_TAG) + ) + + return ReduxFeature( + initialState = WelcomeOnboardingFeature.initialState(params), + reducer = welcomeOnboardingReducer + ) + .wrapWithActionDispatcher(welcomeOnboardingActionDispatcher) + .wrapWithAnalyticLogger(analyticInteractor) { + (it as? WelcomeOnboardingFeature.InternalAction.LogAnalyticEvent)?.event + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/model/WelcomeOnboardingProgrammingLanguage.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/model/WelcomeOnboardingProgrammingLanguage.kt new file mode 100644 index 0000000000..508bf4227f --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/model/WelcomeOnboardingProgrammingLanguage.kt @@ -0,0 +1,10 @@ +package org.hyperskill.app.welcome_onboarding.root.model + +enum class WelcomeOnboardingProgrammingLanguage { + JAVA, + JAVA_SCRIPT, + KOTLIN, + PYTHON, + SQL, + UNDEFINED +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/model/WelcomeOnboardingStartScreen.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/model/WelcomeOnboardingStartScreen.kt new file mode 100644 index 0000000000..390f2298e3 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/model/WelcomeOnboardingStartScreen.kt @@ -0,0 +1,6 @@ +package org.hyperskill.app.welcome_onboarding.root.model + +enum class WelcomeOnboardingStartScreen { + START_SCREEN, + NOTIFICATION_ONBOARDING +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingActionDispatcher.kt new file mode 100644 index 0000000000..a3115e7e4d --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingActionDispatcher.kt @@ -0,0 +1,45 @@ +package org.hyperskill.app.welcome_onboarding.root.presentation + +import co.touchlab.kermit.Logger +import org.hyperskill.app.core.presentation.ActionDispatcherOptions +import org.hyperskill.app.learning_activities.domain.model.LearningActivityType +import org.hyperskill.app.learning_activities.domain.repository.LearningActivitiesRepository +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.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Action +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.InternalAction +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.InternalMessage +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Message +import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher + +internal class WelcomeOnboardingActionDispatcher( + config: ActionDispatcherOptions, + private val sentryInteractor: SentryInteractor, + private val learningActivityRepository: LearningActivitiesRepository, + private val logger: Logger +) : CoroutineActionDispatcher(config.createConfig()) { + override suspend fun doSuspendableAction(action: Action) { + when (action) { + InternalAction.FetchNextLearningActivity -> handleFetchNextLearningActivity(::onNewMessage) + else -> { + // no op + } + } + } + + private suspend fun handleFetchNextLearningActivity(onNewMessage: (Message) -> Unit) = + sentryInteractor.withTransaction( + HyperskillSentryTransactionBuilder.buildWelcomeOnboardingFeatureFetchNextLearningActivity(), + onError = { e -> + logger.e(e) { "Failed to fetch next learning activity" } + InternalMessage.FetchNextLearningActivityError + } + ) { + InternalMessage.FetchNextLearningActivitySuccess( + learningActivityRepository + .getNextLearningActivity(types = setOf(LearningActivityType.LEARN_TOPIC)) + .getOrThrow() + ) + }.let(onNewMessage) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingFeature.kt new file mode 100644 index 0000000000..3b419694bd --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingFeature.kt @@ -0,0 +1,93 @@ +package org.hyperskill.app.welcome_onboarding.root.presentation + +import org.hyperskill.app.analytic.domain.model.AnalyticEvent +import org.hyperskill.app.learning_activities.domain.model.LearningActivity +import org.hyperskill.app.profile.domain.model.Profile +import org.hyperskill.app.profile.domain.model.isNewUser +import org.hyperskill.app.step.domain.model.StepRoute +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingFeatureParams +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireItemType +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireType +import org.hyperskill.app.welcome_onboarding.root.model.WelcomeOnboardingProgrammingLanguage +import org.hyperskill.app.welcome_onboarding.root.model.WelcomeOnboardingStartScreen + +object WelcomeOnboardingFeature { + + data class State( + internal val initialStep: WelcomeOnboardingStartScreen, + internal val selectedTrack: WelcomeOnboardingTrack?, + internal val nextLearningActivityState: NextLearningActivityState, + val isNextLearningActivityLoadingShown: Boolean + ) + + sealed interface NextLearningActivityState { + object Idle : NextLearningActivityState + object Loading : NextLearningActivityState + object Error : NextLearningActivityState + data class Success(val nextLearningActivity: LearningActivity?) : NextLearningActivityState + } + + fun shouldBeLaunchedAfterAuthorization(profile: Profile, isNotificationPermissionGranted: Boolean): Boolean = + profile.isNewUser || !isNotificationPermissionGranted + + fun shouldBeLaunchedOnStartup(profile: Profile): Boolean = + profile.isNewUser + + internal fun initialState(params: WelcomeOnboardingFeatureParams) = + State( + initialStep = when { + params.profile.isNewUser -> WelcomeOnboardingStartScreen.START_SCREEN + !params.isNotificationPermissionGranted -> WelcomeOnboardingStartScreen.NOTIFICATION_ONBOARDING + else -> error("Welcome onboarding should not be shown") + }, + selectedTrack = null, + nextLearningActivityState = NextLearningActivityState.Idle, + isNextLearningActivityLoadingShown = false + ) + + sealed interface Message { + object Initialize : Message + object StartJourneyClicked : Message + data class QuestionnaireItemClicked( + val questionnaireType: WelcomeQuestionnaireType, + val itemType: WelcomeQuestionnaireItemType + ) : Message + data class ProgrammingLanguageSelected(val language: WelcomeOnboardingProgrammingLanguage) : Message + data class TrackSelected( + val selectedTrack: WelcomeOnboardingTrack, + val isNotificationPermissionGranted: Boolean + ) : Message + object NotificationPermissionOnboardingCompleted : Message + object FinishClicked : Message + + object StartOnboardingViewed : Message + class UserQuestionnaireViewed(val questionnaireType: WelcomeQuestionnaireType) : Message + object SelectProgrammingLanguageViewed : Message + object FinishOnboardingViewed : Message + } + + internal sealed interface InternalMessage : Message { + data class FetchNextLearningActivitySuccess(val nextLearningActivity: LearningActivity?) : InternalMessage + object FetchNextLearningActivityError : InternalMessage + } + + sealed interface Action { + sealed interface ViewAction : Action { + sealed interface NavigateTo : ViewAction { + object StartScreen : NavigateTo + data class WelcomeOnboardingQuestionnaire(val type: WelcomeQuestionnaireType) : NavigateTo + object ChooseProgrammingLanguage : NavigateTo + data class TrackDetails(val track: WelcomeOnboardingTrack) : NavigateTo + object NotificationOnboarding : NavigateTo + data class OnboardingFinish(val selectedTrack: WelcomeOnboardingTrack) : NavigateTo + } + data class CompleteWelcomeOnboarding(val stepRoute: StepRoute?) : ViewAction + } + } + + internal sealed interface InternalAction : Action { + data class LogAnalyticEvent(val event: AnalyticEvent) : InternalAction + object FetchNextLearningActivity : InternalAction + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingReducer.kt new file mode 100644 index 0000000000..7f0dfa2bfa --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/root/presentation/WelcomeOnboardingReducer.kt @@ -0,0 +1,215 @@ +package org.hyperskill.app.welcome_onboarding.root.presentation + +import org.hyperskill.app.first_problem_onboarding.domain.analytic.OnboardingCompletionAppsFlyerAnalyticEvent +import org.hyperskill.app.learning_activities.domain.model.LearningActivity +import org.hyperskill.app.learning_activities.domain.model.LearningActivityTargetType +import org.hyperskill.app.step.domain.model.StepRoute +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.questionnaire.model.WelcomeQuestionnaireType +import org.hyperskill.app.welcome_onboarding.root.domain.analytic.WelcomeOnboardingFinishScreenStartClickedHSAnalyticEvent +import org.hyperskill.app.welcome_onboarding.root.domain.analytic.WelcomeOnboardingFinishScreenViewedHSAnalyticEvent +import org.hyperskill.app.welcome_onboarding.root.domain.analytic.WelcomeOnboardingProgrammingLanguageClickedHSAnalyticEvent +import org.hyperskill.app.welcome_onboarding.root.domain.analytic.WelcomeOnboardingQuestionnaireItemClickedHSAnalyticEvent +import org.hyperskill.app.welcome_onboarding.root.domain.analytic.WelcomeOnboardingSelectProgrammingLanguageViewedHSAnalyticEvent +import org.hyperskill.app.welcome_onboarding.root.domain.analytic.WelcomeOnboardingStartJourneyClickedHSAnalyticEvent +import org.hyperskill.app.welcome_onboarding.root.domain.analytic.WelcomeOnboardingStartScreenViewedHSAnalyticEvent +import org.hyperskill.app.welcome_onboarding.root.domain.analytic.WelcomeOnboardingUserQuestionnaireViewedHSAnalyticEvent +import org.hyperskill.app.welcome_onboarding.root.model.WelcomeOnboardingProgrammingLanguage +import org.hyperskill.app.welcome_onboarding.root.model.WelcomeOnboardingStartScreen +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Action +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Action.ViewAction.NavigateTo +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.InternalAction +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.InternalMessage +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.Message +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.NextLearningActivityState +import org.hyperskill.app.welcome_onboarding.root.presentation.WelcomeOnboardingFeature.State +import ru.nobird.app.presentation.redux.reducer.StateReducer + +private typealias WelcomeOnboardingReducerResult = Pair> + +internal class WelcomeOnboardingReducer : StateReducer { + override fun reduce(state: State, message: Message): WelcomeOnboardingReducerResult = + when (message) { + Message.Initialize -> handleInitialize(state) + Message.StartJourneyClicked -> handleStartJourneyClicked(state) + is Message.QuestionnaireItemClicked -> handleQuestionnaireItemClicked(state, message) + is Message.ProgrammingLanguageSelected -> handleProgrammingLanguageSelected(state, message) + is Message.TrackSelected -> handleTrackSelected(state, message) + Message.NotificationPermissionOnboardingCompleted -> handleNotificationPermissionOnboardingCompleted(state) + Message.FinishClicked -> handleFinishClicked(state) + is InternalMessage.FetchNextLearningActivitySuccess -> + handleFetchNextLearningActivitySuccess(state, message) + InternalMessage.FetchNextLearningActivityError -> + handleFetchNextLearningActivityError(state) + + Message.StartOnboardingViewed -> handleStartOnboardingViewed(state) + is Message.UserQuestionnaireViewed -> handleUserQuestionnaireViewed(state, message) + Message.SelectProgrammingLanguageViewed -> handleSelectProgrammingLanguageViewed(state) + Message.FinishOnboardingViewed -> handleFinishOnboardingViewed(state) + } + + private fun handleInitialize(state: State): WelcomeOnboardingReducerResult = + when (state.initialStep) { + WelcomeOnboardingStartScreen.START_SCREEN -> state to setOf(NavigateTo.StartScreen) + WelcomeOnboardingStartScreen.NOTIFICATION_ONBOARDING -> + state.copy(nextLearningActivityState = NextLearningActivityState.Loading) to setOf( + NavigateTo.NotificationOnboarding, + InternalAction.FetchNextLearningActivity + ) + } + + private fun handleStartJourneyClicked(state: State): WelcomeOnboardingReducerResult = + state to setOf( + InternalAction.LogAnalyticEvent(WelcomeOnboardingStartJourneyClickedHSAnalyticEvent), + NavigateTo.WelcomeOnboardingQuestionnaire( + WelcomeQuestionnaireType.HOW_DID_YOU_HEAR_ABOUT_HYPERSKILL + ) + ) + + private fun handleQuestionnaireItemClicked( + state: State, + message: Message.QuestionnaireItemClicked + ): WelcomeOnboardingReducerResult = + state to setOf( + InternalAction.LogAnalyticEvent( + WelcomeOnboardingQuestionnaireItemClickedHSAnalyticEvent( + message.questionnaireType, + message.itemType + ) + ), + when (message.questionnaireType) { + WelcomeQuestionnaireType.HOW_DID_YOU_HEAR_ABOUT_HYPERSKILL -> + NavigateTo.WelcomeOnboardingQuestionnaire(WelcomeQuestionnaireType.LEARNING_REASON) + WelcomeQuestionnaireType.LEARNING_REASON -> + NavigateTo.WelcomeOnboardingQuestionnaire(WelcomeQuestionnaireType.CODING_EXPERIENCE) + WelcomeQuestionnaireType.CODING_EXPERIENCE -> + NavigateTo.ChooseProgrammingLanguage + } + ) + + private fun handleProgrammingLanguageSelected( + state: State, + message: Message.ProgrammingLanguageSelected + ): WelcomeOnboardingReducerResult = + state to setOf( + InternalAction.LogAnalyticEvent( + WelcomeOnboardingProgrammingLanguageClickedHSAnalyticEvent(message.language) + ), + NavigateTo.TrackDetails( + when (message.language) { + WelcomeOnboardingProgrammingLanguage.JAVA -> WelcomeOnboardingTrack.JAVA + WelcomeOnboardingProgrammingLanguage.JAVA_SCRIPT -> WelcomeOnboardingTrack.JAVA_SCRIPT + WelcomeOnboardingProgrammingLanguage.KOTLIN -> WelcomeOnboardingTrack.KOTLIN + WelcomeOnboardingProgrammingLanguage.SQL -> WelcomeOnboardingTrack.SQL + WelcomeOnboardingProgrammingLanguage.PYTHON, + WelcomeOnboardingProgrammingLanguage.UNDEFINED -> WelcomeOnboardingTrack.PYTHON + } + ) + ) + + private fun handleTrackSelected( + state: State, + message: Message.TrackSelected + ): WelcomeOnboardingReducerResult = + if (!message.isNotificationPermissionGranted) { + state.copy( + nextLearningActivityState = NextLearningActivityState.Loading, + selectedTrack = message.selectedTrack + ) to setOf( + NavigateTo.NotificationOnboarding, + InternalAction.FetchNextLearningActivity + ) + } else { + state.copy( + nextLearningActivityState = NextLearningActivityState.Loading, + isNextLearningActivityLoadingShown = true, + selectedTrack = message.selectedTrack + ) to setOf(InternalAction.FetchNextLearningActivity) + } + + private fun handleNotificationPermissionOnboardingCompleted(state: State): WelcomeOnboardingReducerResult = + if (state.selectedTrack != null) { + state to setOf(NavigateTo.OnboardingFinish(state.selectedTrack)) + } else { + finishOnboarding(state) + } + + private fun handleStartOnboardingViewed(state: State): WelcomeOnboardingReducerResult = + state to setOf( + InternalAction.LogAnalyticEvent(WelcomeOnboardingStartScreenViewedHSAnalyticEvent) + ) + + private fun handleUserQuestionnaireViewed( + state: State, + message: Message.UserQuestionnaireViewed + ): WelcomeOnboardingReducerResult = + state to setOf( + InternalAction.LogAnalyticEvent( + WelcomeOnboardingUserQuestionnaireViewedHSAnalyticEvent(message.questionnaireType) + ) + ) + + private fun handleSelectProgrammingLanguageViewed(state: State): WelcomeOnboardingReducerResult = + state to setOf( + InternalAction.LogAnalyticEvent(WelcomeOnboardingSelectProgrammingLanguageViewedHSAnalyticEvent) + ) + + private fun handleFinishOnboardingViewed(state: State): WelcomeOnboardingReducerResult = + state to setOf( + InternalAction.LogAnalyticEvent(WelcomeOnboardingFinishScreenViewedHSAnalyticEvent) + ) + + private fun handleFinishClicked(state: State): WelcomeOnboardingReducerResult { + val (newState, actions) = finishOnboarding(state) + return newState to setOf( + InternalAction.LogAnalyticEvent(OnboardingCompletionAppsFlyerAnalyticEvent), + InternalAction.LogAnalyticEvent(WelcomeOnboardingFinishScreenStartClickedHSAnalyticEvent) + ) + actions + } + + private fun finishOnboarding(state: State): WelcomeOnboardingReducerResult = + when (state.nextLearningActivityState) { + NextLearningActivityState.Idle -> state to setOf(InternalAction.FetchNextLearningActivity) + NextLearningActivityState.Loading -> { + // Wait for a next learning activity + state.copy(isNextLearningActivityLoadingShown = true) to emptySet() + } + NextLearningActivityState.Error -> completeWelcomeOnboarding(state, null) + is NextLearningActivityState.Success -> + completeWelcomeOnboarding(state, state.nextLearningActivityState.nextLearningActivity) + } + + private fun handleFetchNextLearningActivitySuccess( + state: State, + message: InternalMessage.FetchNextLearningActivitySuccess + ): WelcomeOnboardingReducerResult = + if (state.isNextLearningActivityLoadingShown) { + completeWelcomeOnboarding(state, message.nextLearningActivity) + } else { + state.copy( + nextLearningActivityState = NextLearningActivityState.Success(message.nextLearningActivity) + ) to emptySet() + } + + private fun handleFetchNextLearningActivityError(state: State): WelcomeOnboardingReducerResult = + if (state.isNextLearningActivityLoadingShown) { + completeWelcomeOnboarding(state, nextLearningActivity = null) + } else { + state.copy(nextLearningActivityState = NextLearningActivityState.Error) to emptySet() + } + + private fun completeWelcomeOnboarding( + state: State, + nextLearningActivity: LearningActivity? + ): WelcomeOnboardingReducerResult { + val stepRoute = if (nextLearningActivity?.targetType == LearningActivityTargetType.STEP) { + nextLearningActivity.targetId?.let { stepId -> + StepRoute.Learn.Step(stepId = stepId, topicId = nextLearningActivity.topicId) + } + } else { + null + } + return state.copy(isNextLearningActivityLoadingShown = false) to + setOf(Action.ViewAction.CompleteWelcomeOnboarding(stepRoute)) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/domain/analytic/WelcomeOnboardingSelectTrackClickedHSAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/domain/analytic/WelcomeOnboardingSelectTrackClickedHSAnalyticEvent.kt new file mode 100644 index 0000000000..6654992149 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/domain/analytic/WelcomeOnboardingSelectTrackClickedHSAnalyticEvent.kt @@ -0,0 +1,42 @@ +package org.hyperskill.app.welcome_onboarding.track_details.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticPart +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticTarget +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack + +/** + * Represents a select track button clicked analytic event. + * + * Click on the Track widget: + * ``` + * { + * "route": "/onboarding/track-select", + * "action": "click", + * "part": "main", + * "target": "track", + * "context": + * { + * "track": "python" + * } + * } + * ``` + * @see HyperskillAnalyticEvent + */ +class WelcomeOnboardingSelectTrackClickedHSAnalyticEvent( + track: WelcomeOnboardingTrack +) : HyperskillAnalyticEvent( + route = HyperskillAnalyticRoute.Onboarding.TrackSelection, + action = HyperskillAnalyticAction.CLICK, + part = HyperskillAnalyticPart.MAIN, + target = HyperskillAnalyticTarget.TRACK, + context = mapOf( + TRACK_KEY to track.name.lowercase() + ) +) { + companion object { + private const val TRACK_KEY = "track" + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/domain/analytic/WelcomeOnboardingSelectTrackViewedHSAnalyticEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/domain/analytic/WelcomeOnboardingSelectTrackViewedHSAnalyticEvent.kt new file mode 100644 index 0000000000..54690e065c --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/domain/analytic/WelcomeOnboardingSelectTrackViewedHSAnalyticEvent.kt @@ -0,0 +1,23 @@ +package org.hyperskill.app.welcome_onboarding.track_details.domain.analytic + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticAction +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute + +/** + * Represents a view analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/onboarding/track-select", + * "action": "view" + * } + * ``` + * + * @see HyperskillAnalyticEvent + */ +object WelcomeOnboardingSelectTrackViewedHSAnalyticEvent : HyperskillAnalyticEvent( + HyperskillAnalyticRoute.Onboarding.ProgrammingLanguageQuestionnaire, + HyperskillAnalyticAction.VIEW +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/WelcomeOnboardingTrackDetailsComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/WelcomeOnboardingTrackDetailsComponent.kt new file mode 100644 index 0000000000..945c7d888f --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/WelcomeOnboardingTrackDetailsComponent.kt @@ -0,0 +1,10 @@ +package org.hyperskill.app.welcome_onboarding.track_details.injection + +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Action +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Message +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.ViewState +import ru.nobird.app.presentation.redux.feature.Feature + +interface WelcomeOnboardingTrackDetailsComponent { + val welcomeOnboardingTrackDetails: Feature +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/WelcomeOnboardingTrackDetailsComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/WelcomeOnboardingTrackDetailsComponentImpl.kt new file mode 100644 index 0000000000..1675a9c606 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/WelcomeOnboardingTrackDetailsComponentImpl.kt @@ -0,0 +1,24 @@ +package org.hyperskill.app.welcome_onboarding.track_details.injection + +import org.hyperskill.app.core.injection.AppGraph +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Action +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Message +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.ViewState +import ru.nobird.app.presentation.redux.feature.Feature + +internal class WelcomeOnboardingTrackDetailsComponentImpl( + private val track: WelcomeOnboardingTrack, + private val appGraph: AppGraph +) : WelcomeOnboardingTrackDetailsComponent { + override val welcomeOnboardingTrackDetails: Feature + get() = WelcomeOnboardingTrackDetailsFeatureBuilder.build( + track = track, + currentProfileStateRepository = appGraph.profileDataComponent.currentProfileStateRepository, + profileRepository = appGraph.profileDataComponent.profileRepository, + analyticInteractor = appGraph.analyticComponent.analyticInteractor, + logger = appGraph.loggerComponent.logger, + buildVariant = appGraph.commonComponent.buildKonfig.buildVariant, + resourceProvider = appGraph.commonComponent.resourceProvider + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/WelcomeOnboardingTrackDetailsFeatureBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/WelcomeOnboardingTrackDetailsFeatureBuilder.kt new file mode 100644 index 0000000000..0ec5712f3f --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/injection/WelcomeOnboardingTrackDetailsFeatureBuilder.kt @@ -0,0 +1,61 @@ +package org.hyperskill.app.welcome_onboarding.track_details.injection + +import co.touchlab.kermit.Logger +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.core.presentation.transformState +import org.hyperskill.app.core.view.mapper.ResourceProvider +import org.hyperskill.app.logging.presentation.wrapWithLogger +import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository +import org.hyperskill.app.profile.domain.repository.ProfileRepository +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsActionDispatcher +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Action +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.InternalAction +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Message +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.ViewState +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsReducer +import org.hyperskill.app.welcome_onboarding.track_details.view.WelcomeOnboardingTrackDetailsViewStateMapper +import ru.nobird.app.presentation.redux.dispatcher.wrapWithActionDispatcher +import ru.nobird.app.presentation.redux.feature.Feature +import ru.nobird.app.presentation.redux.feature.ReduxFeature + +internal object WelcomeOnboardingTrackDetailsFeatureBuilder { + private const val LOG_TAG = "WelcomeOnboardingTrackDetailsFeature" + + fun build( + track: WelcomeOnboardingTrack, + currentProfileStateRepository: CurrentProfileStateRepository, + profileRepository: ProfileRepository, + analyticInteractor: AnalyticInteractor, + resourceProvider: ResourceProvider, + logger: Logger, + buildVariant: BuildVariant + ): Feature { + val welcomeOnboardingTrackDetailsReducer = + WelcomeOnboardingTrackDetailsReducer() + .wrapWithLogger(buildVariant, logger, LOG_TAG) + + val welcomeOnboardingTrackDetailsActionDispatcher = WelcomeOnboardingTrackDetailsActionDispatcher( + ActionDispatcherOptions(), + profileRepository = profileRepository, + currentProfileStateRepository = currentProfileStateRepository, + logger = logger.withTag(LOG_TAG) + ) + + val viewStateMapper = WelcomeOnboardingTrackDetailsViewStateMapper(resourceProvider) + + return ReduxFeature( + initialState = WelcomeOnboardingTrackDetailsFeature.initialState(track), + reducer = welcomeOnboardingTrackDetailsReducer + ) + .wrapWithActionDispatcher(welcomeOnboardingTrackDetailsActionDispatcher) + .transformState(viewStateMapper::map) + .wrapWithAnalyticLogger(analyticInteractor) { + (it as? InternalAction.LogAnalyticEvent)?.event + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsActionDispatcher.kt new file mode 100644 index 0000000000..858ab79e68 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsActionDispatcher.kt @@ -0,0 +1,51 @@ +package org.hyperskill.app.welcome_onboarding.track_details.presentation + +import co.touchlab.kermit.Logger +import org.hyperskill.app.core.presentation.ActionDispatcherOptions +import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository +import org.hyperskill.app.profile.domain.repository.ProfileRepository +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Action +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.InternalAction +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.InternalMessage +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Message +import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher + +internal class WelcomeOnboardingTrackDetailsActionDispatcher( + config: ActionDispatcherOptions, + private val profileRepository: ProfileRepository, + private val currentProfileStateRepository: CurrentProfileStateRepository, + private val logger: Logger +) : CoroutineActionDispatcher(config.createConfig()) { + override suspend fun doSuspendableAction(action: Action) { + when (action) { + is InternalAction.SelectTrack -> handleSelectTrack(action, ::onNewMessage) + else -> { + // no op + } + } + } + + private suspend fun handleSelectTrack( + action: InternalAction.SelectTrack, + onNewMessage: (InternalMessage) -> Unit + ) { + selectTrack(action.trackId) + .fold( + onSuccess = { + InternalMessage.SelectTrackSuccess + }, + onFailure = { e -> + logger.e(e) { "Failed to select track with id=${action.trackId}" } + InternalMessage.SelectTrackFailed + } + ) + .let(onNewMessage) + } + + private suspend fun selectTrack(trackId: Long): Result = + runCatching { + val currentProfile = currentProfileStateRepository.getState().getOrThrow() + val newProfile = profileRepository.selectTrack(currentProfile.id, trackId).getOrThrow() + currentProfileStateRepository.updateState(newProfile) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsFeature.kt new file mode 100644 index 0000000000..6b58a3e510 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsFeature.kt @@ -0,0 +1,52 @@ +package org.hyperskill.app.welcome_onboarding.track_details.presentation + +import org.hyperskill.app.analytic.domain.model.AnalyticEvent +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack + +object WelcomeOnboardingTrackDetailsFeature { + internal data class State( + val track: WelcomeOnboardingTrack, + val isLoadingShowed: Boolean + ) + + internal fun initialState(track: WelcomeOnboardingTrack) = + State( + track = track, + isLoadingShowed = false + ) + + /** + * @param trackDescriptionHtml represents an html string that contains a `` tag. + */ + data class ViewState( + val track: WelcomeOnboardingTrack, + val title: String, + val trackTitle: String, + val trackDescriptionHtml: String, + val changeText: String, + val buttonText: String, + val isLoadingShowed: Boolean + ) + + sealed interface Message { + object ViewedEventMessage : Message + object ContinueClicked : Message + } + + internal sealed interface InternalMessage : Message { + object SelectTrackSuccess : InternalMessage + object SelectTrackFailed : InternalMessage + } + + sealed interface Action { + sealed interface ViewAction : Action { + object NotifyTrackSelected : ViewAction + object ShowTrackSelectionError : ViewAction + } + } + + internal sealed interface InternalAction : Action { + data class LogAnalyticEvent(val event: AnalyticEvent) : InternalAction + data class SelectTrack(val trackId: Long) : InternalAction + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsReducer.kt new file mode 100644 index 0000000000..9355802a5c --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/presentation/WelcomeOnboardingTrackDetailsReducer.kt @@ -0,0 +1,61 @@ +package org.hyperskill.app.welcome_onboarding.track_details.presentation + +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.track_details.domain.analytic.WelcomeOnboardingSelectTrackClickedHSAnalyticEvent +import org.hyperskill.app.welcome_onboarding.track_details.domain.analytic.WelcomeOnboardingSelectTrackViewedHSAnalyticEvent +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Action +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.InternalAction +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.InternalMessage.SelectTrackFailed +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.InternalMessage.SelectTrackSuccess +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.Message +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.State +import ru.nobird.app.presentation.redux.reducer.StateReducer + +private typealias WelcomeOnboardingTrackDetailsReducerResult = Pair> + +internal class WelcomeOnboardingTrackDetailsReducer : StateReducer { + + companion object { + private const val JAVA_TRACK_ID = 8L + private const val JS_TRACK_ID = 32L + private const val KOTLIN_TRACK_ID = 69L + private const val PYTHON_TRACK_ID = 6L + private const val SQL_TRACK_ID = 31L + } + + override fun reduce(state: State, message: Message): WelcomeOnboardingTrackDetailsReducerResult = + when (message) { + Message.ViewedEventMessage -> handleSelectTrackViewed(state) + Message.ContinueClicked -> handleContinueClicked(state) + SelectTrackSuccess -> handleSelectTrackSuccess(state) + SelectTrackFailed -> handleSelectTrackFailed(state) + } + + private fun handleSelectTrackViewed(state: State): WelcomeOnboardingTrackDetailsReducerResult = + state to setOf( + InternalAction.LogAnalyticEvent(WelcomeOnboardingSelectTrackViewedHSAnalyticEvent) + ) + + private fun handleContinueClicked(state: State): WelcomeOnboardingTrackDetailsReducerResult = + state.copy(isLoadingShowed = true) to setOf( + InternalAction.LogAnalyticEvent( + WelcomeOnboardingSelectTrackClickedHSAnalyticEvent(state.track) + ), + InternalAction.SelectTrack(getTrackId(state.track)) + ) + + private fun handleSelectTrackSuccess(state: State): WelcomeOnboardingTrackDetailsReducerResult = + state to setOf(Action.ViewAction.NotifyTrackSelected) + + private fun handleSelectTrackFailed(state: State): WelcomeOnboardingTrackDetailsReducerResult = + state.copy(isLoadingShowed = false) to setOf(Action.ViewAction.ShowTrackSelectionError) + + private fun getTrackId(track: WelcomeOnboardingTrack): Long = + when (track) { + WelcomeOnboardingTrack.JAVA -> JAVA_TRACK_ID + WelcomeOnboardingTrack.JAVA_SCRIPT -> JS_TRACK_ID + WelcomeOnboardingTrack.KOTLIN -> KOTLIN_TRACK_ID + WelcomeOnboardingTrack.PYTHON -> PYTHON_TRACK_ID + WelcomeOnboardingTrack.SQL -> SQL_TRACK_ID + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/view/WelcomeOnboardingTrackDetailsTitle.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/view/WelcomeOnboardingTrackDetailsTitle.kt new file mode 100644 index 0000000000..00d61e3c04 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/view/WelcomeOnboardingTrackDetailsTitle.kt @@ -0,0 +1,14 @@ +package org.hyperskill.app.welcome_onboarding.track_details.view + +import dev.icerock.moko.resources.StringResource +import org.hyperskill.app.SharedResources +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack + +internal val WelcomeOnboardingTrack.titleStringResource: StringResource + get() = when (this) { + WelcomeOnboardingTrack.JAVA -> SharedResources.strings.welcome_onboarding_track_details_java_title + WelcomeOnboardingTrack.JAVA_SCRIPT -> SharedResources.strings.welcome_onboarding_track_details_js_title + WelcomeOnboardingTrack.KOTLIN -> SharedResources.strings.welcome_onboarding_track_details_kotlin_title + WelcomeOnboardingTrack.PYTHON -> SharedResources.strings.welcome_onboarding_track_details_python_title + WelcomeOnboardingTrack.SQL -> SharedResources.strings.welcome_onboarding_track_details_sql_title + } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/view/WelcomeOnboardingTrackDetailsViewStateMapper.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/view/WelcomeOnboardingTrackDetailsViewStateMapper.kt new file mode 100644 index 0000000000..18227a50a4 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/welcome_onboarding/track_details/view/WelcomeOnboardingTrackDetailsViewStateMapper.kt @@ -0,0 +1,42 @@ +package org.hyperskill.app.welcome_onboarding.track_details.view + +import dev.icerock.moko.resources.StringResource +import org.hyperskill.app.SharedResources +import org.hyperskill.app.core.view.mapper.ResourceProvider +import org.hyperskill.app.welcome_onboarding.model.WelcomeOnboardingTrack +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.State +import org.hyperskill.app.welcome_onboarding.track_details.presentation.WelcomeOnboardingTrackDetailsFeature.ViewState + +internal class WelcomeOnboardingTrackDetailsViewStateMapper( + private val resourceProvider: ResourceProvider +) { + + fun map(state: State): ViewState = + ViewState( + track = state.track, + title = resourceProvider.getString(SharedResources.strings.welcome_onboarding_track_details_title), + trackTitle = resourceProvider.getString(state.track.titleStringResource), + trackDescriptionHtml = resourceProvider.getString(getTrackDescriptionRes(state.track)), + changeText = resourceProvider.getString( + SharedResources.strings.welcome_onboarding_track_details_change_text + ), + buttonText = resourceProvider.getString( + SharedResources.strings.welcome_onboarding_track_details_continue_btn + ), + isLoadingShowed = state.isLoadingShowed + ) + + private fun getTrackDescriptionRes(track: WelcomeOnboardingTrack): StringResource = + when (track) { + WelcomeOnboardingTrack.JAVA -> + SharedResources.strings.welcome_onboarding_track_details_java_description + WelcomeOnboardingTrack.JAVA_SCRIPT -> + SharedResources.strings.welcome_onboarding_track_details_js_description + WelcomeOnboardingTrack.KOTLIN -> + SharedResources.strings.welcome_onboarding_track_details_kotlin_description + WelcomeOnboardingTrack.PYTHON -> + SharedResources.strings.welcome_onboarding_track_details_python_description + WelcomeOnboardingTrack.SQL -> + SharedResources.strings.welcome_onboarding_track_details_sql_description + } +} \ No newline at end of file diff --git a/shared/src/commonMain/resources/MR/base/strings.xml b/shared/src/commonMain/resources/MR/base/strings.xml index 85e5c7145d..3210116a5c 100644 --- a/shared/src/commonMain/resources/MR/base/strings.xml +++ b/shared/src/commonMain/resources/MR/base/strings.xml @@ -750,4 +750,70 @@ Report Skip Open in Web + + + Welcome + We\'re here to help you build the ideal study plan! A few quick questions will guide us. + Start my journey + + + How did you hear about Hyperskill? + TikTok + Google Search + News/article/blog + Play Store + App Store + Facebook/Instagram + Friends/family + YouTube + Other + + + Why are you learning to code? + Start a programming-related career + Get better at my current job + Change technology stack + Complement my studies + Just for fun + For a different reason + + + What\'s your coding background? + I have no coding experience + I have some basic understanding of programming concepts. + I have written some projects + I am working professional + + + Pick a language to learn! + Java + JavaScript + Kotlin + Python + SQL + I\'m not sure yet + + + Here\'s the track for you! + You can always change this later + Continue + + Java Developer + backend systems and Android apps. Master the language that powers millions of devices worldwide!]]> + + Frontend Developer + websites to large-scale web applications. The ultimate language for web development]]> + + Python Developer + data science and machine learning. Start your journey with this easy-to-learn language and unlock a world of possibilities!]]> + + Kotlin Developer + Android and server-side applications!]]> + + SQL + Your pathway to becoming a data analyst. Gain the skills to query and analyze data effectively in any industry! + + You\'re all set! + Embark on your journey in %s right now! + Start my journey diff --git a/shared/src/commonTest/kotlin/org/hyperskill/main/AppFeatureTest.kt b/shared/src/commonTest/kotlin/org/hyperskill/main/AppFeatureTest.kt index 052d86d841..fe2bf6bbed 100644 --- a/shared/src/commonTest/kotlin/org/hyperskill/main/AppFeatureTest.kt +++ b/shared/src/commonTest/kotlin/org/hyperskill/main/AppFeatureTest.kt @@ -17,7 +17,6 @@ import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryFeature import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryReducer import org.hyperskill.app.subscriptions.domain.model.Subscription import org.hyperskill.app.subscriptions.domain.model.SubscriptionType -import org.hyperskill.app.welcome_onboarding.presentation.WelcomeOnboardingReducer import org.hyperskill.profile.stub import org.hyperskill.subscriptions.stub @@ -25,7 +24,6 @@ class AppFeatureTest { private val appReducer = AppReducer( streakRecoveryReducer = StreakRecoveryReducer(resourceProvider = ResourceProviderStub()), notificationClickHandlingReducer = NotificationClickHandlingReducer(), - welcomeOnboardingReducer = WelcomeOnboardingReducer() ) @Test