From 166a1eebab80331fffdaa477686afdf3f6e86d36 Mon Sep 17 00:00:00 2001 From: PavloNetrebchuk <141041606+PavloNetrebchuk@users.noreply.github.com> Date: Thu, 2 Nov 2023 16:15:16 +0200 Subject: [PATCH] Feature/app update (#79) * App update header parsing logic * Added dialog with a recommendation to update the application * Added AppUpgradeRecommendedBox and AppUpgradeRequiredScreen * Minor code style changes * Added app version item in profile * Fixed logout logic * Added upgrade required screen to sign in, sign up and restore password fragment * jUnit test fixes * Minor code style changes * Fixed dialogs bugs, minor code refactoring * Minor review changes --- app/build.gradle | 2 +- .../main/java/org/openedx/app/AppRouter.kt | 17 +- .../main/java/org/openedx/app/AppViewModel.kt | 2 +- .../main/java/org/openedx/app/MainFragment.kt | 33 +- .../java/org/openedx/app/MainViewModel.kt | 15 + .../data/networking/AppUpgradeInterceptor.kt | 44 ++ .../data/networking/HandleErrorInterceptor.kt | 2 +- .../app/data/networking/HeadersInterceptor.kt | 12 +- .../app/data/storage/PreferencesManager.kt | 1 - .../main/java/org/openedx/app/di/AppModule.kt | 4 + .../org/openedx/app/di/NetworkingModule.kt | 20 +- .../java/org/openedx/app/di/ScreenModule.kt | 28 +- .../restore/RestorePasswordFragment.kt | 35 +- .../restore/RestorePasswordViewModel.kt | 24 +- .../presentation/signin/SignInFragment.kt | 98 +++-- .../presentation/signin/SignInViewModel.kt | 25 +- .../presentation/signup/SignUpFragment.kt | 50 ++- .../presentation/signup/SignUpViewModel.kt | 23 +- .../restore/RestorePasswordViewModelTest.kt | 25 +- .../signin/SignInViewModelTest.kt | 32 +- .../signup/SignUpViewModelTest.kt | 26 +- .../java/org/openedx/core/AppUpdateState.kt | 34 ++ .../appupgrade/AppUpgradeDialogFragment.kt | 52 +++ .../SelectBottomDialogFragment.kt | 9 +- .../SelectDialogViewModel.kt | 2 +- .../global/app_upgrade/AppUpdateUI.kt | 383 ++++++++++++++++++ .../global/app_upgrade/AppUpgradeRouter.kt | 7 + .../app_upgrade/UpgradeRequiredFragment.kt | 42 ++ .../core/system/notifier/AppUpgradeEvent.kt | 6 + .../system/notifier/AppUpgradeNotifier.kt | 15 + .../java/org/openedx/core/ui/theme/Color.kt | 4 +- .../java/org/openedx/core/ui/theme/Theme.kt | 10 +- .../java/org/openedx/core/utils/TimeUtils.kt | 10 + .../res/drawable/core_ic_icon_upgrade.xml | 32 ++ .../src/main/res/drawable/core_ic_warning.xml | 15 + core/src/main/res/values-uk/strings.xml | 14 + core/src/main/res/values/strings.xml | 13 + .../unit/video/VideoUnitFragment.kt | 7 +- .../unit/video/YoutubeVideoUnitFragment.kt | 2 +- .../presentation/DashboardFragment.kt | 59 ++- .../presentation/DashboardViewModel.kt | 20 +- .../presentation/DashboardViewModelTest.kt | 42 +- .../presentation/DiscoveryFragment.kt | 92 ++++- .../discovery/presentation/DiscoveryRouter.kt | 2 + .../presentation/DiscoveryViewModel.kt | 38 +- .../presentation/DiscoveryViewModelTest.kt | 37 +- .../data/repository/ProfileRepository.kt | 25 +- .../presentation/profile/ProfileFragment.kt | 216 +++++++++- .../presentation/profile/ProfileViewModel.kt | 25 +- .../profile/ProfileViewModelTest.kt | 48 ++- 50 files changed, 1530 insertions(+), 249 deletions(-) create mode 100644 app/src/main/java/org/openedx/app/MainViewModel.kt create mode 100644 app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt create mode 100644 core/src/main/java/org/openedx/core/AppUpdateState.kt create mode 100644 core/src/main/java/org/openedx/core/presentation/dialog/appupgrade/AppUpgradeDialogFragment.kt rename core/src/main/java/org/openedx/core/presentation/dialog/{ => selectorbottomsheet}/SelectBottomDialogFragment.kt (98%) rename core/src/main/java/org/openedx/core/presentation/dialog/{ => selectorbottomsheet}/SelectDialogViewModel.kt (90%) create mode 100644 core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpdateUI.kt create mode 100644 core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpgradeRouter.kt create mode 100644 core/src/main/java/org/openedx/core/presentation/global/app_upgrade/UpgradeRequiredFragment.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/AppUpgradeEvent.kt create mode 100644 core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt create mode 100644 core/src/main/res/drawable/core_ic_icon_upgrade.xml create mode 100644 core/src/main/res/drawable/core_ic_warning.xml diff --git a/app/build.gradle b/app/build.gradle index c7c25260b..b7e99d896 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,7 +14,7 @@ android { minSdk 24 targetSdk 34 versionCode 1 - versionName "1.0" + versionName "1.0.0" resourceConfigurations += ["en", "uk"] diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 06e6bd7a2..a37e76efa 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -10,6 +10,8 @@ import org.openedx.auth.presentation.signup.SignUpFragment import org.openedx.core.FragmentViewType import org.openedx.core.domain.model.CoursewareAccess import org.openedx.core.presentation.course.CourseViewMode +import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter +import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.container.CourseContainerFragment import org.openedx.course.presentation.container.NoAccessCourseContainerFragment @@ -36,6 +38,7 @@ import org.openedx.profile.presentation.ProfileRouter import org.openedx.profile.presentation.anothers_account.AnothersProfileFragment import org.openedx.profile.presentation.delete.DeleteProfileFragment import org.openedx.profile.presentation.edit.EditProfileFragment +import org.openedx.profile.presentation.profile.ProfileFragment import org.openedx.profile.presentation.settings.video.VideoQualityFragment import org.openedx.profile.presentation.settings.video.VideoSettingsFragment import org.openedx.whatsnew.WhatsNewRouter @@ -43,7 +46,7 @@ import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment import java.util.Date class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, DiscussionRouter, - ProfileRouter, WhatsNewRouter { + ProfileRouter, AppUpgradeRouter, WhatsNewRouter { //region AuthRouter override fun navigateToMain(fm: FragmentManager) { @@ -77,6 +80,10 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di override fun navigateToCourseSearch(fm: FragmentManager) { replaceFragmentWithBackStack(fm, CourseSearchFragment()) } + + override fun navigateToUpgradeRequired(fm: FragmentManager) { + replaceFragmentWithBackStack(fm, UpgradeRequiredFragment()) + } //endregion //region DashboardRouter @@ -289,4 +296,12 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di .replace(R.id.container, fragment, fragment.javaClass.simpleName) .commit() } + + //App upgrade + override fun navigateToUserProfile(fm: FragmentManager) { + fm.popBackStack() + fm.beginTransaction() + .replace(R.id.container, ProfileFragment()) + .commit() + } } \ No newline at end of file diff --git a/app/src/main/java/org/openedx/app/AppViewModel.kt b/app/src/main/java/org/openedx/app/AppViewModel.kt index 66447eefe..a5200d907 100644 --- a/app/src/main/java/org/openedx/app/AppViewModel.kt +++ b/app/src/main/java/org/openedx/app/AppViewModel.kt @@ -21,9 +21,9 @@ class AppViewModel( private val analytics: AppAnalytics ) : BaseViewModel() { + private val _logoutUser = SingleEventLiveData() val logoutUser: LiveData get() = _logoutUser - private val _logoutUser = SingleEventLiveData() private var logoutHandledAt: Long = 0 diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index 9152a0023..9935a5f5d 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -2,23 +2,35 @@ package org.openedx.app import android.os.Bundle import android.view.View -import androidx.viewpager2.widget.ViewPager2 import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResultListener +import androidx.viewpager2.widget.ViewPager2 +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.app.adapter.MainNavigationFragmentAdapter +import org.openedx.app.databinding.FragmentMainBinding +import org.openedx.core.presentation.global.app_upgrade.UpgradeRequiredFragment import org.openedx.core.presentation.global.viewBinding import org.openedx.dashboard.presentation.DashboardFragment import org.openedx.discovery.presentation.DiscoveryFragment -import org.openedx.app.adapter.MainNavigationFragmentAdapter import org.openedx.profile.presentation.profile.ProfileFragment -import org.koin.android.ext.android.inject -import org.openedx.app.databinding.FragmentMainBinding class MainFragment : Fragment(R.layout.fragment_main) { private val binding by viewBinding(FragmentMainBinding::bind) private val analytics by inject() + private val viewModel by viewModel() private lateinit var adapter: MainNavigationFragmentAdapter + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setFragmentResultListener(UpgradeRequiredFragment.REQUEST_KEY) { _, _ -> + binding.bottomNavView.selectedItemId = R.id.fragmentProfile + viewModel.enableBottomBar(false) + } + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -30,14 +42,17 @@ class MainFragment : Fragment(R.layout.fragment_main) { analytics.discoveryTabClickedEvent() binding.viewPager.setCurrentItem(0, false) } + R.id.fragmentDashboard -> { analytics.dashboardTabClickedEvent() binding.viewPager.setCurrentItem(1, false) } + R.id.fragmentPrograms -> { analytics.programsTabClickedEvent() binding.viewPager.setCurrentItem(2, false) } + R.id.fragmentProfile -> { analytics.profileTabClickedEvent() binding.viewPager.setCurrentItem(3, false) @@ -45,6 +60,10 @@ class MainFragment : Fragment(R.layout.fragment_main) { } true } + + viewModel.isBottomBarEnabled.observe(viewLifecycleOwner) { isBottomBarEnabled -> + enableBottomBar(isBottomBarEnabled) + } } private fun initViewPager() { @@ -59,4 +78,10 @@ class MainFragment : Fragment(R.layout.fragment_main) { binding.viewPager.adapter = adapter binding.viewPager.isUserInputEnabled = false } + + private fun enableBottomBar(enable: Boolean) { + for (i in 0 until binding.bottomNavView.menu.size()) { + binding.bottomNavView.menu.getItem(i).isEnabled = enable + } + } } \ No newline at end of file diff --git a/app/src/main/java/org/openedx/app/MainViewModel.kt b/app/src/main/java/org/openedx/app/MainViewModel.kt new file mode 100644 index 000000000..295938aa0 --- /dev/null +++ b/app/src/main/java/org/openedx/app/MainViewModel.kt @@ -0,0 +1,15 @@ +package org.openedx.app + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import org.openedx.core.BaseViewModel + +class MainViewModel: BaseViewModel() { + private val _isBottomBarEnabled = MutableLiveData(true) + val isBottomBarEnabled: LiveData + get() = _isBottomBarEnabled + + fun enableBottomBar(enable: Boolean) { + _isBottomBarEnabled.value = enable + } +} \ No newline at end of file diff --git a/app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt b/app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt new file mode 100644 index 000000000..4e88eec42 --- /dev/null +++ b/app/src/main/java/org/openedx/app/data/networking/AppUpgradeInterceptor.kt @@ -0,0 +1,44 @@ +package org.openedx.app.data.networking + +import kotlinx.coroutines.runBlocking +import okhttp3.Interceptor +import okhttp3.Response +import org.openedx.app.BuildConfig +import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.AppUpgradeNotifier +import org.openedx.core.utils.TimeUtils +import java.util.Date + +class AppUpgradeInterceptor( + private val appUpgradeNotifier: AppUpgradeNotifier +) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val response = chain.proceed(chain.request()) + val responseCode = response.code + val latestAppVersion = response.header(HEADER_APP_LATEST_VERSION) ?: "" + val lastSupportedDateString = response.header(HEADER_APP_VERSION_LAST_SUPPORTED_DATE) ?: "" + val lastSupportedDateTime = TimeUtils.iso8601WithTimeZoneToDate(lastSupportedDateString)?.time ?: 0L + runBlocking { + when { + responseCode == 426 -> { + appUpgradeNotifier.send(AppUpgradeEvent.UpgradeRequiredEvent) + } + + BuildConfig.VERSION_NAME != latestAppVersion && lastSupportedDateTime > Date().time -> { + appUpgradeNotifier.send(AppUpgradeEvent.UpgradeRecommendedEvent(latestAppVersion)) + } + + latestAppVersion.isNotEmpty() && BuildConfig.VERSION_NAME != latestAppVersion && lastSupportedDateTime < Date().time -> { + appUpgradeNotifier.send(AppUpgradeEvent.UpgradeRequiredEvent) + } + } + } + return response + } + + companion object { + const val HEADER_APP_LATEST_VERSION = "EDX-APP-LATEST-VERSION" + const val HEADER_APP_VERSION_LAST_SUPPORTED_DATE = "EDX-APP-VERSION-LAST-SUPPORTED-DATE" + } +} + diff --git a/app/src/main/java/org/openedx/app/data/networking/HandleErrorInterceptor.kt b/app/src/main/java/org/openedx/app/data/networking/HandleErrorInterceptor.kt index 501208bc3..bd4aa1920 100644 --- a/app/src/main/java/org/openedx/app/data/networking/HandleErrorInterceptor.kt +++ b/app/src/main/java/org/openedx/app/data/networking/HandleErrorInterceptor.kt @@ -32,7 +32,7 @@ class HandleErrorInterceptor( return response } } - } else if (errorResponse.errorDescription != null) { + } else if (errorResponse?.errorDescription != null) { throw EdxError.ValidationException(errorResponse.errorDescription ?: "") } } catch (e: JsonSyntaxException) { diff --git a/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt b/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt index 09af2943c..879b7d577 100644 --- a/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt +++ b/app/src/main/java/org/openedx/app/data/networking/HeadersInterceptor.kt @@ -1,11 +1,13 @@ package org.openedx.app.data.networking +import android.content.Context import okhttp3.Interceptor import okhttp3.Response -import org.openedx.core.data.storage.CorePreferences +import org.openedx.app.BuildConfig import org.openedx.core.BuildConfig.ACCESS_TOKEN_TYPE +import org.openedx.core.data.storage.CorePreferences -class HeadersInterceptor(private val preferencesManager: CorePreferences) : Interceptor { +class HeadersInterceptor(private val context: Context, private val preferencesManager: CorePreferences) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response = chain.run { proceed( @@ -18,6 +20,12 @@ class HeadersInterceptor(private val preferencesManager: CorePreferences) : Inte } addHeader("Accept", "application/json") + addHeader( + "User-Agent", System.getProperty("http.agent") + " " + + context.getString(org.openedx.core.R.string.app_name) + "/" + + BuildConfig.APPLICATION_ID + "/" + + BuildConfig.VERSION_NAME + ) }.build() ) } diff --git a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt index 66d5b96db..0ec0f1b06 100644 --- a/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt +++ b/app/src/main/java/org/openedx/app/data/storage/PreferencesManager.kt @@ -19,7 +19,6 @@ class PreferencesManager(context: Context) : CorePreferences, ProfilePreferences putString(key, value) }.apply() } - private fun getString(key: String): String = sharedPreferences.getString(key, "") ?: "" override fun clear() { diff --git a/app/src/main/java/org/openedx/app/di/AppModule.kt b/app/src/main/java/org/openedx/app/di/AppModule.kt index 50eb86f49..98e09bf08 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -37,6 +37,8 @@ import org.koin.android.ext.koin.androidApplication import org.koin.core.qualifier.named import org.koin.dsl.module import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRouter +import org.openedx.core.system.notifier.AppUpgradeNotifier import org.openedx.profile.data.storage.ProfilePreferences import org.openedx.whatsnew.WhatsNewFileManager import org.openedx.whatsnew.WhatsNewRouter @@ -59,6 +61,7 @@ val appModule = module { single { CourseNotifier() } single { DiscussionNotifier() } single { ProfileNotifier() } + single { AppUpgradeNotifier() } single { AppRouter() } single { get() } @@ -68,6 +71,7 @@ val appModule = module { single { get() } single { get() } single { get() } + single { get() } single { NetworkConnection(get()) } diff --git a/app/src/main/java/org/openedx/app/di/NetworkingModule.kt b/app/src/main/java/org/openedx/app/di/NetworkingModule.kt index ea8b645bd..5d987e568 100644 --- a/app/src/main/java/org/openedx/app/di/NetworkingModule.kt +++ b/app/src/main/java/org/openedx/app/di/NetworkingModule.kt @@ -1,17 +1,18 @@ package org.openedx.app.di +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import org.koin.dsl.module +import org.openedx.app.data.networking.AppUpgradeInterceptor +import org.openedx.app.data.networking.HandleErrorInterceptor +import org.openedx.app.data.networking.HeadersInterceptor +import org.openedx.app.data.networking.OauthRefreshTokenAuthenticator import org.openedx.auth.data.api.AuthApi +import org.openedx.core.BuildConfig import org.openedx.core.data.api.CookiesApi import org.openedx.core.data.api.CourseApi import org.openedx.discussion.data.api.DiscussionApi -import org.openedx.app.data.networking.HandleErrorInterceptor -import org.openedx.app.data.networking.HeadersInterceptor -import org.openedx.app.data.networking.OauthRefreshTokenAuthenticator import org.openedx.profile.data.api.ProfileApi -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import org.koin.dsl.module -import org.openedx.core.BuildConfig import retrofit2.Retrofit import retrofit2.converter.gson.GsonConverterFactory import java.util.concurrent.TimeUnit @@ -24,18 +25,19 @@ val networkingModule = module { OkHttpClient.Builder().apply { writeTimeout(60, TimeUnit.SECONDS) readTimeout(60, TimeUnit.SECONDS) - addInterceptor(HeadersInterceptor(get())) + addInterceptor(HeadersInterceptor(get(), get())) if (BuildConfig.DEBUG) { addNetworkInterceptor(HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY)) } addInterceptor(HandleErrorInterceptor(get())) + addInterceptor(AppUpgradeInterceptor(get())) authenticator(get()) }.build() } single { Retrofit.Builder() - .baseUrl(org.openedx.core.BuildConfig.BASE_URL) + .baseUrl(BuildConfig.BASE_URL) .client(get()) .addConverterFactory(GsonConverterFactory.create(get())) .build() diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt index f92af2364..f61cf393f 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -1,20 +1,23 @@ package org.openedx.app.di +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.core.qualifier.named +import org.koin.dsl.module +import org.openedx.app.AppViewModel +import org.openedx.app.MainViewModel import org.openedx.auth.data.repository.AuthRepository import org.openedx.auth.domain.interactor.AuthInteractor import org.openedx.auth.presentation.restore.RestorePasswordViewModel import org.openedx.auth.presentation.signin.SignInViewModel import org.openedx.auth.presentation.signup.SignUpViewModel import org.openedx.core.Validator -import org.openedx.profile.domain.model.Account -import org.openedx.core.presentation.dialog.SelectDialogViewModel +import org.openedx.core.presentation.dialog.selectorbottomsheet.SelectDialogViewModel import org.openedx.course.data.repository.CourseRepository import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.container.CourseContainerViewModel import org.openedx.course.presentation.detail.CourseDetailsViewModel import org.openedx.course.presentation.handouts.HandoutsViewModel import org.openedx.course.presentation.outline.CourseOutlineViewModel -import org.openedx.discovery.presentation.search.CourseSearchViewModel import org.openedx.course.presentation.section.CourseSectionViewModel import org.openedx.course.presentation.unit.container.CourseUnitContainerViewModel import org.openedx.course.presentation.unit.video.VideoViewModel @@ -25,6 +28,7 @@ import org.openedx.dashboard.presentation.DashboardViewModel import org.openedx.discovery.data.repository.DiscoveryRepository import org.openedx.discovery.domain.interactor.DiscoveryInteractor import org.openedx.discovery.presentation.DiscoveryViewModel +import org.openedx.discovery.presentation.search.CourseSearchViewModel import org.openedx.discussion.data.repository.DiscussionRepository import org.openedx.discussion.domain.interactor.DiscussionInteractor import org.openedx.discussion.domain.model.DiscussionComment @@ -34,17 +38,14 @@ import org.openedx.discussion.presentation.search.DiscussionSearchThreadViewMode import org.openedx.discussion.presentation.threads.DiscussionAddThreadViewModel import org.openedx.discussion.presentation.threads.DiscussionThreadsViewModel import org.openedx.discussion.presentation.topics.DiscussionTopicsViewModel -import org.openedx.app.AppViewModel import org.openedx.profile.data.repository.ProfileRepository import org.openedx.profile.domain.interactor.ProfileInteractor +import org.openedx.profile.domain.model.Account import org.openedx.profile.presentation.delete.DeleteProfileViewModel import org.openedx.profile.presentation.edit.EditProfileViewModel import org.openedx.profile.presentation.profile.ProfileViewModel import org.openedx.profile.presentation.settings.video.VideoQualityViewModel import org.openedx.profile.presentation.settings.video.VideoSettingsViewModel -import org.koin.androidx.viewmodel.dsl.viewModel -import org.koin.core.qualifier.named -import org.koin.dsl.module import org.openedx.course.presentation.unit.video.EncodedVideoUnitViewModel import org.openedx.course.presentation.unit.video.VideoUnitViewModel import org.openedx.profile.presentation.anothers_account.AnothersProfileViewModel @@ -53,25 +54,26 @@ import org.openedx.whatsnew.presentation.whatsnew.WhatsNewViewModel val screenModule = module { viewModel { AppViewModel(get(), get(), get(), get(named("IODispatcher")), get()) } + viewModel { MainViewModel() } factory { AuthRepository(get(), get()) } factory { AuthInteractor(get()) } factory { Validator() } - viewModel { SignInViewModel(get(), get(), get(), get(), get()) } - viewModel { SignUpViewModel(get(), get(), get(), get()) } - viewModel { RestorePasswordViewModel(get(), get(), get()) } + viewModel { SignInViewModel(get(), get(), get(), get(), get(), get()) } + viewModel { SignUpViewModel(get(), get(), get(), get(), get()) } + viewModel { RestorePasswordViewModel(get(), get(), get(), get()) } factory { DashboardRepository(get(), get(),get()) } factory { DashboardInteractor(get()) } - viewModel { DashboardViewModel(get(), get(), get(), get(), get()) } + viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get()) } factory { DiscoveryRepository(get(), get()) } factory { DiscoveryInteractor(get()) } - viewModel { DiscoveryViewModel(get(), get(), get(), get()) } + viewModel { DiscoveryViewModel(get(), get(), get(), get(), get()) } factory { ProfileRepository(get(), get(), get(), get()) } factory { ProfileInteractor(get()) } - viewModel { ProfileViewModel(get(), get(), get(), get(named("IODispatcher")), get(), get(), get()) } + viewModel { ProfileViewModel(get(), get(), get(), get(named("IODispatcher")), get(), get(), get(), get()) } viewModel { (account: Account) -> EditProfileViewModel(get(), get(), get(), get(), account) } viewModel { VideoSettingsViewModel(get(), get()) } viewModel { VideoQualityViewModel(get(), get()) } diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt index a48b6371a..fca8ae8e4 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordFragment.kt @@ -36,6 +36,8 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRequiredScreen +import org.openedx.core.AppUpdateState import org.openedx.auth.R as authR class RestorePasswordFragment : Fragment() { @@ -54,17 +56,27 @@ class RestorePasswordFragment : Fragment() { val uiState by viewModel.uiState.observeAsState(RestorePasswordUIState.Initial) val uiMessage by viewModel.uiMessage.observeAsState() - RestorePasswordScreen( - windowSize = windowSize, - uiState = uiState, - uiMessage = uiMessage, - onBackClick = { - requireActivity().supportFragmentManager.popBackStackImmediate() - }, - onRestoreButtonClick = { - viewModel.passwordReset(it) - } - ) + val appUpgradeEvent by viewModel.appUpgradeEventUIState.observeAsState(null) + + if (appUpgradeEvent == null) { + RestorePasswordScreen( + windowSize = windowSize, + uiState = uiState, + uiMessage = uiMessage, + onBackClick = { + requireActivity().supportFragmentManager.popBackStackImmediate() + }, + onRestoreButtonClick = { + viewModel.passwordReset(it) + } + ) + } else { + AppUpgradeRequiredScreen( + onUpdateClick = { + AppUpdateState.openPlayMarket(requireContext()) + } + ) + } } } } @@ -244,6 +256,7 @@ private fun RestorePasswordScreen( } } } + is RestorePasswordUIState.Success -> { Column( Modifier diff --git a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt index e8d635e03..427f2f263 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/restore/RestorePasswordViewModel.kt @@ -3,6 +3,7 @@ package org.openedx.auth.presentation.restore import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch import org.openedx.auth.domain.interactor.AuthInteractor import org.openedx.auth.presentation.AuthAnalytics import org.openedx.core.BaseViewModel @@ -13,12 +14,14 @@ import org.openedx.core.extension.isEmailValid import org.openedx.core.extension.isInternetError import org.openedx.core.system.EdxError import org.openedx.core.system.ResourceManager -import kotlinx.coroutines.launch +import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.AppUpgradeNotifier class RestorePasswordViewModel( private val interactor: AuthInteractor, private val resourceManager: ResourceManager, - private val analytics: AuthAnalytics + private val analytics: AuthAnalytics, + private val appUpgradeNotifier: AppUpgradeNotifier ) : BaseViewModel() { private val _uiState = MutableLiveData() @@ -29,6 +32,14 @@ class RestorePasswordViewModel( val uiMessage: LiveData get() = _uiMessage + private val _appUpgradeEvent = MutableLiveData() + val appUpgradeEventUIState: LiveData + get() = _appUpgradeEvent + + init { + collectAppUpgradeEvent() + } + fun passwordReset(email: String) { _uiState.value = RestorePasswordUIState.Loading viewModelScope.launch { @@ -64,4 +75,13 @@ class RestorePasswordViewModel( } } } + + private fun collectAppUpgradeEvent() { + viewModelScope.launch { + appUpgradeNotifier.notifier.collect { event -> + _appUpgradeEvent.value = event + } + } + } + } \ No newline at end of file diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt index 6440720fe..868765c06 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt @@ -7,15 +7,38 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.* -import androidx.compose.runtime.* +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedTextField +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextFieldDefaults +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -33,15 +56,26 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.auth.R import org.openedx.auth.presentation.AuthRouter import org.openedx.auth.presentation.ui.LoginTextField import org.openedx.core.UIMessage -import org.openedx.core.ui.* +import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRequiredScreen +import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.OpenEdXButton +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.noRippleClickable +import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography +import org.openedx.core.ui.windowSizeValue +import org.openedx.core.AppUpdateState import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.presentation.global.AppDataHolder @@ -64,33 +98,43 @@ class SignInFragment : Fragment() { val showProgress by viewModel.showProgress.observeAsState(initial = false) val uiMessage by viewModel.uiMessage.observeAsState() val loginSuccess by viewModel.loginSuccess.observeAsState(initial = false) + val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState(null) - LoginScreen( - windowSize = windowSize, - showProgress = showProgress, - uiMessage = uiMessage, - onLoginClick = { login, password -> - viewModel.login(login, password) - }, - onRegisterClick = { - viewModel.signUpClickedEvent() - router.navigateToSignUp(parentFragmentManager) - }, - onForgotPasswordClick = { - viewModel.forgotPasswordClickedEvent() - router.navigateToRestorePassword(parentFragmentManager) - } - ) + if (appUpgradeEvent == null) { + LoginScreen( + windowSize = windowSize, + showProgress = showProgress, + uiMessage = uiMessage, + onLoginClick = { login, password -> + viewModel.login(login, password) + }, + onRegisterClick = { + viewModel.signUpClickedEvent() + router.navigateToSignUp(parentFragmentManager) + }, + onForgotPasswordClick = { + viewModel.forgotPasswordClickedEvent() + router.navigateToRestorePassword(parentFragmentManager) + } + ) - LaunchedEffect(loginSuccess) { - val isNeedToShowWhatsNew = (requireActivity() as AppDataHolder).shouldShowWhatsNew() - if (loginSuccess) { - if (isNeedToShowWhatsNew) { - router.navigateToWhatsNew(parentFragmentManager) - } else { - router.navigateToMain(parentFragmentManager) + LaunchedEffect(loginSuccess) { + val isNeedToShowWhatsNew = (requireActivity() as AppDataHolder).shouldShowWhatsNew() + if (loginSuccess) { + if (isNeedToShowWhatsNew) { + router.navigateToWhatsNew(parentFragmentManager) + } else { + router.navigateToMain(parentFragmentManager) + } } } + + } else { + AppUpgradeRequiredScreen( + onUpdateClick = { + AppUpdateState.openPlayMarket(requireContext()) + } + ) } } } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt index d9ca3bed5..c1a4d1126 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt @@ -3,6 +3,7 @@ package org.openedx.auth.presentation.signin import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch import org.openedx.auth.R import org.openedx.auth.domain.interactor.AuthInteractor import org.openedx.auth.presentation.AuthAnalytics @@ -10,11 +11,12 @@ import org.openedx.core.BaseViewModel import org.openedx.core.SingleEventLiveData import org.openedx.core.UIMessage import org.openedx.core.Validator +import org.openedx.core.data.storage.CorePreferences import org.openedx.core.extension.isInternetError import org.openedx.core.system.EdxError import org.openedx.core.system.ResourceManager -import kotlinx.coroutines.launch -import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.AppUpgradeNotifier import org.openedx.core.R as CoreRes class SignInViewModel( @@ -22,7 +24,8 @@ class SignInViewModel( private val resourceManager: ResourceManager, private val preferencesManager: CorePreferences, private val validator: Validator, - private val analytics: AuthAnalytics + private val analytics: AuthAnalytics, + private val appUpgradeNotifier: AppUpgradeNotifier ) : BaseViewModel() { private val _showProgress = MutableLiveData() @@ -37,6 +40,14 @@ class SignInViewModel( val loginSuccess: LiveData get() = _loginSuccess + private val _appUpgradeEvent = MutableLiveData() + val appUpgradeEvent: LiveData + get() = _appUpgradeEvent + + init { + collectAppUpgradeEvent() + } + fun login(username: String, password: String) { if (!validator.isEmailValid(username)) { _uiMessage.value = @@ -72,6 +83,14 @@ class SignInViewModel( } } + private fun collectAppUpgradeEvent() { + viewModelScope.launch { + appUpgradeNotifier.notifier.collect { event -> + _appUpgradeEvent.value = event + } + } + } + fun signUpClickedEvent() { analytics.signUpClickedEvent() } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt index 4eb7416fd..f7af163f1 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt @@ -34,6 +34,9 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment +import kotlinx.coroutines.launch +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.auth.presentation.AuthRouter import org.openedx.auth.presentation.ui.ExpandableText import org.openedx.auth.presentation.ui.OptionalFields @@ -42,11 +45,10 @@ import org.openedx.core.R import org.openedx.core.UIMessage import org.openedx.core.domain.model.RegistrationField import org.openedx.core.domain.model.RegistrationFieldType +import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRequiredScreen import org.openedx.core.ui.* import org.openedx.core.ui.theme.* -import kotlinx.coroutines.launch -import org.koin.android.ext.android.inject -import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.AppUpdateState class SignUpFragment : Fragment() { @@ -73,25 +75,34 @@ class SignUpFragment : Fragment() { val isButtonClicked by viewModel.isButtonLoading.observeAsState(false) val successLogin by viewModel.successLogin.observeAsState() val validationError by viewModel.validationError.observeAsState(false) + val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState(null) - RegistrationScreen( - windowSize = windowSize, - uiState = uiState!!, - uiMessage = uiMessage, - isButtonClicked = isButtonClicked, - validationError, - onBackClick = { - requireActivity().supportFragmentManager.popBackStackImmediate() - }, - onRegisterClick = { map -> - viewModel.register(map.mapValues { it.value ?: "" }) - } - ) + if (appUpgradeEvent == null) { + RegistrationScreen( + windowSize = windowSize, + uiState = uiState!!, + uiMessage = uiMessage, + isButtonClicked = isButtonClicked, + validationError, + onBackClick = { + requireActivity().supportFragmentManager.popBackStackImmediate() + }, + onRegisterClick = { map -> + viewModel.register(map.mapValues { it.value ?: "" }) + } + ) - LaunchedEffect(successLogin) { - if (successLogin == true) { - router.navigateToMain(parentFragmentManager) + LaunchedEffect(successLogin) { + if (successLogin == true) { + router.navigateToMain(parentFragmentManager) + } } + } else { + AppUpgradeRequiredScreen( + onUpdateClick = { + AppUpdateState.openPlayMarket(requireContext()) + } + ) } } } @@ -304,6 +315,7 @@ internal fun RegistrationScreen( CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } + is SignUpUIState.Fields -> { mapFields.let { if (it.isEmpty()) { diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt index 4ec76f151..5e02b121a 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpViewModel.kt @@ -3,6 +3,7 @@ package org.openedx.auth.presentation.signup import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch import org.openedx.auth.domain.interactor.AuthInteractor import org.openedx.auth.presentation.AuthAnalytics import org.openedx.core.ApiConstants @@ -14,13 +15,15 @@ import org.openedx.core.data.storage.CorePreferences import org.openedx.core.domain.model.RegistrationField import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager -import kotlinx.coroutines.launch +import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.AppUpgradeNotifier class SignUpViewModel( private val interactor: AuthInteractor, private val resourceManager: ResourceManager, private val analytics: AuthAnalytics, - private val preferencesManager: CorePreferences + private val preferencesManager: CorePreferences, + private val appUpgradeNotifier: AppUpgradeNotifier ) : BaseViewModel() { private val _uiState = MutableLiveData(SignUpUIState.Loading) @@ -43,9 +46,17 @@ class SignUpViewModel( val validationError: LiveData get() = _validationError + private val _appUpgradeEvent = MutableLiveData() + val appUpgradeEvent: LiveData + get() = _appUpgradeEvent + private val optionalFields = mutableMapOf() private val allFields = mutableListOf() + init { + collectAppUpgradeEvent() + } + fun getRegistrationFields() { _uiState.value = SignUpUIState.Loading viewModelScope.launch { @@ -128,6 +139,14 @@ class SignUpViewModel( ) } + private fun collectAppUpgradeEvent() { + viewModelScope.launch { + appUpgradeNotifier.notifier.collect { event -> + _appUpgradeEvent.value = event + } + } + } + private fun setUserId() { preferencesManager.user?.let { diff --git a/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt index 895a87739..e80b93db7 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/restore/RestorePasswordViewModelTest.kt @@ -14,6 +14,7 @@ import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.* import org.junit.After import org.junit.Assert.* @@ -21,6 +22,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule +import org.openedx.core.system.notifier.AppUpgradeNotifier import java.net.UnknownHostException @OptIn(ExperimentalCoroutinesApi::class) @@ -33,6 +35,7 @@ class RestorePasswordViewModelTest { private val resourceManager = mockk() private val interactor = mockk() private val analytics = mockk() + private val appUpgradeNotifier = mockk() //region parameters @@ -53,6 +56,7 @@ class RestorePasswordViewModelTest { every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong every { resourceManager.getString(org.openedx.auth.R.string.auth_invalid_email) } returns invalidEmail every { resourceManager.getString(org.openedx.auth.R.string.auth_invalid_password) } returns invalidPassword + every { appUpgradeNotifier.notifier } returns emptyFlow() } @After @@ -62,13 +66,14 @@ class RestorePasswordViewModelTest { @Test fun `passwordReset empty email validation error`() = runTest { - val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics) + val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) coEvery { interactor.passwordReset(emptyEmail) } returns true every { analytics.resetPasswordClickedEvent(false) } returns Unit viewModel.passwordReset(emptyEmail) advanceUntilIdle() coVerify(exactly = 0) { interactor.passwordReset(any()) } verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -78,13 +83,14 @@ class RestorePasswordViewModelTest { @Test fun `passwordReset invalid email validation error`() = runTest { - val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics) + val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) coEvery { interactor.passwordReset(invalidEmail) } returns true every { analytics.resetPasswordClickedEvent(false) } returns Unit viewModel.passwordReset(invalidEmail) advanceUntilIdle() coVerify(exactly = 0) { interactor.passwordReset(any()) } verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -94,13 +100,14 @@ class RestorePasswordViewModelTest { @Test fun `passwordReset validation error`() = runTest { - val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics) + val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) coEvery { interactor.passwordReset(correctEmail) } throws EdxError.ValidationException("error") every { analytics.resetPasswordClickedEvent(false) } returns Unit viewModel.passwordReset(correctEmail) advanceUntilIdle() coVerify(exactly = 1) { interactor.passwordReset(any()) } verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -110,13 +117,14 @@ class RestorePasswordViewModelTest { @Test fun `passwordReset no internet error`() = runTest { - val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics) + val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) coEvery { interactor.passwordReset(correctEmail) } throws UnknownHostException() every { analytics.resetPasswordClickedEvent(false) } returns Unit viewModel.passwordReset(correctEmail) advanceUntilIdle() coVerify(exactly = 1) { interactor.passwordReset(any()) } verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -126,13 +134,14 @@ class RestorePasswordViewModelTest { @Test fun `passwordReset unknown error`() = runTest { - val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics) + val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) coEvery { interactor.passwordReset(correctEmail) } throws Exception() every { analytics.resetPasswordClickedEvent(false) } returns Unit viewModel.passwordReset(correctEmail) advanceUntilIdle() coVerify(exactly = 1) { interactor.passwordReset(any()) } verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -142,13 +151,14 @@ class RestorePasswordViewModelTest { @Test fun `unSuccess restore password`() = runTest { - val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics) + val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) coEvery { interactor.passwordReset(correctEmail) } returns false every { analytics.resetPasswordClickedEvent(false) } returns Unit viewModel.passwordReset(correctEmail) advanceUntilIdle() coVerify(exactly = 1) { interactor.passwordReset(any()) } verify(exactly = 1) { analytics.resetPasswordClickedEvent(false) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -159,13 +169,14 @@ class RestorePasswordViewModelTest { @Test fun `success restore password`() = runTest { - val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics) + val viewModel = RestorePasswordViewModel(interactor, resourceManager, analytics, appUpgradeNotifier) coEvery { interactor.passwordReset(correctEmail) } returns true every { analytics.resetPasswordClickedEvent(true) } returns Unit viewModel.passwordReset(correctEmail) advanceUntilIdle() coVerify(exactly = 1) { interactor.passwordReset(any()) } verify(exactly = 1) { analytics.resetPasswordClickedEvent(true) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val state = viewModel.uiState.value as? RestorePasswordUIState.Success val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt index c360e5a58..b608bd479 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt @@ -16,6 +16,7 @@ import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain @@ -28,6 +29,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.system.notifier.AppUpgradeNotifier import java.net.UnknownHostException import org.openedx.core.R as CoreRes @@ -44,6 +46,7 @@ class SignInViewModelTest { private val preferencesManager = mockk() private val interactor = mockk() private val analytics = mockk() + private val appUpgradeNotifier = mockk() private val invalidCredential = "Invalid credentials" private val noInternet = "Slow or no internet connection" @@ -61,6 +64,7 @@ class SignInViewModelTest { every { resourceManager.getString(CoreRes.string.core_error_unknown_error) } returns somethingWrong every { resourceManager.getString(R.string.auth_invalid_email) } returns invalidEmail every { resourceManager.getString(R.string.auth_invalid_password) } returns invalidPassword + every { appUpgradeNotifier.notifier } returns emptyFlow() } @After @@ -73,8 +77,7 @@ class SignInViewModelTest { every { validator.isEmailValid(any()) } returns false every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit - val viewModel = - SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics) + val viewModel = SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics, appUpgradeNotifier) viewModel.login("", "") coVerify(exactly = 0) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } @@ -92,7 +95,7 @@ class SignInViewModelTest { every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit val viewModel = - SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics) + SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics, appUpgradeNotifier) viewModel.login("acc@test.o", "") coVerify(exactly = 0) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } @@ -112,12 +115,12 @@ class SignInViewModelTest { every { analytics.setUserIdForSession(any()) } returns Unit coVerify(exactly = 0) { interactor.login(any(), any()) } val viewModel = - SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics) + SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics, appUpgradeNotifier) viewModel.login("acc@test.org", "") verify(exactly = 0) { analytics.setUserIdForSession(any()) } - val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage + val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage assertEquals(invalidPassword, message.message) assert(viewModel.showProgress.value != true) assert(viewModel.loginSuccess.value != true) @@ -130,14 +133,13 @@ class SignInViewModelTest { every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit val viewModel = - SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics) + SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics, appUpgradeNotifier) viewModel.login("acc@test.org", "ed") coVerify(exactly = 0) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage - assertEquals(invalidPassword, message.message) assert(viewModel.showProgress.value != true) assert(viewModel.loginSuccess.value != true) @@ -150,8 +152,7 @@ class SignInViewModelTest { every { analytics.userLoginEvent(any()) } returns Unit every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit - val viewModel = - SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics) + val viewModel = SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics, appUpgradeNotifier) coEvery { interactor.login("acc@test.org", "edx") } returns Unit viewModel.login("acc@test.org", "edx") advanceUntilIdle() @@ -159,6 +160,7 @@ class SignInViewModelTest { coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 1) { analytics.userLoginEvent(any()) } verify(exactly = 1) { analytics.setUserIdForSession(any()) } + verify(exactly = 1) { appUpgradeNotifier.notifier } assertEquals(false, viewModel.showProgress.value) assertEquals(true, viewModel.loginSuccess.value) @@ -172,16 +174,16 @@ class SignInViewModelTest { every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit val viewModel = - SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics) + SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics, appUpgradeNotifier) coEvery { interactor.login("acc@test.org", "edx") } throws UnknownHostException() viewModel.login("acc@test.org", "edx") advanceUntilIdle() coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage - assertEquals(false, viewModel.showProgress.value) assert(viewModel.loginSuccess.value != true) assertEquals(noInternet, message?.message) @@ -194,16 +196,16 @@ class SignInViewModelTest { every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit val viewModel = - SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics) + SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics, appUpgradeNotifier) coEvery { interactor.login("acc@test.org", "edx") } throws EdxError.InvalidGrantException() viewModel.login("acc@test.org", "edx") advanceUntilIdle() coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage - assertEquals(false, viewModel.showProgress.value) assert(viewModel.loginSuccess.value != true) assertEquals(invalidCredential, message.message) @@ -216,16 +218,16 @@ class SignInViewModelTest { every { preferencesManager.user } returns user every { analytics.setUserIdForSession(any()) } returns Unit val viewModel = - SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics) + SignInViewModel(interactor, resourceManager, preferencesManager, validator, analytics, appUpgradeNotifier) coEvery { interactor.login("acc@test.org", "edx") } throws IllegalStateException() viewModel.login("acc@test.org", "edx") advanceUntilIdle() coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as UIMessage.SnackBarMessage - assertEquals(false, viewModel.showProgress.value) assert(viewModel.loginSuccess.value != true) assertEquals(somethingWrong, message.message) diff --git a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt index 3022e052f..2309bda01 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signup/SignUpViewModelTest.kt @@ -18,6 +18,7 @@ import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.resetMain @@ -30,6 +31,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.system.notifier.AppUpgradeNotifier import java.net.UnknownHostException @@ -44,6 +46,7 @@ class SignUpViewModelTest { private val preferencesManager = mockk() private val interactor = mockk() private val analytics = mockk() + private val appUpgradeNotifier = mockk() //region parameters @@ -91,7 +94,7 @@ class SignUpViewModelTest { every { resourceManager.getString(R.string.core_error_invalid_grant) } returns "Invalid credentials" every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong - + every { appUpgradeNotifier.notifier } returns emptyFlow() } @After @@ -101,7 +104,7 @@ class SignUpViewModelTest { @Test fun `register has validation errors`() = runTest { - val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager) + val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager, appUpgradeNotifier) coEvery { interactor.validateRegistrationFields(parametersMap) } returns ValidationFields( parametersMap ) @@ -117,6 +120,7 @@ class SignUpViewModelTest { coVerify(exactly = 0) { interactor.register(any()) } coVerify(exactly = 0) { interactor.login(any(), any()) } verify(exactly = 0) { analytics.setUserIdForSession(any()) } + verify(exactly = 1) { appUpgradeNotifier.notifier } assertEquals(true, viewModel.validationError.value) assert(viewModel.successLogin.value != true) @@ -126,7 +130,7 @@ class SignUpViewModelTest { @Test fun `register no internet error`() = runTest { - val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager) + val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager, appUpgradeNotifier) coEvery { interactor.validateRegistrationFields(parametersMap) } throws UnknownHostException() coEvery { interactor.register(parametersMap) } returns Unit coEvery { @@ -145,6 +149,7 @@ class SignUpViewModelTest { coVerify(exactly = 1) { interactor.validateRegistrationFields(any()) } coVerify(exactly = 0) { interactor.register(any()) } coVerify(exactly = 0) { interactor.login(any(), any()) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -156,7 +161,7 @@ class SignUpViewModelTest { @Test fun `something went wrong error`() = runTest { - val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager) + val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager, appUpgradeNotifier) coEvery { interactor.validateRegistrationFields(parametersMap) } throws Exception() coEvery { interactor.register(parametersMap) } returns Unit coEvery { interactor.login("", "") } returns Unit @@ -170,6 +175,7 @@ class SignUpViewModelTest { coVerify(exactly = 1) { interactor.validateRegistrationFields(any()) } coVerify(exactly = 0) { interactor.register(any()) } coVerify(exactly = 0) { interactor.login(any(), any()) } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -182,7 +188,7 @@ class SignUpViewModelTest { @Test fun `success register`() = runTest { - val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager) + val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager, appUpgradeNotifier) coEvery { interactor.validateRegistrationFields(parametersMap) } returns ValidationFields( emptyMap() ) @@ -205,6 +211,7 @@ class SignUpViewModelTest { coVerify(exactly = 1) { interactor.login(any(), any()) } verify(exactly = 1) { analytics.createAccountClickedEvent(any()) } verify(exactly = 1) { analytics.registrationSuccessEvent(any()) } + verify(exactly = 1) { appUpgradeNotifier.notifier } assertEquals(false, viewModel.validationError.value) assertEquals(false, viewModel.isButtonLoading.value) @@ -214,11 +221,12 @@ class SignUpViewModelTest { @Test fun `getRegistrationFields no internet error`() = runTest { - val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager) + val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager, appUpgradeNotifier) coEvery { interactor.getRegistrationFields() } throws UnknownHostException() viewModel.getRegistrationFields() advanceUntilIdle() coVerify(exactly = 1) { interactor.getRegistrationFields() } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -228,11 +236,12 @@ class SignUpViewModelTest { @Test fun `getRegistrationFields unknown error`() = runTest { - val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager) + val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager, appUpgradeNotifier) coEvery { interactor.getRegistrationFields() } throws Exception() viewModel.getRegistrationFields() advanceUntilIdle() coVerify(exactly = 1) { interactor.getRegistrationFields() } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage @@ -242,11 +251,12 @@ class SignUpViewModelTest { @Test fun `getRegistrationFields success`() = runTest { - val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager) + val viewModel = SignUpViewModel(interactor, resourceManager, analytics, preferencesManager, appUpgradeNotifier) coEvery { interactor.getRegistrationFields() } returns listOfFields viewModel.getRegistrationFields() advanceUntilIdle() coVerify(exactly = 1) { interactor.getRegistrationFields() } + verify(exactly = 1) { appUpgradeNotifier.notifier } //val fields = viewModel.uiState.value as? SignUpUIState.Fields diff --git a/core/src/main/java/org/openedx/core/AppUpdateState.kt b/core/src/main/java/org/openedx/core/AppUpdateState.kt new file mode 100644 index 000000000..bf347cd29 --- /dev/null +++ b/core/src/main/java/org/openedx/core/AppUpdateState.kt @@ -0,0 +1,34 @@ +package org.openedx.core + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.compose.runtime.mutableStateOf +import org.openedx.core.system.notifier.AppUpgradeEvent + +object AppUpdateState { + var wasUpdateDialogDisplayed = false + var wasUpdateDialogClosed = mutableStateOf(false) + + fun openPlayMarket(context: Context) { + try { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=${context.packageName}"))) + } catch (e: ActivityNotFoundException) { + context.startActivity( + Intent( + Intent.ACTION_VIEW, + Uri.parse("https://play.google.com/store/apps/details?id=${context.packageName}") + ) + ) + } + } + + data class AppUpgradeParameters( + val appUpgradeEvent: AppUpgradeEvent? = null, + val wasUpdateDialogClosed: Boolean = AppUpdateState.wasUpdateDialogClosed.value, + val appUpgradeRecommendedDialog: () -> Unit = {}, + val onAppUpgradeRecommendedBoxClick: () -> Unit = {}, + val onAppUpgradeRequired: () -> Unit = {}, + ) +} \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/appupgrade/AppUpgradeDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/appupgrade/AppUpgradeDialogFragment.kt new file mode 100644 index 000000000..4c5c4ce56 --- /dev/null +++ b/core/src/main/java/org/openedx/core/presentation/dialog/appupgrade/AppUpgradeDialogFragment.kt @@ -0,0 +1,52 @@ +package org.openedx.core.presentation.dialog.appupgrade + +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.fragment.app.DialogFragment +import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendDialog +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.AppUpdateState + +class AppUpgradeDialogFragment : DialogFragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ) = ComposeView(requireContext()).apply { + if (dialog != null && dialog!!.window != null) { + dialog!!.window?.setBackgroundDrawable(ColorDrawable(android.graphics.Color.TRANSPARENT)) + } + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + AppUpgradeRecommendDialog( + onNotNowClick = this@AppUpgradeDialogFragment::onNotNowClick, + onUpdateClick = this@AppUpgradeDialogFragment::onUpdateClick + ) + } + } + } + + private fun onNotNowClick() { + AppUpdateState.wasUpdateDialogClosed.value = true + dismiss() + } + + private fun onUpdateClick() { + AppUpdateState.wasUpdateDialogClosed.value = true + dismiss() + AppUpdateState.openPlayMarket(requireContext()) + } + + companion object { + fun newInstance(): AppUpgradeDialogFragment { + return AppUpgradeDialogFragment() + } + } + +} diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/SelectBottomDialogFragment.kt b/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectBottomDialogFragment.kt similarity index 98% rename from core/src/main/java/org/openedx/core/presentation/dialog/SelectBottomDialogFragment.kt rename to core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectBottomDialogFragment.kt index 8d15d6973..e2b6bdd58 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/SelectBottomDialogFragment.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectBottomDialogFragment.kt @@ -1,4 +1,4 @@ -package org.openedx.core.presentation.dialog +package org.openedx.core.presentation.dialog.selectorbottomsheet import android.graphics.Color import android.graphics.drawable.ColorDrawable @@ -29,16 +29,15 @@ import androidx.fragment.app.DialogFragment import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.R import org.openedx.core.domain.model.RegistrationField import org.openedx.core.extension.parcelableArrayList import org.openedx.core.ui.SheetContent -import org.openedx.core.ui.rememberWindowSize -import org.openedx.core.ui.theme.OpenEdXTheme -import org.openedx.core.ui.theme.appColors -import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.ui.isImeVisibleState import org.openedx.core.ui.noRippleClickable +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes class SelectBottomDialogFragment : BottomSheetDialogFragment() { diff --git a/core/src/main/java/org/openedx/core/presentation/dialog/SelectDialogViewModel.kt b/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectDialogViewModel.kt similarity index 90% rename from core/src/main/java/org/openedx/core/presentation/dialog/SelectDialogViewModel.kt rename to core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectDialogViewModel.kt index 07a2f1b74..6a09f5724 100644 --- a/core/src/main/java/org/openedx/core/presentation/dialog/SelectDialogViewModel.kt +++ b/core/src/main/java/org/openedx/core/presentation/dialog/selectorbottomsheet/SelectDialogViewModel.kt @@ -1,4 +1,4 @@ -package org.openedx.core.presentation.dialog +package org.openedx.core.presentation.dialog.selectorbottomsheet import androidx.lifecycle.viewModelScope import org.openedx.core.BaseViewModel diff --git a/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpdateUI.kt b/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpdateUI.kt new file mode 100644 index 000000000..9dcf32b75 --- /dev/null +++ b/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpdateUI.kt @@ -0,0 +1,383 @@ +package org.openedx.core.presentation.global.app_upgrade + +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.content.res.Configuration.UI_MODE_NIGHT_NO +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.Button +import androidx.compose.material.ButtonDefaults +import androidx.compose.material.Card +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalConfiguration +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 org.openedx.core.R +import org.openedx.core.ui.noRippleClickable +import org.openedx.core.ui.statusBarsInset +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.ui.theme.appColors +import org.openedx.core.ui.theme.appShapes +import org.openedx.core.ui.theme.appTypography + +@Composable +fun AppUpgradeRequiredScreen( + modifier: Modifier = Modifier, + onUpdateClick: () -> Unit +) { + AppUpgradeRequiredScreen( + modifier = modifier, + showAccountSettingsButton = false, + onAccountSettingsClick = {}, + onUpdateClick = onUpdateClick + ) +} + +@Composable +fun AppUpgradeRequiredScreen( + modifier: Modifier = Modifier, + showAccountSettingsButton: Boolean, + onAccountSettingsClick: () -> Unit, + onUpdateClick: () -> Unit +) { + Box( + modifier = modifier + .fillMaxSize() + .background(color = MaterialTheme.appColors.background) + .statusBarsInset(), + contentAlignment = Alignment.TopCenter + ) { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp, bottom = 12.dp), + text = stringResource(id = R.string.core_deprecated_app_version), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium, + textAlign = TextAlign.Center + ) + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + AppUpgradeRequiredContent( + modifier = Modifier.padding(horizontal = 32.dp), + showAccountSettingsButton = showAccountSettingsButton, + onAccountSettingsClick = onAccountSettingsClick, + onUpdateClick = onUpdateClick + ) + } + } +} + +@Composable +fun AppUpgradeRecommendDialog( + modifier: Modifier = Modifier, + onNotNowClick: () -> Unit, + onUpdateClick: () -> Unit +) { + val orientation = LocalConfiguration.current.orientation + val imageModifier = if (orientation == ORIENTATION_LANDSCAPE) { + Modifier.size(60.dp) + } else { + Modifier + } + + Surface( + modifier = modifier, + color = Color.Transparent + ) { + Box( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 12.dp) + .noRippleClickable { + onNotNowClick() + }, + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .widthIn(max = 640.dp) + .fillMaxWidth() + .clip(MaterialTheme.appShapes.cardShape) + .noRippleClickable {} + .background( + color = MaterialTheme.appColors.background, + shape = MaterialTheme.appShapes.cardShape + ) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(32.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + Image( + modifier = imageModifier, + painter = painterResource(id = R.drawable.core_ic_icon_upgrade), + contentDescription = null + ) + Text( + text = stringResource(id = R.string.core_app_upgrade_title), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium + ) + Text( + text = stringResource(id = R.string.core_app_upgrade_dialog_description), + color = MaterialTheme.appColors.textPrimary, + textAlign = TextAlign.Center, + style = MaterialTheme.appTypography.bodyMedium + ) + AppUpgradeDialogButtons( + onNotNowClick = onNotNowClick, + onUpdateClick = onUpdateClick + ) + } + } + } + } +} + +@Composable +fun AppUpgradeRequiredContent( + modifier: Modifier = Modifier, + showAccountSettingsButton: Boolean, + onAccountSettingsClick: () -> Unit, + onUpdateClick: () -> Unit +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(32.dp) + ) { + Image( + painter = painterResource(id = R.drawable.core_ic_warning), + contentDescription = null + ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + Text( + text = stringResource(id = R.string.core_app_update_required_title), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleMedium + ) + Text( + text = stringResource(id = R.string.core_app_update_required_description), + color = MaterialTheme.appColors.textPrimary, + textAlign = TextAlign.Center, + style = MaterialTheme.appTypography.bodyMedium + ) + } + AppUpgradeRequiredButtons( + showAccountSettingsButton = showAccountSettingsButton, + onAccountSettingsClick = onAccountSettingsClick, + onUpdateClick = onUpdateClick + ) + } +} + +@Composable +fun AppUpgradeRequiredButtons( + showAccountSettingsButton: Boolean, + onAccountSettingsClick: () -> Unit, + onUpdateClick: () -> Unit +) { + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp) + ) { + if (showAccountSettingsButton) { + TransparentTextButton( + text = stringResource(id = R.string.core_account_settings), + onClick = onAccountSettingsClick + ) + } + DefaultTextButton( + text = stringResource(id = R.string.core_update), + onClick = onUpdateClick + ) + } +} + +@Composable +fun AppUpgradeDialogButtons( + onNotNowClick: () -> Unit, + onUpdateClick: () -> Unit +) { + Row( + horizontalArrangement = Arrangement.spacedBy(24.dp) + ) { + TransparentTextButton( + text = stringResource(id = R.string.core_not_now), + onClick = onNotNowClick + ) + DefaultTextButton( + text = stringResource(id = R.string.core_update), + onClick = onUpdateClick + ) + } +} + +@Composable +fun TransparentTextButton( + text: String, + onClick: () -> Unit +) { + Button( + modifier = Modifier + .height(42.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = Color.Transparent + ), + elevation = null, + shape = MaterialTheme.appShapes.navigationButtonShape, + onClick = onClick + ) { + Text( + color = MaterialTheme.appColors.textAccent, + style = MaterialTheme.appTypography.labelLarge, + text = text + ) + } +} + +@Composable +fun DefaultTextButton( + text: String, + onClick: () -> Unit +) { + Button( + modifier = Modifier + .height(42.dp), + colors = ButtonDefaults.buttonColors( + backgroundColor = MaterialTheme.appColors.buttonBackground + ), + elevation = null, + shape = MaterialTheme.appShapes.navigationButtonShape, + onClick = onClick + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Text( + text = text, + color = MaterialTheme.appColors.buttonText, + style = MaterialTheme.appTypography.labelLarge + ) + } + } +} + +@Composable +fun AppUpgradeRecommendedBox( + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + Card( + modifier = modifier + .fillMaxWidth() + .padding(20.dp) + .clickable { + onClick() + }, + shape = MaterialTheme.appShapes.cardShape, + backgroundColor = MaterialTheme.appColors.primary + ) { + Row( + modifier = Modifier.padding(horizontal = 20.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + modifier = Modifier.size(40.dp), + painter = painterResource(id = R.drawable.core_ic_icon_upgrade), + contentDescription = null, + tint = Color.White + ) + Column { + Text( + text = stringResource(id = R.string.core_app_upgrade_title), + color = Color.White, + style = MaterialTheme.appTypography.titleMedium + ) + Text( + text = stringResource(id = R.string.core_app_upgrade_box_description), + color = Color.White, + style = MaterialTheme.appTypography.bodyMedium + ) + } + } + } +} + +@Preview(uiMode = UI_MODE_NIGHT_NO) +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun AppUpgradeRequiredScreenPreview() { + OpenEdXTheme { + AppUpgradeRequiredScreen( + showAccountSettingsButton = true, + onAccountSettingsClick = {}, + onUpdateClick = {} + ) + } +} + +@Preview +@Composable +private fun AppUpgradeRecommendedBoxPreview() { + OpenEdXTheme { + AppUpgradeRecommendedBox( + onClick = {} + ) + } +} + +@Preview +@Composable +private fun AppUpgradeDialogButtonsPreview() { + OpenEdXTheme { + AppUpgradeDialogButtons( + onNotNowClick = {}, + onUpdateClick = {} + ) + } +} + +@Preview(uiMode = UI_MODE_NIGHT_NO) +@Preview(uiMode = UI_MODE_NIGHT_YES) +@Composable +private fun AppUpgradeRecommendDialogPreview() { + OpenEdXTheme { + AppUpgradeRecommendDialog( + onNotNowClick = {}, + onUpdateClick = {} + ) + } +} \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpgradeRouter.kt b/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpgradeRouter.kt new file mode 100644 index 000000000..482c91093 --- /dev/null +++ b/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/AppUpgradeRouter.kt @@ -0,0 +1,7 @@ +package org.openedx.core.presentation.global.app_upgrade + +import androidx.fragment.app.FragmentManager + +interface AppUpgradeRouter { + fun navigateToUserProfile(fm: FragmentManager) +} \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/UpgradeRequiredFragment.kt b/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/UpgradeRequiredFragment.kt new file mode 100644 index 000000000..da8685435 --- /dev/null +++ b/core/src/main/java/org/openedx/core/presentation/global/app_upgrade/UpgradeRequiredFragment.kt @@ -0,0 +1,42 @@ +package org.openedx.core.presentation.global.app_upgrade + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import org.openedx.core.ui.theme.OpenEdXTheme +import org.openedx.core.AppUpdateState + +class UpgradeRequiredFragment : Fragment() { + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ) = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + AppUpgradeRequiredScreen( + showAccountSettingsButton = true, + onAccountSettingsClick = { + setFragmentResult(REQUEST_KEY, bundleOf(OPEN_ACCOUNT_SETTINGS_KEY to "")) + parentFragmentManager.popBackStack() + }, + onUpdateClick = { + AppUpdateState.openPlayMarket(requireContext()) + } + ) + } + } + } + + companion object { + const val REQUEST_KEY = "UpgradeRequiredFragmentRequestKey" + const val OPEN_ACCOUNT_SETTINGS_KEY = "openAccountSettings" + } +} \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeEvent.kt b/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeEvent.kt new file mode 100644 index 000000000..f99086a11 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeEvent.kt @@ -0,0 +1,6 @@ +package org.openedx.core.system.notifier + +sealed class AppUpgradeEvent { + object UpgradeRequiredEvent : AppUpgradeEvent() + class UpgradeRecommendedEvent(val newVersionName: String) : AppUpgradeEvent() +} diff --git a/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt b/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt new file mode 100644 index 000000000..0f5a274d5 --- /dev/null +++ b/core/src/main/java/org/openedx/core/system/notifier/AppUpgradeNotifier.kt @@ -0,0 +1,15 @@ +package org.openedx.core.system.notifier + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +class AppUpgradeNotifier { + + private val channel = MutableSharedFlow(replay = 0, extraBufferCapacity = 0) + + val notifier: Flow = channel.asSharedFlow() + + suspend fun send(event: AppUpgradeEvent) = channel.emit(event) + +} \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/ui/theme/Color.kt b/core/src/main/java/org/openedx/core/ui/theme/Color.kt index 4dedf421c..b84774bd1 100644 --- a/core/src/main/java/org/openedx/core/ui/theme/Color.kt +++ b/core/src/main/java/org/openedx/core/ui/theme/Color.kt @@ -29,7 +29,9 @@ data class AppColors( val certificateForeground: Color, val bottomSheetToggle: Color, val warning: Color, - val info: Color + val info: Color, + + val accessGreen:Color ) { val primary: Color get() = material.primary val primaryVariant: Color get() = material.primaryVariant diff --git a/core/src/main/java/org/openedx/core/ui/theme/Theme.kt b/core/src/main/java/org/openedx/core/ui/theme/Theme.kt index a468ebb72..c7ee45e61 100644 --- a/core/src/main/java/org/openedx/core/ui/theme/Theme.kt +++ b/core/src/main/java/org/openedx/core/ui/theme/Theme.kt @@ -30,7 +30,7 @@ private val DarkColorPalette = AppColors( textPrimaryVariant = Color(0xFF79889F), textSecondary = Color(0xFFB3B3B3), textDark = Color(0xFF19212F), - textAccent = Color(0xFF5478F9), + textAccent = Color(0xFF879FF5), textFieldBackground = Color(0xFF273346), textFieldBackgroundVariant = Color(0xFF273346), @@ -50,7 +50,9 @@ private val DarkColorPalette = AppColors( bottomSheetToggle = Color(0xFF4E5A70), warning = Color(0xFFFFC248), - info = Color(0xFF0095FF) + info = Color(0xFF0095FF), + + accessGreen = Color(0xFF23BCA0) ) private val LightColorPalette = AppColors( @@ -92,7 +94,9 @@ private val LightColorPalette = AppColors( bottomSheetToggle = Color(0xFF4E5A70), warning = Color(0xFFFFC94D), - info = Color(0xFF42AAFF) + info = Color(0xFF42AAFF), + + accessGreen = Color(0xFF23BCA0) ) val MaterialTheme.appColors: AppColors diff --git a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt index bf30e40e2..dc31f7102 100644 --- a/core/src/main/java/org/openedx/core/utils/TimeUtils.kt +++ b/core/src/main/java/org/openedx/core/utils/TimeUtils.kt @@ -12,6 +12,7 @@ import java.util.* object TimeUtils { private const val FORMAT_ISO_8601 = "yyyy-MM-dd'T'HH:mm:ss'Z'" + private const val FORMAT_ISO_8601_WITH_TIME_ZONE = "yyyy-MM-dd'T'HH:mm:ssXXX" private const val FORMAT_APPLICATION = "dd.MM.yyyy HH:mm" private const val FORMAT_DATE = "dd MMM, yyyy" @@ -26,6 +27,15 @@ object TimeUtils { } } + fun iso8601WithTimeZoneToDate(text: String): Date? { + return try { + val sdf = SimpleDateFormat(FORMAT_ISO_8601_WITH_TIME_ZONE, Locale.getDefault()) + sdf.parse(text) + } catch (e: ParseException) { + null + } + } + fun iso8601ToDateWithTime(context: Context,text: String): String { return try { val courseDateFormat = SimpleDateFormat(FORMAT_ISO_8601, Locale.getDefault()) diff --git a/core/src/main/res/drawable/core_ic_icon_upgrade.xml b/core/src/main/res/drawable/core_ic_icon_upgrade.xml new file mode 100644 index 000000000..6e3ffa576 --- /dev/null +++ b/core/src/main/res/drawable/core_ic_icon_upgrade.xml @@ -0,0 +1,32 @@ + + + + + + diff --git a/core/src/main/res/drawable/core_ic_warning.xml b/core/src/main/res/drawable/core_ic_warning.xml new file mode 100644 index 000000000..7270dc5a9 --- /dev/null +++ b/core/src/main/res/drawable/core_ic_warning.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/core/src/main/res/values-uk/strings.xml b/core/src/main/res/values-uk/strings.xml index 68027aa79..581b19366 100644 --- a/core/src/main/res/values-uk/strings.xml +++ b/core/src/main/res/values-uk/strings.xml @@ -38,6 +38,20 @@ Відгук клієнта dd MMMM dd MMM yyyy HH:mm + Оновлення додатку + Ми рекомендуємо вам оновитись до останньої версії. Оновіться зараз, щоб отримати останні функції та виправлення. + Доступне нове оновлення! Оновіть зараз, щоб отримати останні можливості та виправлення + Не зараз + Оновити + Застаріла версія додатку + Налаштування аккаунту + Необхідне оновлення додатку + Ця версія додатка %1$s застаріла. Щоб продовжити навчання та отримати останні можливості та виправлення, будь ласка, оновіть до останньої версії. + Версія: %1$s + Оновлено + Натисніть, щоб оновити до версії %1$s + Натисніть, щоб встановити обов\'язкове оновлення додатку + %1$s зображення профілю diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 45e5092f4..975d96869 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -40,6 +40,19 @@ Customer Feedback MMMM dd dd MMM yyyy hh:mm aaa + App Update + We recommend that you update to the latest version. Upgrade now to receive the latest features and fixes. + New update available! Upgrade now to receive the latest features and fixes + Not Now + Update + Deprecated App Version + Account Settings + App Update Required + This version of the OpenEdX app is out-of-date. To continue learning and get the latest features and fixes, please upgrade to the latest version. + Version: %1$s + Up-to-date + Tap to update to version %1$s + Tap to install required app update %1$s profile image diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt index 2d82c3f5b..8077c537e 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/VideoUnitFragment.kt @@ -17,15 +17,11 @@ import androidx.compose.ui.Modifier import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.fragment.app.Fragment -import androidx.media3.cast.CastPlayer import androidx.media3.cast.SessionAvailabilityListener import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi -import androidx.media3.exoplayer.ExoPlayer import androidx.window.layout.WindowMetricsCalculator -import com.google.android.gms.cast.framework.CastContext import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf @@ -33,7 +29,7 @@ import org.openedx.core.extension.computeWindowSizeClasses import org.openedx.core.extension.dpToPixel import org.openedx.core.extension.objectToString import org.openedx.core.extension.stringToObject -import org.openedx.core.presentation.dialog.SelectBottomDialogFragment +import org.openedx.core.presentation.dialog.selectorbottomsheet.SelectBottomDialogFragment import org.openedx.core.presentation.global.viewBinding import org.openedx.core.ui.WindowSize import org.openedx.core.ui.theme.OpenEdXTheme @@ -45,7 +41,6 @@ import org.openedx.course.presentation.CourseRouter import org.openedx.course.presentation.ui.ConnectionErrorView import org.openedx.course.presentation.ui.VideoSubtitles import org.openedx.course.presentation.ui.VideoTitle -import java.util.concurrent.Executors import kotlin.math.roundToInt class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { diff --git a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt index 505128eac..6d440170c 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/video/YoutubeVideoUnitFragment.kt @@ -27,7 +27,7 @@ import org.koin.core.parameter.parametersOf import org.openedx.core.extension.computeWindowSizeClasses import org.openedx.core.extension.objectToString import org.openedx.core.extension.stringToObject -import org.openedx.core.presentation.dialog.SelectBottomDialogFragment +import org.openedx.core.presentation.dialog.selectorbottomsheet.SelectBottomDialogFragment import org.openedx.core.ui.WindowSize import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt index 33f5b280a..f7406bca0 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt @@ -43,11 +43,14 @@ import org.koin.android.ext.android.inject import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.UIMessage import org.openedx.core.domain.model.* +import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox +import org.openedx.core.system.notifier.AppUpgradeEvent import org.openedx.core.ui.* import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography +import org.openedx.core.AppUpdateState import org.openedx.core.utils.TimeUtils import org.openedx.dashboard.R import java.util.* @@ -75,6 +78,7 @@ class DashboardFragment : Fragment() { val uiMessage by viewModel.uiMessage.observeAsState() val refreshing by viewModel.updating.observeAsState(false) val canLoadMore by viewModel.canLoadMore.observeAsState(false) + val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState() MyCoursesScreen( windowSize = windowSize, @@ -99,7 +103,13 @@ class DashboardFragment : Fragment() { }, paginationCallback = { viewModel.fetchMore() - } + }, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters( + appUpgradeEvent = appUpgradeEvent, + onAppUpgradeRecommendedBoxClick = { + AppUpdateState.openPlayMarket(requireContext()) + }, + ), ) } } @@ -119,6 +129,7 @@ internal fun MyCoursesScreen( onSwipeRefresh: () -> Unit, paginationCallback: () -> Unit, onItemClick: (EnrolledCourse) -> Unit, + appUpgradeParameters: AppUpdateState.AppUpgradeParameters, ) { val scaffoldState = rememberScaffoldState() val pullRefreshState = @@ -294,19 +305,35 @@ internal fun MyCoursesScreen( pullRefreshState, Modifier.align(Alignment.TopCenter) ) - if (!isInternetConnectionShown && !hasInternetConnection) { - OfflineModeDialog( - Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), - onDismissCLick = { - isInternetConnectionShown = true - }, - onReloadClick = { - isInternetConnectionShown = true - onReloadClick() + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + ) { + when (appUpgradeParameters.appUpgradeEvent) { + is AppUpgradeEvent.UpgradeRecommendedEvent -> { + AppUpgradeRecommendedBox( + modifier = Modifier.fillMaxWidth(), + onClick = appUpgradeParameters.onAppUpgradeRecommendedBoxClick + ) } - ) + + else -> {} + } + + if (!isInternetConnectionShown && !hasInternetConnection) { + OfflineModeDialog( + Modifier + .fillMaxWidth(), + onDismissCLick = { + isInternetConnectionShown = true + }, + onReloadClick = { + isInternetConnectionShown = true + onReloadClick() + } + ) + } } } } @@ -494,7 +521,8 @@ private fun MyCoursesScreenDay() { hasInternetConnection = true, refreshing = false, canLoadMore = false, - paginationCallback = {} + paginationCallback = {}, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters() ) } } @@ -523,7 +551,8 @@ private fun MyCoursesScreenTabletPreview() { hasInternetConnection = true, refreshing = false, canLoadMore = false, - paginationCallback = {} + paginationCallback = {}, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters() ) } } diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt index 714d0c91e..9ff14d983 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt @@ -4,6 +4,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.R import org.openedx.core.SingleEventLiveData @@ -12,10 +13,11 @@ import org.openedx.core.domain.model.EnrolledCourse import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection +import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.AppUpgradeNotifier import org.openedx.core.system.notifier.CourseDashboardUpdate import org.openedx.core.system.notifier.CourseNotifier import org.openedx.dashboard.domain.interactor.DashboardInteractor -import kotlinx.coroutines.launch class DashboardViewModel( @@ -23,7 +25,8 @@ class DashboardViewModel( private val interactor: DashboardInteractor, private val resourceManager: ResourceManager, private val notifier: CourseNotifier, - private val analytics: DashboardAnalytics + private val analytics: DashboardAnalytics, + private val appUpgradeNotifier: AppUpgradeNotifier ) : BaseViewModel() { private val coursesList = mutableListOf() @@ -48,6 +51,10 @@ class DashboardViewModel( val canLoadMore: LiveData get() = _canLoadMore + private val _appUpgradeEvent = MutableLiveData() + val appUpgradeEvent: LiveData + get() = _appUpgradeEvent + override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) viewModelScope.launch { @@ -61,6 +68,7 @@ class DashboardViewModel( init { getCourses() + collectAppUpgradeEvent() } fun getCourses() { @@ -153,6 +161,14 @@ class DashboardViewModel( } } + private fun collectAppUpgradeEvent() { + viewModelScope.launch { + appUpgradeNotifier.notifier.collect { event -> + _appUpgradeEvent.value = event + } + } + } + fun dashboardCourseClickedEvent(courseId: String, courseName: String) { analytics.dashboardCourseClickedEvent(courseId, courseName) } diff --git a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt index 05624f98b..2ab9133d1 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt @@ -17,8 +17,10 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.* import org.junit.After @@ -27,6 +29,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule +import org.openedx.core.system.notifier.AppUpgradeNotifier import java.net.UnknownHostException @OptIn(ExperimentalCoroutinesApi::class) @@ -42,6 +45,7 @@ class DashboardViewModelTest { private val networkConnection = mockk() private val notifier = mockk() private val analytics = mockk() + private val appUpgradeNotifier = mockk() private val noInternet = "Slow or no internet connection" private val somethingWrong = "Something went wrong" @@ -56,6 +60,7 @@ class DashboardViewModelTest { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { appUpgradeNotifier.notifier } returns emptyFlow() } @After @@ -65,14 +70,14 @@ class DashboardViewModelTest { @Test fun `getCourses no internet connection`() = runTest { - val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics) + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException() - advanceUntilIdle() coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) @@ -81,14 +86,14 @@ class DashboardViewModelTest { @Test fun `getCourses unknown error`() = runTest { - val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics) + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } throws Exception() - advanceUntilIdle() coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) @@ -97,15 +102,15 @@ class DashboardViewModelTest { @Test fun `getCourses from network`() = runTest { - val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics) + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk()) - advanceUntilIdle() coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } + verify(exactly = 1) { appUpgradeNotifier.notifier } assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is DashboardUIState.Courses) @@ -113,7 +118,7 @@ class DashboardViewModelTest { @Test fun `getCourses from network with next page`() = runTest { - val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics) + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList.copy( Pagination( @@ -129,6 +134,7 @@ class DashboardViewModelTest { coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } + verify(exactly = 1) { appUpgradeNotifier.notifier } assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is DashboardUIState.Courses) @@ -138,13 +144,13 @@ class DashboardViewModelTest { fun `getCourses from cache`() = runTest { every { networkConnection.isOnline() } returns false coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk()) - - val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics) + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) advanceUntilIdle() coVerify(exactly = 0) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 1) { interactor.getEnrolledCoursesFromCache() } + verify(exactly = 1) { appUpgradeNotifier.notifier } assert(viewModel.uiMessage.value == null) assert(viewModel.uiState.value is DashboardUIState.Courses) @@ -154,7 +160,7 @@ class DashboardViewModelTest { fun `updateCourses no internet error`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics) + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException() viewModel.updateCourses() @@ -162,6 +168,7 @@ class DashboardViewModelTest { coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) @@ -173,8 +180,7 @@ class DashboardViewModelTest { fun `updateCourses unknown exception`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - - val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics) + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) coEvery { interactor.getEnrolledCourses(any()) } throws Exception() viewModel.updateCourses() @@ -182,6 +188,7 @@ class DashboardViewModelTest { coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) @@ -193,13 +200,14 @@ class DashboardViewModelTest { fun `updateCourses success`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics) + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) viewModel.updateCourses() advanceUntilIdle() coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } + verify(exactly = 1) { appUpgradeNotifier.notifier } assert(viewModel.uiMessage.value == null) assert(viewModel.updating.value == false) @@ -210,13 +218,14 @@ class DashboardViewModelTest { fun `updateCourses success with next page`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList.copy(Pagination(10,"2",2,"")) - val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics) + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) viewModel.updateCourses() advanceUntilIdle() coVerify(exactly = 2) { interactor.getEnrolledCourses(any()) } coVerify(exactly = 0) { interactor.getEnrolledCoursesFromCache() } + verify(exactly = 1) { appUpgradeNotifier.notifier } assert(viewModel.uiMessage.value == null) assert(viewModel.updating.value == false) @@ -226,8 +235,7 @@ class DashboardViewModelTest { @Test fun `CourseDashboardUpdate notifier test`() = runTest { coEvery { notifier.notifier } returns flow { emit(CourseDashboardUpdate()) } - - val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics) + val viewModel = DashboardViewModel(networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) @@ -237,7 +245,7 @@ class DashboardViewModelTest { advanceUntilIdle() coVerify(exactly = 1) { interactor.getEnrolledCourses(any()) } + verify(exactly = 1) { appUpgradeNotifier.notifier } } - } \ No newline at end of file diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryFragment.kt index 1dee5a0af..b229489a2 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryFragment.kt @@ -25,16 +25,21 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.fragment.app.Fragment +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.UIMessage import org.openedx.core.domain.model.Course import org.openedx.core.domain.model.Media +import org.openedx.core.presentation.dialog.appupgrade.AppUpgradeDialogFragment +import org.openedx.core.presentation.global.app_upgrade.AppUpgradeRecommendedBox +import org.openedx.core.system.notifier.AppUpgradeEvent import org.openedx.core.ui.* import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography +import org.openedx.core.AppUpdateState +import org.openedx.core.AppUpdateState.wasUpdateDialogClosed import org.openedx.discovery.R -import org.koin.android.ext.android.inject -import org.koin.androidx.viewmodel.ext.android.viewModel class DiscoveryFragment : Fragment() { @@ -55,6 +60,8 @@ class DiscoveryFragment : Fragment() { val uiMessage by viewModel.uiMessage.observeAsState() val canLoadMore by viewModel.canLoadMore.observeAsState(false) val refreshing by viewModel.isUpdating.observeAsState(false) + val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState() + val wasUpdateDialogClosed by remember { wasUpdateDialogClosed } DiscoveryScreen( windowSize = windowSize, @@ -63,6 +70,25 @@ class DiscoveryFragment : Fragment() { canLoadMore = canLoadMore, refreshing = refreshing, hasInternetConnection = viewModel.hasInternetConnection, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters( + appUpgradeEvent = appUpgradeEvent, + wasUpdateDialogClosed = wasUpdateDialogClosed, + appUpgradeRecommendedDialog = { + val dialog = AppUpgradeDialogFragment.newInstance() + dialog.show( + requireActivity().supportFragmentManager, + AppUpgradeDialogFragment::class.simpleName + ) + }, + onAppUpgradeRecommendedBoxClick = { + AppUpdateState.openPlayMarket(requireContext()) + }, + onAppUpgradeRequired = { + router.navigateToUpgradeRequired( + requireActivity().supportFragmentManager + ) + } + ), onSearchClick = { viewModel.discoverySearchBarClickedEvent() router.navigateToCourseSearch( @@ -100,6 +126,7 @@ internal fun DiscoveryScreen( canLoadMore: Boolean, refreshing: Boolean, hasInternetConnection: Boolean, + appUpgradeParameters: AppUpdateState.AppUpgradeParameters, onSearchClick: () -> Unit, onSwipeRefresh: () -> Unit, onReloadClick: () -> Unit, @@ -267,19 +294,50 @@ internal fun DiscoveryScreen( pullRefreshState, Modifier.align(Alignment.TopCenter) ) - if (!isInternetConnectionShown && !hasInternetConnection) { - OfflineModeDialog( - Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), - onDismissCLick = { - isInternetConnectionShown = true - }, - onReloadClick = { - isInternetConnectionShown = true - onReloadClick() + + Column( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + ) { + when (appUpgradeParameters.appUpgradeEvent) { + is AppUpgradeEvent.UpgradeRecommendedEvent -> { + if (appUpgradeParameters.wasUpdateDialogClosed) { + AppUpgradeRecommendedBox( + modifier = Modifier.fillMaxWidth(), + onClick = appUpgradeParameters.onAppUpgradeRecommendedBoxClick + ) + } else { + if (!AppUpdateState.wasUpdateDialogDisplayed) { + AppUpdateState.wasUpdateDialogDisplayed = true + appUpgradeParameters.appUpgradeRecommendedDialog() + } + } } - ) + + is AppUpgradeEvent.UpgradeRequiredEvent -> { + if (!AppUpdateState.wasUpdateDialogDisplayed) { + AppUpdateState.wasUpdateDialogDisplayed = true + appUpgradeParameters.onAppUpgradeRequired() + } + } + + else -> {} + } + + if (!isInternetConnectionShown && !hasInternetConnection) { + OfflineModeDialog( + Modifier + .fillMaxWidth(), + onDismissCLick = { + isInternetConnectionShown = true + }, + onReloadClick = { + isInternetConnectionShown = true + onReloadClick() + } + ) + } } } } @@ -327,7 +385,8 @@ private fun DiscoveryScreenPreview() { onReloadClick = {}, canLoadMore = false, refreshing = false, - hasInternetConnection = true + hasInternetConnection = true, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters() ) } } @@ -360,7 +419,8 @@ private fun DiscoveryScreenTabletPreview() { onReloadClick = {}, canLoadMore = false, refreshing = false, - hasInternetConnection = true + hasInternetConnection = true, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters() ) } } diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt index 12ee9ce93..408507d46 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryRouter.kt @@ -8,4 +8,6 @@ interface DiscoveryRouter { fun navigateToCourseSearch(fm: FragmentManager) + fun navigateToUpgradeRequired(fm: FragmentManager) + } \ No newline at end of file diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryViewModel.kt index a1352333e..7d602972b 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/DiscoveryViewModel.kt @@ -3,19 +3,27 @@ package org.openedx.discovery.presentation import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope -import org.openedx.core.* +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.launch +import org.openedx.core.BaseViewModel +import org.openedx.core.R +import org.openedx.core.SingleEventLiveData +import org.openedx.core.UIMessage import org.openedx.core.domain.model.Course import org.openedx.core.extension.isInternetError import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection +import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.AppUpgradeNotifier import org.openedx.discovery.domain.interactor.DiscoveryInteractor -import kotlinx.coroutines.launch class DiscoveryViewModel( private val networkConnection: NetworkConnection, private val interactor: DiscoveryInteractor, private val resourceManager: ResourceManager, - private val analytics: DiscoveryAnalytics + private val analytics: DiscoveryAnalytics, + private val appUpgradeNotifier: AppUpgradeNotifier ) : BaseViewModel() { private val _uiState = MutableLiveData(DiscoveryUIState.Loading) @@ -34,6 +42,10 @@ class DiscoveryViewModel( val isUpdating: LiveData get() = _isUpdating + private val _appUpgradeEvent = MutableLiveData() + val appUpgradeEvent: LiveData + get() = _appUpgradeEvent + val hasInternetConnection: Boolean get() = networkConnection.isOnline() @@ -43,6 +55,7 @@ class DiscoveryViewModel( init { getCoursesList() + collectAppUpgradeEvent() } private fun loadCoursesInternal( @@ -136,6 +149,25 @@ class DiscoveryViewModel( } } + @OptIn(FlowPreview::class) + private fun collectAppUpgradeEvent() { + viewModelScope.launch { + appUpgradeNotifier.notifier + .debounce(100) + .collect { event -> + when (event) { + is AppUpgradeEvent.UpgradeRecommendedEvent -> { + _appUpgradeEvent.value = event + } + + is AppUpgradeEvent.UpgradeRequiredEvent -> { + _appUpgradeEvent.value = AppUpgradeEvent.UpgradeRequiredEvent + } + } + } + } + } + fun discoverySearchBarClickedEvent() { analytics.discoverySearchBarClickedEvent() } diff --git a/discovery/src/test/java/org/openedx/discovery/presentation/DiscoveryViewModelTest.kt b/discovery/src/test/java/org/openedx/discovery/presentation/DiscoveryViewModelTest.kt index deb539ef8..dd12c6664 100644 --- a/discovery/src/test/java/org/openedx/discovery/presentation/DiscoveryViewModelTest.kt +++ b/discovery/src/test/java/org/openedx/discovery/presentation/DiscoveryViewModelTest.kt @@ -12,8 +12,10 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.* import org.junit.After import org.junit.Assert.assertEquals @@ -21,6 +23,7 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule +import org.openedx.core.system.notifier.AppUpgradeNotifier import java.net.UnknownHostException @OptIn(ExperimentalCoroutinesApi::class) @@ -36,6 +39,7 @@ class DiscoveryViewModelTest { private val interactor = mockk() private val networkConnection = mockk() private val analytics = mockk() + private val appUpgradeNotifier = mockk() private val noInternet = "Slow or no internet connection" private val somethingWrong = "Something went wrong" @@ -45,6 +49,7 @@ class DiscoveryViewModelTest { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { appUpgradeNotifier.notifier } returns emptyFlow() } @After @@ -54,14 +59,14 @@ class DiscoveryViewModelTest { @Test fun `getCoursesList no internet connection`() = runTest { - val viewModel = - DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics) + val viewModel = DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getCoursesList(any(), any(), any()) } throws UnknownHostException() advanceUntilIdle() coVerify(exactly = 1) { interactor.getCoursesList(any(), any(), any()) } coVerify(exactly = 0) { interactor.getCoursesListFromCache() } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) @@ -71,8 +76,7 @@ class DiscoveryViewModelTest { @Test fun `getCoursesList unknown exception`() = runTest { - val viewModel = - DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics) + val viewModel = DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getCoursesList(any(), any(), any()) } throws Exception() advanceUntilIdle() @@ -88,8 +92,7 @@ class DiscoveryViewModelTest { @Test fun `getCoursesList from cache`() = runTest { - val viewModel = - DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics) + val viewModel = DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns false coEvery { interactor.getCoursesListFromCache() } returns emptyList() advanceUntilIdle() @@ -104,8 +107,7 @@ class DiscoveryViewModelTest { @Test fun `getCoursesList from network with next page`() = runTest { - val viewModel = - DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics) + val viewModel = DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getCoursesList(any(), any(), any()) } returns CourseList( Pagination( @@ -127,8 +129,7 @@ class DiscoveryViewModelTest { @Test fun `getCoursesList from network without next page`() = runTest { - val viewModel = - DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics) + val viewModel = DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getCoursesList(any(), any(), any()) } returns CourseList( Pagination( @@ -151,10 +152,8 @@ class DiscoveryViewModelTest { @Test fun `updateData no internet connection`() = runTest { - val viewModel = - DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics) + val viewModel = DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true - coEvery { interactor.getCoursesList(any(), any(), any()) } throws UnknownHostException() viewModel.updateData() advanceUntilIdle() @@ -170,10 +169,8 @@ class DiscoveryViewModelTest { @Test fun `updateData unknown exception`() = runTest { - val viewModel = - DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics) + val viewModel = DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true - coEvery { interactor.getCoursesList(any(), any(), any()) } throws Exception() viewModel.updateData() advanceUntilIdle() @@ -189,10 +186,8 @@ class DiscoveryViewModelTest { @Test fun `updateData success with next page`() = runTest { - val viewModel = - DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics) + val viewModel = DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true - coEvery { interactor.getCoursesList(any(), any(), any()) } returns CourseList( Pagination( 10, @@ -214,10 +209,8 @@ class DiscoveryViewModelTest { @Test fun `updateData success without next page`() = runTest { - val viewModel = - DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics) + val viewModel = DiscoveryViewModel(networkConnection, interactor, resourceManager, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true - coEvery { interactor.getCoursesList(any(), any(), any()) } returns CourseList( Pagination( 10, diff --git a/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt b/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt index b8887a984..2b59c22d4 100644 --- a/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt +++ b/profile/src/main/java/org/openedx/profile/data/repository/ProfileRepository.kt @@ -1,14 +1,14 @@ package org.openedx.profile.data.repository import androidx.room.RoomDatabase -import org.openedx.core.ApiConstants -import org.openedx.profile.data.api.ProfileApi import okhttp3.MediaType.Companion.toMediaType import okhttp3.RequestBody.Companion.asRequestBody +import org.openedx.core.ApiConstants import org.openedx.core.data.storage.CorePreferences +import org.openedx.profile.data.api.ProfileApi import org.openedx.profile.data.storage.ProfilePreferences -import java.io.File import org.openedx.profile.domain.model.Account +import java.io.File class ProfileRepository( private val api: ProfileApi, @@ -28,7 +28,7 @@ class ProfileRepository( return account.mapToDomain() } - fun getCachedAccount() : Account? { + fun getCachedAccount(): Account? { return profilePreferences.profile?.mapToDomain() } @@ -52,12 +52,15 @@ class ProfileRepository( suspend fun deactivateAccount(password: String) = api.deactivateAccount(password) suspend fun logout() { - api.revokeAccessToken( - org.openedx.core.BuildConfig.CLIENT_ID, - corePreferences.refreshToken, - ApiConstants.TOKEN_TYPE_REFRESH - ) - corePreferences.clear() - room.clearAllTables() + try { + api.revokeAccessToken( + org.openedx.core.BuildConfig.CLIENT_ID, + corePreferences.refreshToken, + ApiConstants.TOKEN_TYPE_REFRESH + ) + } finally { + corePreferences.clear() + room.clearAllTables() + } } } \ No newline at end of file diff --git a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt index 1507fda36..bef45cd43 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileFragment.kt @@ -42,11 +42,13 @@ import org.openedx.core.UIMessage import org.openedx.core.domain.model.ProfileImage import org.openedx.core.presentation.global.AppData import org.openedx.core.presentation.global.AppDataHolder +import org.openedx.core.system.notifier.AppUpgradeEvent import org.openedx.core.ui.* import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography +import org.openedx.core.AppUpdateState import org.openedx.core.utils.EmailUtil import org.openedx.profile.domain.model.Account import org.openedx.profile.presentation.ProfileRouter @@ -77,6 +79,7 @@ class ProfileFragment : Fragment() { val logoutSuccess by viewModel.successLogout.observeAsState(false) val uiMessage by viewModel.uiMessage.observeAsState() val refreshing by viewModel.isUpdating.observeAsState(false) + val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState(null) ProfileScreen( windowSize = windowSize, @@ -84,6 +87,7 @@ class ProfileFragment : Fragment() { uiMessage = uiMessage, appData = (requireActivity() as AppDataHolder).appData, refreshing = refreshing, + appUpgradeEvent = appUpgradeEvent, logout = { viewModel.logout() }, @@ -109,6 +113,9 @@ class ProfileFragment : Fragment() { SupportClickAction.COOKIE_POLICY -> viewModel.cookiePolicyClickedEvent() SupportClickAction.PRIVACY_POLICY -> viewModel.privacyPolicyClickedEvent() } + }, + onAppVersionClick = { + AppUpdateState.openPlayMarket(requireContext()) } ) @@ -135,11 +142,13 @@ private fun ProfileScreen( appData: AppData, uiMessage: UIMessage?, refreshing: Boolean, + appUpgradeEvent: AppUpgradeEvent?, onVideoSettingsClick: () -> Unit, logout: () -> Unit, onSwipeRefresh: () -> Unit, onSupportClick: (SupportClickAction) -> Unit, - editAccountClicked: (Account) -> Unit + editAccountClicked: (Account) -> Unit, + onAppVersionClick: () -> Unit ) { val scaffoldState = rememberScaffoldState() var showLogoutDialog by rememberSaveable { mutableStateOf(false) } @@ -267,7 +276,12 @@ private fun ProfileScreen( Spacer(modifier = Modifier.height(24.dp)) - SupportInfoSection(appData, onClick = onSupportClick) + SupportInfoSection( + appData = appData, + onClick = onSupportClick, + appUpgradeEvent = appUpgradeEvent, + onAppVersionClick = onAppVersionClick + ) Spacer(modifier = Modifier.height(24.dp)) @@ -324,6 +338,8 @@ fun SettingsSection(onVideoSettingsClick: () -> Unit) { @Composable private fun SupportInfoSection( appData: AppData, + appUpgradeEvent: AppUpgradeEvent?, + onAppVersionClick: () -> Unit, onClick: (SupportClickAction) -> Unit ) { val uriHandler = LocalUriHandler.current @@ -374,6 +390,12 @@ private fun SupportInfoSection( uriHandler.openUri(context.getString(R.string.privacy_policy_link)) } ) + Divider(color = MaterialTheme.appColors.divider) + AppVersionItem( + appData = appData, + appUpgradeEvent = appUpgradeEvent, + onClick = onAppVersionClick + ) } } } @@ -517,6 +539,188 @@ private fun ProfileInfoItem(text: String, onClick: () -> Unit) { } } +@Composable +fun AppVersionItem( + appData: AppData, + appUpgradeEvent: AppUpgradeEvent?, + onClick: () -> Unit +) { + when (appUpgradeEvent) { + is AppUpgradeEvent.UpgradeRecommendedEvent -> { + AppVersionItemUpgradeRecommended( + versionName = appData.versionName, + appUpgradeEvent = appUpgradeEvent, + onClick = onClick + ) + } + + is AppUpgradeEvent.UpgradeRequiredEvent -> { + AppVersionItemUpgradeRequired( + versionName = appData.versionName, + onClick = onClick + ) + } + + else -> { + AppVersionItemAppToDate( + versionName = appData.versionName + ) + } + } +} + +@Composable +private fun AppVersionItemAppToDate(versionName: String) { + Column( + Modifier + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(id = R.string.core_version, versionName), + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textPrimary + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + modifier = Modifier.size((MaterialTheme.appTypography.labelLarge.fontSize.value + 4).dp), + painter = painterResource(id = R.drawable.core_ic_check), + contentDescription = null, + tint = MaterialTheme.appColors.accessGreen + ) + Text( + text = stringResource(id = R.string.core_up_to_date), + color = MaterialTheme.appColors.textSecondary, + style = MaterialTheme.appTypography.labelLarge + ) + } + } +} + +@Composable +fun AppVersionItemUpgradeRecommended( + versionName: String, + appUpgradeEvent: AppUpgradeEvent.UpgradeRecommendedEvent, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onClick() + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(id = R.string.core_version, versionName), + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textPrimary + ) + Text( + text = stringResource(id = R.string.core_tap_to_update_to_version, appUpgradeEvent.newVersionName), + color = MaterialTheme.appColors.textAccent, + style = MaterialTheme.appTypography.labelLarge + ) + } + Icon( + modifier = Modifier.size(28.dp), + painter = painterResource(id = R.drawable.core_ic_icon_upgrade), + tint = MaterialTheme.appColors.primary, + contentDescription = null + ) + } +} + +@Composable +fun AppVersionItemUpgradeRequired( + versionName: String, + onClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onClick() + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Image( + modifier = Modifier + .size((MaterialTheme.appTypography.labelLarge.fontSize.value + 8).dp), + painter = painterResource(id = R.drawable.core_ic_warning), + contentDescription = null + ) + Text( + text = stringResource(id = R.string.core_version, versionName), + style = MaterialTheme.appTypography.titleMedium, + color = MaterialTheme.appColors.textPrimary + ) + } + Text( + text = stringResource(id = R.string.core_tap_to_install_required_app_update), + color = MaterialTheme.appColors.textAccent, + style = MaterialTheme.appTypography.labelLarge + ) + } + Icon( + modifier = Modifier.size(28.dp), + painter = painterResource(id = R.drawable.core_ic_icon_upgrade), + tint = MaterialTheme.appColors.primary, + contentDescription = null + ) + } +} + +@Preview +@Composable +fun AppVersionItemAppToDatePreview() { + OpenEdXTheme { + AppVersionItem( + appData = AppData("1.0.0"), + appUpgradeEvent = null, + onClick = {} + ) + } +} + +@Preview +@Composable +fun AppVersionItemUpgradeRecommendedPreview() { + OpenEdXTheme { + AppVersionItem( + appData = AppData("1.0.0"), + appUpgradeEvent = AppUpgradeEvent.UpgradeRecommendedEvent("1.0.1"), + onClick = {} + ) + } +} + +@Preview +@Composable +fun AppVersionItemUpgradeRequiredPreview() { + OpenEdXTheme { + AppVersionItem( + appData = AppData("1.0.0"), + appUpgradeEvent = AppUpgradeEvent.UpgradeRequiredEvent, + onClick = {} + ) + } +} + @Preview @Composable fun LogoutDialogPreview() { @@ -540,7 +744,9 @@ private fun ProfileScreenPreview() { editAccountClicked = {}, onVideoSettingsClick = {}, onSupportClick = {}, - appData = AppData("1") + appData = AppData("1"), + appUpgradeEvent = null, + onAppVersionClick = {} ) } } @@ -561,7 +767,9 @@ private fun ProfileScreenTabletPreview() { editAccountClicked = {}, onVideoSettingsClick = {}, onSupportClick = {}, - appData = AppData("1") + appData = AppData("1"), + appUpgradeEvent = null, + onAppVersionClick = {} ) } } diff --git a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt index ee18ee038..7f8bc5405 100644 --- a/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt +++ b/profile/src/main/java/org/openedx/profile/presentation/profile/ProfileViewModel.kt @@ -14,6 +14,8 @@ import org.openedx.core.extension.isInternetError import org.openedx.core.module.DownloadWorkerController import org.openedx.core.system.AppCookieManager import org.openedx.core.system.ResourceManager +import org.openedx.core.system.notifier.AppUpgradeEvent +import org.openedx.core.system.notifier.AppUpgradeNotifier import org.openedx.profile.domain.interactor.ProfileInteractor import org.openedx.profile.presentation.ProfileAnalytics import org.openedx.profile.system.notifier.AccountDeactivated @@ -27,7 +29,8 @@ class ProfileViewModel( private val dispatcher: CoroutineDispatcher, private val cookieManager: AppCookieManager, private val workerController: DownloadWorkerController, - private val analytics: ProfileAnalytics + private val analytics: ProfileAnalytics, + private val appUpgradeNotifier: AppUpgradeNotifier ) : BaseViewModel() { private val _uiState = MutableLiveData(ProfileUIState.Loading) @@ -46,8 +49,13 @@ class ProfileViewModel( val isUpdating: LiveData get() = _isUpdating + private val _appUpgradeEvent = MutableLiveData() + val appUpgradeEvent: LiveData + get() = _appUpgradeEvent + init { getAccount() + collectAppUpgradeEvent() } override fun onCreate(owner: LifecycleOwner) { @@ -101,9 +109,6 @@ class ProfileViewModel( withContext(dispatcher) { interactor.logout() } - cookieManager.clearWebViewCookie() - analytics.logoutEvent(false) - _successLogout.value = true } catch (e: Exception) { if (e.isInternetError()) { _uiMessage.value = @@ -112,6 +117,18 @@ class ProfileViewModel( _uiMessage.value = UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_unknown_error)) } + } finally { + cookieManager.clearWebViewCookie() + analytics.logoutEvent(false) + _successLogout.value = true + } + } + } + + private fun collectAppUpgradeEvent() { + viewModelScope.launch { + appUpgradeNotifier.notifier.collect { event -> + _appUpgradeEvent.value = event } } } diff --git a/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt b/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt index 731adf2dd..6cdbbd5b1 100644 --- a/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt +++ b/profile/src/test/java/org/openedx/profile/presentation/profile/ProfileViewModelTest.kt @@ -20,6 +20,7 @@ import io.mockk.mockk import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.flow import kotlinx.coroutines.test.* import org.junit.After @@ -29,6 +30,7 @@ import org.junit.Rule import org.junit.Test import org.junit.rules.TestRule import org.openedx.core.domain.model.ProfileImage +import org.openedx.core.system.notifier.AppUpgradeNotifier import java.net.UnknownHostException @OptIn(ExperimentalCoroutinesApi::class) @@ -45,6 +47,7 @@ class ProfileViewModelTest { private val cookieManager = mockk() private val workerController = mockk() private val analytics = mockk() + private val appUpgradeNotifier = mockk() private val account = org.openedx.profile.domain.model.Account( username = "", @@ -73,6 +76,7 @@ class ProfileViewModelTest { Dispatchers.setMain(dispatcher) every { resourceManager.getString(R.string.core_error_no_connection) } returns noInternet every { resourceManager.getString(R.string.core_error_unknown_error) } returns somethingWrong + every { appUpgradeNotifier.notifier } returns emptyFlow() } @After @@ -89,13 +93,15 @@ class ProfileViewModelTest { dispatcher, cookieManager, workerController, - analytics + analytics, + appUpgradeNotifier ) coEvery { interactor.getCachedAccount() } returns null coEvery { interactor.getAccount() } throws UnknownHostException() advanceUntilIdle() coVerify(exactly = 1) { interactor.getAccount() } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assert(viewModel.uiState.value is ProfileUIState.Loading) @@ -111,13 +117,15 @@ class ProfileViewModelTest { dispatcher, cookieManager, workerController, - analytics + analytics, + appUpgradeNotifier ) coEvery { interactor.getCachedAccount() } returns account coEvery { interactor.getAccount() } throws UnknownHostException() advanceUntilIdle() coVerify(exactly = 1) { interactor.getAccount() } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assert(viewModel.uiState.value is ProfileUIState.Data) @@ -133,13 +141,15 @@ class ProfileViewModelTest { dispatcher, cookieManager, workerController, - analytics + analytics, + appUpgradeNotifier ) coEvery { interactor.getCachedAccount() } returns null coEvery { interactor.getAccount() } throws Exception() advanceUntilIdle() coVerify(exactly = 1) { interactor.getAccount() } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assert(viewModel.uiState.value is ProfileUIState.Loading) @@ -155,13 +165,15 @@ class ProfileViewModelTest { dispatcher, cookieManager, workerController, - analytics + analytics, + appUpgradeNotifier ) coEvery { interactor.getCachedAccount() } returns null coEvery { interactor.getAccount() } returns account advanceUntilIdle() coVerify(exactly = 1) { interactor.getAccount() } + verify(exactly = 1) { appUpgradeNotifier.notifier } assert(viewModel.uiState.value is ProfileUIState.Data) assert(viewModel.uiMessage.value == null) @@ -176,19 +188,22 @@ class ProfileViewModelTest { dispatcher, cookieManager, workerController, - analytics + analytics, + appUpgradeNotifier ) coEvery { interactor.logout() } throws UnknownHostException() coEvery { workerController.cancelWork() } returns Unit - + every { analytics.logoutEvent(false) } returns Unit + every { cookieManager.clearWebViewCookie() } returns Unit viewModel.logout() advanceUntilIdle() coVerify(exactly = 1) { interactor.logout() } + verify(exactly = 1) { appUpgradeNotifier.notifier } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(noInternet, message?.message) - assert(viewModel.successLogout.value == null) + assert(viewModel.successLogout.value == true) } @Test @@ -200,18 +215,24 @@ class ProfileViewModelTest { dispatcher, cookieManager, workerController, - analytics + analytics, + appUpgradeNotifier ) coEvery { interactor.logout() } throws Exception() coEvery { workerController.cancelWork() } returns Unit + every { analytics.logoutEvent(false) } returns Unit + every { cookieManager.clearWebViewCookie() } returns Unit viewModel.logout() advanceUntilIdle() coVerify(exactly = 1) { interactor.logout() } + verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { cookieManager.clearWebViewCookie() } + verify { analytics.logoutEvent(false) } val message = viewModel.uiMessage.value as? UIMessage.SnackBarMessage assertEquals(somethingWrong, message?.message) - assert(viewModel.successLogout.value == null) + assert(viewModel.successLogout.value == true) } @Test @@ -223,7 +244,8 @@ class ProfileViewModelTest { dispatcher, cookieManager, workerController, - analytics + analytics, + appUpgradeNotifier ) coEvery { interactor.getCachedAccount() } returns mockk() coEvery { interactor.getAccount() } returns mockk() @@ -235,6 +257,8 @@ class ProfileViewModelTest { advanceUntilIdle() coVerify(exactly = 1) { interactor.logout() } verify { analytics.logoutEvent(false) } + verify(exactly = 1) { appUpgradeNotifier.notifier } + verify(exactly = 1) { cookieManager.clearWebViewCookie() } assert(viewModel.uiMessage.value == null) assert(viewModel.successLogout.value == true) @@ -249,7 +273,8 @@ class ProfileViewModelTest { dispatcher, cookieManager, workerController, - analytics + analytics, + appUpgradeNotifier ) coEvery { interactor.getCachedAccount() } returns null every { notifier.notifier } returns flow { emit(AccountUpdated()) } @@ -261,6 +286,7 @@ class ProfileViewModelTest { advanceUntilIdle() coVerify(exactly = 2) { interactor.getAccount() } + verify(exactly = 1) { appUpgradeNotifier.notifier } } } \ No newline at end of file