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