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 3e94e318d4..d540189691 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 @@ -1,6 +1,8 @@ package org.hyperskill.app.android.main.view.ui.activity +import android.annotation.SuppressLint import android.content.Intent +import android.content.pm.PackageManager import android.os.Bundle import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity @@ -87,6 +89,7 @@ class MainActivity : ) } + @SuppressLint("InlinedApi") override fun onCreate(savedInstanceState: Bundle?) { val splashScreen = installSplashScreen() @@ -116,15 +119,7 @@ class MainActivity : startupViewModel(intent) - lifecycleScope.launch { - router - .observeResult(AuthFragment.AUTH_SUCCESS) - .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) - .collectLatest { - val profile = (it as? Profile) ?: return@collectLatest - mainViewModel.onNewMessage(AppFeature.Message.UserAuthorized(profile)) - } - } + observeAuthFlowSuccess() AppCompatDelegate.setDefaultNightMode(ThemeMapper.getAppCompatDelegate(profileSettings.theme)) @@ -158,6 +153,27 @@ class MainActivity : } } + @SuppressLint("InlinedApi") + private fun observeAuthFlowSuccess() { + lifecycleScope.launch { + router + .observeResult(AuthFragment.AUTH_SUCCESS) + .flowWithLifecycle(lifecycle, Lifecycle.State.STARTED) + .collectLatest { + val profile = (it as? Profile) ?: return@collectLatest + mainViewModel.onNewMessage( + AppFeature.Message.UserAuthorized( + profile = profile, + isNotificationPermissionGranted = ContextCompat.checkSelfPermission( + this@MainActivity, + android.Manifest.permission.POST_NOTIFICATIONS + ) == PackageManager.PERMISSION_GRANTED + ) + ) + } + } + } + override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) if (intent != null) { @@ -195,6 +211,8 @@ class MainActivity : TrackSelectionListParams(isNewUserMode = true) ) ) + is AppFeature.Action.ViewAction.NavigateTo.NotificationOnBoardingScreen -> + TODO("Screen is going to be implemented in ALTAPPS-970") is AppFeature.Action.ViewAction.StreakRecoveryViewAction -> StreakRecoveryViewActionDelegate.handleViewAction( fragmentManager = supportFragmentManager, diff --git a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppViewModel.swift b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppViewModel.swift index 36300562b5..b05c153a60 100644 --- a/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppViewModel.swift +++ b/iosHyperskillApp/iosHyperskillApp/Sources/Modules/App/AppViewModel.swift @@ -92,7 +92,13 @@ final class AppViewModel: FeatureViewModel +) : ReduxViewModel(reduxViewContainer) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticAction.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticAction.kt index e1d4cb181b..4eedd84945 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticAction.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticAction.kt @@ -4,6 +4,5 @@ enum class HyperskillAnalyticAction(val actionName: String) { CLICK("click"), VIEW("view"), HIDDEN("hidden"), - SHOWN("shown"), - ORIENTATION_CHANGED("screen_orientation_changed") + SHOWN("shown") } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt index dc8cddf4d9..3ec1b24d2d 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/analytic/domain/model/hyperskill/HyperskillAnalyticPart.kt @@ -32,6 +32,5 @@ enum class HyperskillAnalyticPart(val partName: String) { STREAK_RECOVERY_MODAL("streak_recovery_modal"), STAGE_COMPLETED_MODAL("stage_completed_modal"), PROJECT_COMPLETED_MODAL("project_completed_modal"), - NEXT_LEARNING_ACTIVITY_WIDGET("next_learning_activity_widget"), - FULL_SCREEN_CODE_EDITOR("full_screen_code_editor") + NEXT_LEARNING_ACTIVITY_WIDGET("next_learning_activity_widget") } \ 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 41b9e2ef49..54cd5a557f 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 @@ -3,8 +3,13 @@ package org.hyperskill.app.analytic.domain.model.hyperskill sealed class HyperskillAnalyticRoute { abstract val path: String - class Onboarding : HyperskillAnalyticRoute() { + open class Onboarding : HyperskillAnalyticRoute() { override val path: String = "/onboarding" + + object Notifications : Onboarding() { + override val path: String + get() = "${super.path}/notifications" + } } 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 65ff80d19b..79010f1d09 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 @@ -89,5 +89,7 @@ enum class HyperskillAnalyticTarget(val targetName: String) { BADGES_VISIBILITY_BUTTON("badges_visibility_button"), BADGE_CARD("badges_card"), BADGE_MODAL("badge_modal"), - EARNED_BADGE_MODAL("earned_badge_modal") + EARNED_BADGE_MODAL("earned_badge_modal"), + ALLOW_NOTIFICATIONS("allow_notifications"), + REMIND_ME_LATER("remind_me_later") } \ 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 6ffe3941fa..b0e580b917 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 @@ -26,6 +26,7 @@ import org.hyperskill.app.notification.local.injection.NotificationComponent import org.hyperskill.app.notification.local.injection.NotificationFlowDataComponent import org.hyperskill.app.notification.remote.injection.PlatformPushNotificationsDataComponent import org.hyperskill.app.notification.remote.injection.PushNotificationsComponent +import org.hyperskill.app.notifications_onboarding.injection.NotificationsOnboardingComponent import org.hyperskill.app.onboarding.injection.OnboardingComponent import org.hyperskill.app.problems_limit.domain.model.ProblemsLimitScreen import org.hyperskill.app.problems_limit.injection.ProblemsLimitComponent @@ -144,4 +145,5 @@ interface AppGraph { fun buildProgressScreenComponent(): ProgressScreenComponent fun buildNextLearningActivityWidgetComponent(): NextLearningActivityWidgetComponent fun buildBadgesDataComponent(): BadgesDataComponent + fun buildNotificationsOnboardingComponent(): NotificationsOnboardingComponent } \ No newline at end of file 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 f66b5d240b..38efb02388 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 @@ -49,6 +49,8 @@ import org.hyperskill.app.notification.local.injection.NotificationFlowDataCompo import org.hyperskill.app.notification.local.injection.NotificationFlowDataComponentImpl import org.hyperskill.app.notification.remote.injection.PushNotificationsComponent import org.hyperskill.app.notification.remote.injection.PushNotificationsComponentImpl +import org.hyperskill.app.notifications_onboarding.injection.NotificationsOnboardingComponent +import org.hyperskill.app.notifications_onboarding.injection.NotificationsOnboardingComponentImpl import org.hyperskill.app.onboarding.injection.OnboardingComponent import org.hyperskill.app.onboarding.injection.OnboardingComponentImpl import org.hyperskill.app.problems_limit.domain.model.ProblemsLimitScreen @@ -409,4 +411,7 @@ abstract class BaseAppGraph : AppGraph { override fun buildBadgesDataComponent(): BadgesDataComponent = BadgesDataComponentImpl(this) + + override fun buildNotificationsOnboardingComponent(): NotificationsOnboardingComponent = + NotificationsOnboardingComponentImpl(this) } \ No newline at end of file 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 402dc4c3df..faea24ba7f 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 @@ -14,6 +14,7 @@ import org.hyperskill.app.notification.click_handling.presentation.NotificationC 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.onboarding.domain.interactor.OnboardingInteractor import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository import org.hyperskill.app.sentry.domain.interactor.SentryInteractor import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryActionDispatcher @@ -38,6 +39,7 @@ object AppFeatureBuilder { notificationClickHandlingDispatcher: NotificationClickHandlingDispatcher, notificationsInteractor: NotificationInteractor, pushNotificationsInteractor: PushNotificationsInteractor, + onboardingInteractor: OnboardingInteractor, platform: Platform ): Feature { val appReducer = AppReducer( @@ -53,7 +55,8 @@ object AppFeatureBuilder { sentryInteractor, stateRepositoriesComponent, notificationsInteractor, - pushNotificationsInteractor + pushNotificationsInteractor, + onboardingInteractor ) return ReduxFeature(initialState ?: State.Idle, appReducer) diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/MainComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main/injection/MainComponentImpl.kt index ba6b1c8bdc..abc54691b3 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 @@ -29,7 +29,8 @@ class MainComponentImpl(private val appGraph: AppGraph) : MainComponent { clickedNotificationComponent.notificationClickHandlingDispatcher, appGraph.buildNotificationComponent().notificationInteractor, appGraph.buildPushNotificationsComponent().pushNotificationsInteractor, - appGraph.commonComponent.platform + appGraph.buildOnboardingComponent().onboardingInteractor, + appGraph.commonComponent.platform, ) override fun appFeature(): Feature = diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppActionDispatcher.kt index 94af3dce5c..f239b16e12 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/main/presentation/AppActionDispatcher.kt @@ -14,6 +14,7 @@ import org.hyperskill.app.main.presentation.AppFeature.Action import org.hyperskill.app.main.presentation.AppFeature.Message import org.hyperskill.app.notification.local.domain.interactor.NotificationInteractor import org.hyperskill.app.notification.remote.domain.interactor.PushNotificationsInteractor +import org.hyperskill.app.onboarding.domain.interactor.OnboardingInteractor import org.hyperskill.app.profile.domain.model.Profile import org.hyperskill.app.profile.domain.model.isNewUser import org.hyperskill.app.profile.domain.repository.CurrentProfileStateRepository @@ -30,7 +31,8 @@ class AppActionDispatcher( private val sentryInteractor: SentryInteractor, private val stateRepositoriesComponent: StateRepositoriesComponent, private val notificationsInteractor: NotificationInteractor, - private val pushNotificationsInteractor: PushNotificationsInteractor + private val pushNotificationsInteractor: PushNotificationsInteractor, + private val onboardingInteractor: OnboardingInteractor ) : CoroutineActionDispatcher(config.createConfig()) { init { authInteractor @@ -105,6 +107,13 @@ class AppActionDispatcher( } ) } + is Action.FetchNotificationOnboardingData -> { + onNewMessage( + Message.NotificationOnboardingDataFetched( + wasNotificationOnBoardingShown = onboardingInteractor.wasNotificationOnboardingShown() + ) + ) + } is Action.IdentifyUserInSentry -> sentryInteractor.setUsedId(action.userId) is Action.ClearUserInSentry -> 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 9185337ab6..ac722e69f0 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 @@ -19,8 +19,17 @@ interface AppFeature { @Serializable object NetworkError : State + /** + * [profile] is used to temporarily store profile + * while handling [Action.ViewAction.NavigateTo.NotificationOnBoardingScreen]. + * + * @see [AppReducer] for [Message.UserAuthorized] & [Message.NotificationOnboardingCompleted] handling. + */ @Serializable - data class Ready(val isAuthorized: Boolean) : State + data class Ready( + val isAuthorized: Boolean, + internal val profile: Profile? = null + ) : State } sealed interface Message { @@ -35,8 +44,15 @@ interface AppFeature { ) : Message object UserAccountStatusError : Message - data class UserAuthorized(val profile: Profile) : Message + data class UserAuthorized( + val profile: Profile, + val isNotificationPermissionGranted: Boolean + ) : Message data class UserDeauthorized(val reason: Reason) : Message + + data class NotificationOnboardingDataFetched(val wasNotificationOnBoardingShown: Boolean) : Message + object NotificationOnboardingCompleted : Message + object OpenAuthScreen : Message object OpenNewUserScreen : Message @@ -63,6 +79,8 @@ interface AppFeature { object SendPushNotificationsToken : Action + object FetchNotificationOnboardingData : Action + /** * Action Wrappers */ @@ -84,6 +102,8 @@ interface AppFeature { data class AuthScreen(val isInSignUpMode: Boolean = false) : NavigateTo object TrackSelectionScreen : NavigateTo object OnboardingScreen : NavigateTo + + object NotificationOnBoardingScreen : NavigateTo } /** 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 a959204800..49f323b8f5 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 @@ -7,6 +7,7 @@ import org.hyperskill.app.main.presentation.AppFeature.Message import org.hyperskill.app.main.presentation.AppFeature.State import org.hyperskill.app.notification.click_handling.presentation.NotificationClickHandlingFeature import org.hyperskill.app.notification.click_handling.presentation.NotificationClickHandlingReducer +import org.hyperskill.app.profile.domain.model.Profile import org.hyperskill.app.profile.domain.model.isNewUser import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryFeature import org.hyperskill.app.streak_recovery.presentation.StreakRecoveryReducer @@ -40,18 +41,7 @@ class AppReducer( null } is Message.UserAuthorized -> - if (state is State.Ready && !state.isAuthorized) { - val navigateToViewAction = if (message.profile.isNewUser) { - Action.ViewAction.NavigateTo.TrackSelectionScreen - } else { - Action.ViewAction.NavigateTo.HomeScreen - } - - State.Ready(isAuthorized = true) to - getOnAuthActions(profileId = message.profile.id) + navigateToViewAction - } else { - null - } + handleUserAuthorized(state, message) is Message.UserDeauthorized -> if (state is State.Ready && state.isAuthorized) { val navigateToViewAction = when (message.reason) { @@ -65,6 +55,10 @@ class AppReducer( } else { null } + is Message.NotificationOnboardingDataFetched -> + handleNotificationOnboardingDataFetched(state, message) + is Message.NotificationOnboardingCompleted -> + handleNotificationOnboardingCompleted(state) is Message.OpenAuthScreen -> state to setOf(Action.ViewAction.NavigateTo.AuthScreen()) is Message.OpenNewUserScreen -> @@ -125,11 +119,62 @@ class AppReducer( } } - State.Ready(isAuthorized) to actions + State.Ready(isAuthorized = isAuthorized) to actions + } else { + state to emptySet() + } + + private fun handleUserAuthorized( + state: State, + message: Message.UserAuthorized + ): ReducerResult = + if (state is State.Ready && !state.isAuthorized) { + val navigationLogicAction = if (message.isNotificationPermissionGranted) { + getAuthorizedUserNavigationAction(message.profile) + } else { + Action.FetchNotificationOnboardingData + } + State.Ready( + isAuthorized = true, + profile = if (!message.isNotificationPermissionGranted) message.profile else null + ) to getAuthorizedUserActions(message.profile) + navigationLogicAction + } else { + state to emptySet() + } + + private fun handleNotificationOnboardingDataFetched( + state: State, + message: Message.NotificationOnboardingDataFetched + ): ReducerResult = + if (state is State.Ready && state.profile != null) { + if (!message.wasNotificationOnBoardingShown) { + state to setOf(Action.ViewAction.NavigateTo.NotificationOnBoardingScreen) + } else { + navigateUserAfterNotificationOnboarding(state.profile) + } } else { state to emptySet() } + private fun handleNotificationOnboardingCompleted( + state: State + ): ReducerResult = + if (state is State.Ready && state.profile != null) { + navigateUserAfterNotificationOnboarding(state.profile) + } else { + state to emptySet() + } + + private fun navigateUserAfterNotificationOnboarding(profile: Profile): ReducerResult = + State.Ready(isAuthorized = true) to setOf(getAuthorizedUserNavigationAction(profile)) + + private fun getAuthorizedUserNavigationAction(profile: Profile): Action = + if (profile.isNewUser) { + Action.ViewAction.NavigateTo.TrackSelectionScreen + } else { + Action.ViewAction.NavigateTo.HomeScreen + } + private fun reduceStreakRecoveryMessage( message: StreakRecoveryFeature.Message ): Set { @@ -176,15 +221,6 @@ class AppReducer( }.toSet() } - private fun getOnAuthActions( - profileId: Long - ): Set = - setOf( - Action.IdentifyUserInSentry(userId = profileId), - Action.UpdateDailyLearningNotificationTime, - Action.SendPushNotificationsToken - ) - private fun getOnAuthorizedAppStartUpActions( profileId: Long, platformType: PlatformType @@ -203,4 +239,11 @@ class AppReducer( private fun getNotAuthorizedAppStartUpActions(): Set = setOf(Action.ClearUserInSentry) + + private fun getAuthorizedUserActions(profile: Profile): Set = + setOf( + Action.IdentifyUserInSentry(userId = profile.id), + Action.UpdateDailyLearningNotificationTime, + Action.SendPushNotificationsToken + ) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notification/local/domain/interactor/NotificationInteractor.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notification/local/domain/interactor/NotificationInteractor.kt index c3a1e52562..e5be3dc5ec 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/notification/local/domain/interactor/NotificationInteractor.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notification/local/domain/interactor/NotificationInteractor.kt @@ -78,8 +78,10 @@ class NotificationInteractor( return isTwoDaysPassed && isNotReachedMaxUserAskedCount } - fun setLastTimeUserAskedToEnableDailyReminders(timestamp: Long) { - notificationRepository.setLastTimeUserAskedToEnableDailyReminders(timestamp) + fun updateLastTimeUserAskedToEnableDailyReminders() { + notificationRepository.setLastTimeUserAskedToEnableDailyReminders( + Clock.System.now().toEpochMilliseconds() + ) } private fun getUserAskedToEnableDailyRemindersCount(): Long = diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/domain/analytics/NotificationsOnboardingClickedAllowNotificationsHyperskillAnalyticsEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/domain/analytics/NotificationsOnboardingClickedAllowNotificationsHyperskillAnalyticsEvent.kt new file mode 100644 index 0000000000..df01feff09 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/domain/analytics/NotificationsOnboardingClickedAllowNotificationsHyperskillAnalyticsEvent.kt @@ -0,0 +1,28 @@ +package org.hyperskill.app.notifications_onboarding.domain.analytics + +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 "Allow notifications" button analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/onboarding/notifications", + * "action": "click", + * "part": "main", + * "target": "allow_notifications_button" + * } + * ``` + * @see HyperskillAnalyticEvent + */ +object NotificationsOnboardingClickedAllowNotificationsHyperskillAnalyticsEvent : HyperskillAnalyticEvent( + route = HyperskillAnalyticRoute.Onboarding.Notifications, + action = HyperskillAnalyticAction.CLICK, + part = HyperskillAnalyticPart.MAIN, + target = HyperskillAnalyticTarget.ALLOW_NOTIFICATIONS +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/domain/analytics/NotificationsOnboardingClickedRemindMeLaterHyperskillAnalyticsEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/domain/analytics/NotificationsOnboardingClickedRemindMeLaterHyperskillAnalyticsEvent.kt new file mode 100644 index 0000000000..74d99dd5de --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/domain/analytics/NotificationsOnboardingClickedRemindMeLaterHyperskillAnalyticsEvent.kt @@ -0,0 +1,28 @@ +package org.hyperskill.app.notifications_onboarding.domain.analytics + +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 "Remind me later" button analytic event. + * + * JSON payload: + * ``` + * { + * "route": "/onboarding/notifications", + * "action": "click", + * "part": "main", + * "target": "remind_me_later" + * } + * ``` + * @see HyperskillAnalyticEvent + */ +object NotificationsOnboardingClickedRemindMeLaterHyperskillAnalyticsEvent : HyperskillAnalyticEvent( + route = HyperskillAnalyticRoute.Onboarding.Notifications, + action = HyperskillAnalyticAction.CLICK, + part = HyperskillAnalyticPart.MAIN, + target = HyperskillAnalyticTarget.REMIND_ME_LATER +) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/domain/analytics/NotificationsOnboardingViewedHyperskillAnalyticsEvent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/domain/analytics/NotificationsOnboardingViewedHyperskillAnalyticsEvent.kt new file mode 100644 index 0000000000..94eccec728 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/domain/analytics/NotificationsOnboardingViewedHyperskillAnalyticsEvent.kt @@ -0,0 +1,20 @@ +package org.hyperskill.app.notifications_onboarding.domain.analytics + +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/notifications", + * "action": "view" + * } + * ``` + * @see HyperskillAnalyticEvent + */ +object NotificationsOnboardingViewedHyperskillAnalyticsEvent : + HyperskillAnalyticEvent(HyperskillAnalyticRoute.Onboarding.Notifications, HyperskillAnalyticAction.VIEW) \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/injection/NotificationsOnboardingComponent.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/injection/NotificationsOnboardingComponent.kt new file mode 100644 index 0000000000..7d8afff9ad --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/injection/NotificationsOnboardingComponent.kt @@ -0,0 +1,10 @@ +package org.hyperskill.app.notifications_onboarding.injection + +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Action +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Message +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.State +import ru.nobird.app.presentation.redux.feature.Feature + +interface NotificationsOnboardingComponent { + val notificationsOnboardingFeature: Feature +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/injection/NotificationsOnboardingComponentImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/injection/NotificationsOnboardingComponentImpl.kt new file mode 100644 index 0000000000..8a6472afcd --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/injection/NotificationsOnboardingComponentImpl.kt @@ -0,0 +1,17 @@ +package org.hyperskill.app.notifications_onboarding.injection + +import org.hyperskill.app.core.injection.AppGraph +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Action +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Message +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.State +import ru.nobird.app.presentation.redux.feature.Feature + +internal class NotificationsOnboardingComponentImpl( + private val appGraph: AppGraph +) : NotificationsOnboardingComponent { + override val notificationsOnboardingFeature: Feature + get() = NotificationsOnboardingFeatureBuilder.build( + analyticInteractor = appGraph.analyticComponent.analyticInteractor, + notificationInteractor = appGraph.buildNotificationComponent().notificationInteractor + ) +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/injection/NotificationsOnboardingFeatureBuilder.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/injection/NotificationsOnboardingFeatureBuilder.kt new file mode 100644 index 0000000000..d1a681cb3a --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/injection/NotificationsOnboardingFeatureBuilder.kt @@ -0,0 +1,28 @@ +package org.hyperskill.app.notifications_onboarding.injection + +import org.hyperskill.app.analytic.domain.interactor.AnalyticInteractor +import org.hyperskill.app.core.presentation.ActionDispatcherOptions +import org.hyperskill.app.notification.local.domain.interactor.NotificationInteractor +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingActionDispatcher +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Action +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Message +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.State +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingReducer +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 NotificationsOnboardingFeatureBuilder { + fun build( + analyticInteractor: AnalyticInteractor, + notificationInteractor: NotificationInteractor + ): Feature { + val reducer = NotificationsOnboardingReducer() + val actionDispatcher = NotificationsOnboardingActionDispatcher( + config = ActionDispatcherOptions(), + analyticInteractor = analyticInteractor, + notificationInteractor = notificationInteractor + ) + return ReduxFeature(State, reducer).wrapWithActionDispatcher(actionDispatcher) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/presentation/NotificationsOnboardingActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/presentation/NotificationsOnboardingActionDispatcher.kt new file mode 100644 index 0000000000..776210048d --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/presentation/NotificationsOnboardingActionDispatcher.kt @@ -0,0 +1,28 @@ +package org.hyperskill.app.notifications_onboarding.presentation + +import org.hyperskill.app.analytic.domain.interactor.AnalyticInteractor +import org.hyperskill.app.core.presentation.ActionDispatcherOptions +import org.hyperskill.app.notification.local.domain.interactor.NotificationInteractor +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Action +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.InternalAction +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Message +import ru.nobird.app.presentation.redux.dispatcher.CoroutineActionDispatcher + +internal class NotificationsOnboardingActionDispatcher( + config: ActionDispatcherOptions, + private val notificationInteractor: NotificationInteractor, + private val analyticInteractor: AnalyticInteractor +) : CoroutineActionDispatcher(config.createConfig()) { + + override suspend fun doSuspendableAction(action: Action) { + when (action) { + InternalAction.UpdateLastNotificationPermissionRequestTime -> + notificationInteractor.updateLastTimeUserAskedToEnableDailyReminders() + is InternalAction.LogAnalyticsEvent -> + analyticInteractor.logEvent(action.event) + else -> { + // no op + } + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/presentation/NotificationsOnboardingFeature.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/presentation/NotificationsOnboardingFeature.kt new file mode 100644 index 0000000000..1ca4fb8c6c --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/presentation/NotificationsOnboardingFeature.kt @@ -0,0 +1,26 @@ +package org.hyperskill.app.notifications_onboarding.presentation + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticEvent + +object NotificationsOnboardingFeature { + object State + + sealed interface Message { + object AllowNotificationClicked : Message + object RemindMeLaterClicked : Message + data class NotificationPermissionRequestResult(val isPermissionGranted: Boolean) : Message + object ViewedEventMessage : Message + } + + sealed interface Action { + sealed interface ViewAction : Action { + object RequestNotificationPermission : ViewAction + object CompleteNotificationOnboarding : ViewAction + } + } + + internal sealed interface InternalAction : Action { + data class LogAnalyticsEvent(val event: HyperskillAnalyticEvent) : InternalAction + object UpdateLastNotificationPermissionRequestTime : InternalAction + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/presentation/NotificationsOnboardingReducer.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/presentation/NotificationsOnboardingReducer.kt new file mode 100644 index 0000000000..c57faf5376 --- /dev/null +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/notifications_onboarding/presentation/NotificationsOnboardingReducer.kt @@ -0,0 +1,51 @@ +package org.hyperskill.app.notifications_onboarding.presentation + +import org.hyperskill.app.analytic.domain.model.hyperskill.HyperskillAnalyticRoute +import org.hyperskill.app.notification.local.domain.analytic.NotificationSystemNoticeHiddenHyperskillAnalyticEvent +import org.hyperskill.app.notification.local.domain.analytic.NotificationSystemNoticeShownHyperskillAnalyticEvent +import org.hyperskill.app.notifications_onboarding.domain.analytics.NotificationsOnboardingClickedAllowNotificationsHyperskillAnalyticsEvent +import org.hyperskill.app.notifications_onboarding.domain.analytics.NotificationsOnboardingClickedRemindMeLaterHyperskillAnalyticsEvent +import org.hyperskill.app.notifications_onboarding.domain.analytics.NotificationsOnboardingViewedHyperskillAnalyticsEvent +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Action +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.InternalAction +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.Message +import org.hyperskill.app.notifications_onboarding.presentation.NotificationsOnboardingFeature.State +import ru.nobird.app.presentation.redux.reducer.StateReducer + +internal class NotificationsOnboardingReducer : StateReducer { + override fun reduce(state: State, message: Message): Pair> = + state to when (message) { + Message.AllowNotificationClicked -> + setOf( + InternalAction.LogAnalyticsEvent( + NotificationsOnboardingClickedAllowNotificationsHyperskillAnalyticsEvent + ), + InternalAction.LogAnalyticsEvent( + NotificationSystemNoticeShownHyperskillAnalyticEvent( + HyperskillAnalyticRoute.Onboarding.Notifications + ) + ), + Action.ViewAction.RequestNotificationPermission + ) + is Message.NotificationPermissionRequestResult -> + setOf( + InternalAction.LogAnalyticsEvent( + NotificationSystemNoticeHiddenHyperskillAnalyticEvent( + route = HyperskillAnalyticRoute.Onboarding.Notifications, + isAllowed = message.isPermissionGranted + ) + ), + InternalAction.UpdateLastNotificationPermissionRequestTime, + Action.ViewAction.CompleteNotificationOnboarding + ) + Message.RemindMeLaterClicked -> + setOf( + InternalAction.LogAnalyticsEvent( + NotificationsOnboardingClickedRemindMeLaterHyperskillAnalyticsEvent + ), + Action.ViewAction.CompleteNotificationOnboarding + ) + Message.ViewedEventMessage -> + setOf(InternalAction.LogAnalyticsEvent(NotificationsOnboardingViewedHyperskillAnalyticsEvent)) + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/cache/OnboardingCacheDataSourceImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/cache/OnboardingCacheDataSourceImpl.kt index fed00c5fbe..d566881926 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/cache/OnboardingCacheDataSourceImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/cache/OnboardingCacheDataSourceImpl.kt @@ -19,4 +19,11 @@ class OnboardingCacheDataSourceImpl( override fun setParsonsOnboardingShown(isShown: Boolean) { settings.putBoolean(OnboardingCacheKeyValues.IS_PARSONS_ONBOARDING_SHOWN, isShown) } + + override fun wasNotificationOnboardingShown(): Boolean = + settings.getBoolean(OnboardingCacheKeyValues.IS_NOTIFICATIONS_ONBOARDING_SHOWN, defaultValue = false) + + override fun setNotificationOnboardingWasShown(wasShown: Boolean) { + settings.putBoolean(OnboardingCacheKeyValues.IS_NOTIFICATIONS_ONBOARDING_SHOWN, wasShown) + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/cache/OnboardingCacheKeyValues.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/cache/OnboardingCacheKeyValues.kt index 560b764197..6eb077e258 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/cache/OnboardingCacheKeyValues.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/cache/OnboardingCacheKeyValues.kt @@ -3,4 +3,5 @@ package org.hyperskill.app.onboarding.cache object OnboardingCacheKeyValues { const val IS_ONBOARDING_SHOWN = "is_onboarding_shown" const val IS_PARSONS_ONBOARDING_SHOWN = "is_parsons_onboarding_shown" + const val IS_NOTIFICATIONS_ONBOARDING_SHOWN = "is_notifications_onboarding_shown" } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/data/repository/OnboardingRepositoryImpl.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/data/repository/OnboardingRepositoryImpl.kt index d78a4ee253..98716c6e83 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/data/repository/OnboardingRepositoryImpl.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/data/repository/OnboardingRepositoryImpl.kt @@ -19,4 +19,11 @@ class OnboardingRepositoryImpl( override fun setParsonsOnboardingShown(isShown: Boolean) { onboardingCacheDataSource.setParsonsOnboardingShown(isShown) } + + override fun wasNotificationOnboardingShown(): Boolean = + onboardingCacheDataSource.wasNotificationOnboardingShown() + + override fun setNotificationOnboardingWasShown(wasShown: Boolean) { + onboardingCacheDataSource.setNotificationOnboardingWasShown(wasShown) + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/data/source/OnboardingCacheDataSource.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/data/source/OnboardingCacheDataSource.kt index f066487115..518b211e65 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/data/source/OnboardingCacheDataSource.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/data/source/OnboardingCacheDataSource.kt @@ -5,4 +5,8 @@ interface OnboardingCacheDataSource { fun setOnboardingShown(isShown: Boolean) fun isParsonsOnboardingShown(): Boolean fun setParsonsOnboardingShown(isShown: Boolean) + + fun wasNotificationOnboardingShown(): Boolean + + fun setNotificationOnboardingWasShown(wasShown: Boolean) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/domain/interactor/OnboardingInteractor.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/domain/interactor/OnboardingInteractor.kt index a22720e91b..43f741e625 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/domain/interactor/OnboardingInteractor.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/domain/interactor/OnboardingInteractor.kt @@ -18,4 +18,11 @@ class OnboardingInteractor( fun setParsonsOnboardingShown(isShown: Boolean) { onboardingRepository.setParsonsOnboardingShown(isShown) } + + fun wasNotificationOnboardingShown(): Boolean = + onboardingRepository.wasNotificationOnboardingShown() + + fun setNotificationOnboardingWasShown(wasShown: Boolean) { + onboardingRepository.setNotificationOnboardingWasShown(wasShown) + } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/domain/repository/OnboardingRepository.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/domain/repository/OnboardingRepository.kt index dba7a2ae07..a5e1b8d76c 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/domain/repository/OnboardingRepository.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/onboarding/domain/repository/OnboardingRepository.kt @@ -5,4 +5,8 @@ interface OnboardingRepository { fun setOnboardingShown(isShown: Boolean) fun isParsonsOnboardingShown(): Boolean fun setParsonsOnboardingShown(isShown: Boolean) + + fun wasNotificationOnboardingShown(): Boolean + + fun setNotificationOnboardingWasShown(wasShown: Boolean) } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionActionDispatcher.kt b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionActionDispatcher.kt index 3203342551..78d05425d7 100644 --- a/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionActionDispatcher.kt +++ b/shared/src/commonMain/kotlin/org/hyperskill/app/step_completion/presentation/StepCompletionActionDispatcher.kt @@ -4,7 +4,6 @@ import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import kotlinx.datetime.Clock import org.hyperskill.app.SharedResources import org.hyperskill.app.analytic.domain.interactor.AnalyticInteractor import org.hyperskill.app.core.presentation.ActionDispatcherOptions @@ -151,9 +150,7 @@ class StepCompletionActionDispatcher( } private fun handlePostponeDailyStudyReminderAction() { - notificationInteractor.setLastTimeUserAskedToEnableDailyReminders( - Clock.System.now().toEpochMilliseconds() - ) + notificationInteractor.updateLastTimeUserAskedToEnableDailyReminders() } private suspend fun handleStepSolved(stepId: Long) {