diff --git a/app/src/main/java/org/openedx/app/AnalyticsManager.kt b/app/src/main/java/org/openedx/app/AnalyticsManager.kt index 993d30f5e..4f152677c 100644 --- a/app/src/main/java/org/openedx/app/AnalyticsManager.kt +++ b/app/src/main/java/org/openedx/app/AnalyticsManager.kt @@ -8,7 +8,7 @@ import org.openedx.app.analytics.FirebaseAnalytics import org.openedx.auth.presentation.AuthAnalytics import org.openedx.core.config.Config import org.openedx.course.presentation.CourseAnalytics -import org.openedx.dashboard.presentation.DashboardAnalytics +import org.openedx.dashboard.presentation.dashboard.DashboardAnalytics import org.openedx.discovery.presentation.DiscoveryAnalytics import org.openedx.discussion.presentation.DiscussionAnalytics import org.openedx.profile.presentation.ProfileAnalytics diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index b601aeaa9..15f7be5a0 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -25,6 +25,7 @@ import org.openedx.course.presentation.unit.container.CourseUnitContainerFragmen import org.openedx.course.presentation.unit.video.VideoFullScreenFragment import org.openedx.course.presentation.unit.video.YoutubeVideoFullScreenFragment import org.openedx.dashboard.presentation.DashboardRouter +import org.openedx.dashboard.presentation.program.ProgramFragment import org.openedx.discovery.presentation.DiscoveryRouter import org.openedx.discovery.presentation.NativeDiscoveryFragment import org.openedx.discovery.presentation.search.CourseSearchFragment @@ -46,7 +47,6 @@ import org.openedx.profile.presentation.settings.video.VideoQualityFragment import org.openedx.profile.presentation.settings.video.VideoSettingsFragment import org.openedx.whatsnew.WhatsNewRouter import org.openedx.whatsnew.presentation.whatsnew.WhatsNewFragment -import java.util.Date class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, DiscussionRouter, ProfileRouter, AppUpgradeRouter, WhatsNewRouter { @@ -131,6 +131,10 @@ class AppRouter : AuthRouter, DiscoveryRouter, DashboardRouter, CourseRouter, Di ) } + override fun navigateToProgramInfo(fm: FragmentManager, pathId: String) { + replaceFragmentWithBackStack(fm, ProgramFragment.newInstance(pathId)) + } + override fun navigateToNoAccess( fm: FragmentManager, title: String diff --git a/app/src/main/java/org/openedx/app/MainFragment.kt b/app/src/main/java/org/openedx/app/MainFragment.kt index 0d23b4cbd..c42c73857 100644 --- a/app/src/main/java/org/openedx/app/MainFragment.kt +++ b/app/src/main/java/org/openedx/app/MainFragment.kt @@ -3,16 +3,20 @@ package org.openedx.app import android.os.Bundle import android.view.View import androidx.core.os.bundleOf +import androidx.core.view.forEach import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResultListener +import androidx.lifecycle.lifecycleScope import androidx.viewpager2.widget.ViewPager2 +import kotlinx.coroutines.launch 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.dashboard.presentation.dashboard.DashboardFragment +import org.openedx.dashboard.presentation.program.ProgramFragment import org.openedx.discovery.presentation.DiscoveryNavigator import org.openedx.discovery.presentation.DiscoveryRouter import org.openedx.profile.presentation.profile.ProfileFragment @@ -28,6 +32,7 @@ class MainFragment : Fragment(R.layout.fragment_main) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + lifecycle.addObserver(viewModel) setFragmentResultListener(UpgradeRequiredFragment.REQUEST_KEY) { _, _ -> binding.bottomNavView.selectedItemId = R.id.fragmentProfile viewModel.enableBottomBar(false) @@ -68,6 +73,14 @@ class MainFragment : Fragment(R.layout.fragment_main) { enableBottomBar(isBottomBarEnabled) } + viewLifecycleOwner.lifecycleScope.launch { + viewModel.navigateToDiscovery.collect { shouldNavigateToDiscovery -> + if (shouldNavigateToDiscovery) { + binding.bottomNavView.selectedItemId = R.id.fragmentHome + } + } + } + requireArguments().apply { this.getString(ARG_COURSE_ID, null)?.apply { router.navigateToCourseDetail(parentFragmentManager, this) @@ -82,11 +95,16 @@ class MainFragment : Fragment(R.layout.fragment_main) { val discoveryFragment = DiscoveryNavigator(viewModel.isDiscoveryTypeWebView) .getDiscoveryFragment() + val programFragment = if (viewModel.isProgramTypeWebView) { + ProgramFragment(true) + } else { + InDevelopmentFragment() + } adapter = MainNavigationFragmentAdapter(this).apply { addFragment(discoveryFragment) addFragment(DashboardFragment()) - addFragment(InDevelopmentFragment()) + addFragment(programFragment) addFragment(ProfileFragment()) } binding.viewPager.adapter = adapter @@ -94,8 +112,8 @@ class MainFragment : Fragment(R.layout.fragment_main) { } private fun enableBottomBar(enable: Boolean) { - for (i in 0 until binding.bottomNavView.menu.size()) { - binding.bottomNavView.menu.getItem(i).isEnabled = enable + binding.bottomNavView.menu.forEach { + it.isEnabled = enable } } diff --git a/app/src/main/java/org/openedx/app/MainViewModel.kt b/app/src/main/java/org/openedx/app/MainViewModel.kt index b0f8d0a65..3b36cc2be 100644 --- a/app/src/main/java/org/openedx/app/MainViewModel.kt +++ b/app/src/main/java/org/openedx/app/MainViewModel.kt @@ -1,20 +1,46 @@ package org.openedx.app +import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.openedx.core.BaseViewModel import org.openedx.core.config.Config +import org.openedx.dashboard.notifier.DashboardEvent +import org.openedx.dashboard.notifier.DashboardNotifier class MainViewModel( - private val config: Config + private val config: Config, + private val notifier: DashboardNotifier, ) : BaseViewModel() { private val _isBottomBarEnabled = MutableLiveData(true) val isBottomBarEnabled: LiveData get() = _isBottomBarEnabled + private val _navigateToDiscovery = MutableSharedFlow() + val navigateToDiscovery: SharedFlow + get() = _navigateToDiscovery.asSharedFlow() + val isDiscoveryTypeWebView get() = config.getDiscoveryConfig().isViewTypeWebView() + val isProgramTypeWebView get() = config.getProgramConfig().isViewTypeWebView() + + override fun onCreate(owner: LifecycleOwner) { + super.onCreate(owner) + notifier.notifier.onEach { + if (it is DashboardEvent.NavigationToDiscovery) { + _navigateToDiscovery.emit(true) + } + }.distinctUntilChanged().launchIn(viewModelScope) + } + fun enableBottomBar(enable: Boolean) { _isBottomBarEnabled.value = enable } 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 860e98de0..9b326da18 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -26,6 +26,7 @@ import org.openedx.auth.presentation.sso.MicrosoftAuthHelper import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.data.storage.InAppReviewPreferences +import org.openedx.core.interfaces.EnrollInCourseInteractor import org.openedx.core.module.DownloadWorkerController import org.openedx.core.module.TranscriptManager import org.openedx.core.module.download.FileDownloader @@ -38,9 +39,11 @@ import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.AppUpgradeNotifier import org.openedx.core.system.notifier.CourseNotifier +import org.openedx.course.domain.interactor.CourseInteractor +import org.openedx.dashboard.notifier.DashboardNotifier import org.openedx.course.presentation.CourseAnalytics import org.openedx.course.presentation.CourseRouter -import org.openedx.dashboard.presentation.DashboardAnalytics +import org.openedx.dashboard.presentation.dashboard.DashboardAnalytics import org.openedx.dashboard.presentation.DashboardRouter import org.openedx.discovery.presentation.DiscoveryAnalytics import org.openedx.discovery.presentation.DiscoveryRouter @@ -75,6 +78,7 @@ val appModule = module { single { DiscussionNotifier() } single { ProfileNotifier() } single { AppUpgradeNotifier() } + single { DashboardNotifier() } single { AppRouter() } single { get() } @@ -153,4 +157,6 @@ val appModule = module { factory { FacebookAuthHelper() } factory { GoogleAuthHelper(get()) } factory { MicrosoftAuthHelper() } + + factory { CourseInteractor(get()) } } 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 6ebcacf64..ae4f748ff 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -29,7 +29,8 @@ import org.openedx.course.presentation.unit.video.VideoViewModel import org.openedx.course.presentation.videos.CourseVideoViewModel import org.openedx.dashboard.data.repository.DashboardRepository import org.openedx.dashboard.domain.interactor.DashboardInteractor -import org.openedx.dashboard.presentation.DashboardViewModel +import org.openedx.dashboard.presentation.dashboard.DashboardViewModel +import org.openedx.dashboard.presentation.program.ProgramViewModel import org.openedx.discovery.data.repository.DiscoveryRepository import org.openedx.discovery.domain.interactor.DiscoveryInteractor import org.openedx.discovery.presentation.NativeDiscoveryViewModel @@ -58,7 +59,7 @@ import org.openedx.whatsnew.presentation.whatsnew.WhatsNewViewModel val screenModule = module { viewModel { AppViewModel(get(), get(), get(), get(), get(named("IODispatcher")), get()) } - viewModel { MainViewModel(get()) } + viewModel { MainViewModel(get(), get()) } factory { AuthRepository(get(), get(), get()) } factory { AuthInteractor(get()) } @@ -85,7 +86,7 @@ val screenModule = module { factory { DashboardRepository(get(), get(), get()) } factory { DashboardInteractor(get()) } - viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModel { DashboardViewModel(get(), get(), get(), get(), get(), get(), get(), get()) } factory { DiscoveryRepository(get(), get()) } factory { DiscoveryInteractor(get()) } @@ -130,9 +131,10 @@ val screenModule = module { get() ) } - viewModel { (courseId: String) -> + viewModel { (courseId: String, courseTitle: String) -> CourseContainerViewModel( courseId, + courseTitle, get(), get(), get(), @@ -257,12 +259,7 @@ val screenModule = module { } viewModel { (courseId: String?) -> WhatsNewViewModel(courseId, get()) } - viewModel { - HtmlUnitViewModel( - get(), - get(), - get(), - get() - ) - } + viewModel { HtmlUnitViewModel(get(), get(), get(), get()) } + + viewModel { ProgramViewModel(get(), get(), get(), get(), get(), get(), get()) } } diff --git a/core/src/main/java/org/openedx/core/config/Config.kt b/core/src/main/java/org/openedx/core/config/Config.kt index 27aeee158..8739c4cfa 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -27,6 +27,10 @@ class Config(context: Context) { return getString(API_HOST_URL, "") } + fun getUriScheme(): String { + return getString(URI_SCHEME, "") + } + fun getOAuthClientId(): String { return getString(OAUTH_CLIENT_ID, "") } @@ -71,6 +75,10 @@ class Config(context: Context) { return getObjectOrNewInstance(DISCOVERY, DiscoveryConfig::class.java) } + fun getProgramConfig(): ProgramConfig { + return getObjectOrNewInstance(PROGRAM, ProgramConfig::class.java) + } + fun isWhatsNewEnabled(): Boolean { return getBoolean(WHATS_NEW_ENABLED, false) } @@ -131,6 +139,7 @@ class Config(context: Context) { companion object { private const val API_HOST_URL = "API_HOST_URL" + private const val URI_SCHEME = "URI_SCHEME" private const val OAUTH_CLIENT_ID = "OAUTH_CLIENT_ID" private const val TOKEN_TYPE = "TOKEN_TYPE" private const val FAQ_URL = "FAQ_URL" @@ -144,9 +153,15 @@ class Config(context: Context) { private const val MICROSOFT = "MICROSOFT" private const val PRE_LOGIN_EXPERIENCE_ENABLED = "PRE_LOGIN_EXPERIENCE_ENABLED" private const val DISCOVERY = "DISCOVERY" + private const val PROGRAM = "PROGRAM" private const val COURSE_NESTED_LIST_ENABLED = "COURSE_NESTED_LIST_ENABLED" private const val COURSE_BANNER_ENABLED = "COURSE_BANNER_ENABLED" private const val COURSE_TOP_TAB_BAR_ENABLED = "COURSE_TOP_TAB_BAR_ENABLED" private const val COURSE_UNIT_PROGRESS_ENABLED = "COURSE_UNIT_PROGRESS_ENABLED" } + + enum class ViewType { + NATIVE, + WEBVIEW + } } diff --git a/core/src/main/java/org/openedx/core/config/DiscoveryConfig.kt b/core/src/main/java/org/openedx/core/config/DiscoveryConfig.kt index 4d02e64c1..f94a9d753 100644 --- a/core/src/main/java/org/openedx/core/config/DiscoveryConfig.kt +++ b/core/src/main/java/org/openedx/core/config/DiscoveryConfig.kt @@ -4,18 +4,14 @@ import com.google.gson.annotations.SerializedName data class DiscoveryConfig( @SerializedName("TYPE") - private val viewType: String = Type.NATIVE.name, + private val viewType: String = Config.ViewType.NATIVE.name, @SerializedName("WEBVIEW") val webViewConfig: DiscoveryWebViewConfig = DiscoveryWebViewConfig(), ) { - enum class Type { - NATIVE, - WEBVIEW - } fun isViewTypeWebView(): Boolean { - return Type.WEBVIEW.name.equals(viewType, ignoreCase = true) + return Config.ViewType.WEBVIEW.name.equals(viewType, ignoreCase = true) } } @@ -23,9 +19,6 @@ data class DiscoveryWebViewConfig( @SerializedName("BASE_URL") val baseUrl: String = "", - @SerializedName("URI_SCHEME") - val uriScheme: String = "", - @SerializedName("COURSE_DETAIL_TEMPLATE") val courseUrlTemplate: String = "", diff --git a/core/src/main/java/org/openedx/core/config/ProgramConfig.kt b/core/src/main/java/org/openedx/core/config/ProgramConfig.kt new file mode 100644 index 000000000..55714dadc --- /dev/null +++ b/core/src/main/java/org/openedx/core/config/ProgramConfig.kt @@ -0,0 +1,21 @@ +package org.openedx.core.config + +import com.google.gson.annotations.SerializedName + +data class ProgramConfig( + @SerializedName("TYPE") + private val viewType: String = Config.ViewType.NATIVE.name, + @SerializedName("WEBVIEW") + val webViewConfig: ProgramWebViewConfig = ProgramWebViewConfig(), +){ + fun isViewTypeWebView(): Boolean { + return Config.ViewType.WEBVIEW.name.equals(viewType, ignoreCase = true) + } +} + +data class ProgramWebViewConfig( + @SerializedName("PROGRAM_URL") + val programUrl: String = "", + @SerializedName("PROGRAM_DETAIL_URL_TEMPLATE") + val programDetailUrlTemplate: String = "", +) diff --git a/core/src/main/java/org/openedx/core/extension/ViewExt.kt b/core/src/main/java/org/openedx/core/extension/ViewExt.kt index cc63687c9..ff2e95d47 100644 --- a/core/src/main/java/org/openedx/core/extension/ViewExt.kt +++ b/core/src/main/java/org/openedx/core/extension/ViewExt.kt @@ -6,6 +6,7 @@ import android.graphics.Rect import android.util.DisplayMetrics import android.view.View import android.view.ViewGroup +import android.widget.Toast import androidx.fragment.app.DialogFragment fun Context.dpToPixel(dp: Int): Float { @@ -40,4 +41,8 @@ fun DialogFragment.setWidthPercent(percentage: Int) { val rect = dm.run { Rect(0, 0, widthPixels, heightPixels) } val percentWidth = rect.width() * percent dialog?.window?.setLayout(percentWidth.toInt(), ViewGroup.LayoutParams.WRAP_CONTENT) -} \ No newline at end of file +} + +fun Context.toastMessage(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() +} diff --git a/core/src/main/java/org/openedx/core/interfaces/EnrollInCourseInteractor.kt b/core/src/main/java/org/openedx/core/interfaces/EnrollInCourseInteractor.kt new file mode 100644 index 000000000..5c82de1f5 --- /dev/null +++ b/core/src/main/java/org/openedx/core/interfaces/EnrollInCourseInteractor.kt @@ -0,0 +1,5 @@ +package org.openedx.core.interfaces + +interface EnrollInCourseInteractor { + suspend fun enrollInACourse(id: String) +} diff --git a/core/src/main/java/org/openedx/core/presentation/catalog/CatalogWebView.kt b/core/src/main/java/org/openedx/core/presentation/catalog/CatalogWebView.kt index 446448ac7..021185ca3 100644 --- a/core/src/main/java/org/openedx/core/presentation/catalog/CatalogWebView.kt +++ b/core/src/main/java/org/openedx/core/presentation/catalog/CatalogWebView.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import org.openedx.core.system.DefaultWebViewClient +import org.openedx.core.presentation.catalog.WebViewLink.Authority as linkAuthority @SuppressLint("SetJavaScriptEnabled", "ComposableNaming") @Composable @@ -16,10 +17,8 @@ fun CatalogWebViewScreen( isAllLinksExternal: Boolean = false, onWebPageLoaded: () -> Unit, refreshSessionCookie: () -> Unit = {}, - openExternalLink: (String) -> Unit, onWebPageUpdated: (String) -> Unit = {}, - onEnrollClick: (String) -> Unit = {}, - onInfoCardClicked: (String, String) -> Unit = { _, _ -> }, + onUriClick: (String, linkAuthority) -> Unit, ): WebView { val context = LocalContext.current @@ -29,7 +28,7 @@ fun CatalogWebViewScreen( context = context, webView = this@apply, isAllLinksExternal = isAllLinksExternal, - openExternalLink = openExternalLink, + onUriClick = onUriClick, refreshSessionCookie = refreshSessionCookie, ) { override fun onPageFinished(view: WebView?, url: String?) { @@ -58,16 +57,23 @@ fun CatalogWebViewScreen( val link = WebViewLink.parse(clickUrl, uriScheme) ?: return false return when (link.authority) { - WebViewLink.Authority.COURSE_INFO, - WebViewLink.Authority.PROGRAM_INFO -> { + linkAuthority.COURSE_INFO, + linkAuthority.PROGRAM_INFO, + linkAuthority.ENROLLED_PROGRAM_INFO -> { val pathId = link.params[WebViewLink.Param.PATH_ID] ?: "" - onInfoCardClicked(pathId, link.authority.name) + onUriClick(pathId, link.authority) true } - WebViewLink.Authority.ENROLL -> { - val courseId = link.params[WebViewLink.Param.COURSE_ID] - courseId?.let { onEnrollClick(it) } + linkAuthority.ENROLL, + linkAuthority.ENROLLED_COURSE_INFO -> { + val courseId = link.params[WebViewLink.Param.COURSE_ID] ?: "" + onUriClick(courseId, link.authority) + true + } + + linkAuthority.COURSE -> { + onUriClick("", link.authority) true } diff --git a/core/src/main/java/org/openedx/core/presentation/catalog/WebViewLink.kt b/core/src/main/java/org/openedx/core/presentation/catalog/WebViewLink.kt index 680fe00d3..f066a3ae8 100644 --- a/core/src/main/java/org/openedx/core/presentation/catalog/WebViewLink.kt +++ b/core/src/main/java/org/openedx/core/presentation/catalog/WebViewLink.kt @@ -16,7 +16,8 @@ class WebViewLink( ENROLL("enroll"), ENROLLED_PROGRAM_INFO("enrolled_program_info"), ENROLLED_COURSE_INFO("enrolled_course_info"), - COURSE("course") + COURSE("course"), + EXTERNAL("external"), } object Param { diff --git a/core/src/main/java/org/openedx/core/system/DefaultWebViewClient.kt b/core/src/main/java/org/openedx/core/system/DefaultWebViewClient.kt index 67e6df7ea..1273685c0 100644 --- a/core/src/main/java/org/openedx/core/system/DefaultWebViewClient.kt +++ b/core/src/main/java/org/openedx/core/system/DefaultWebViewClient.kt @@ -8,6 +8,7 @@ import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient import org.openedx.core.extension.isEmailValid +import org.openedx.core.presentation.catalog.WebViewLink import org.openedx.core.utils.EmailUtil open class DefaultWebViewClient( @@ -15,7 +16,7 @@ open class DefaultWebViewClient( val webView: WebView, val isAllLinksExternal: Boolean, val refreshSessionCookie: () -> Unit, - val openExternalLink: (String) -> Unit, + val onUriClick: (String, WebViewLink.Authority) -> Unit, ) : WebViewClient() { private var hostForThisPage: String? = null @@ -31,8 +32,8 @@ open class DefaultWebViewClient( override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { val clickUrl = request?.url?.toString() ?: "" - if (isAllLinksExternal || isExternalLink(clickUrl)) { - openExternalLink(clickUrl) + if (clickUrl.isNotEmpty() && (isAllLinksExternal || isExternalLink(clickUrl))) { + onUriClick(clickUrl, WebViewLink.Authority.EXTERNAL) return true } diff --git a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt index 6ffb2274a..73a38d61d 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -1,5 +1,6 @@ package org.openedx.core.ui +import android.content.Context import android.os.Build.VERSION.SDK_INT import android.widget.Toast import androidx.compose.foundation.BorderStroke @@ -97,6 +98,7 @@ import org.openedx.core.UIMessage import org.openedx.core.domain.model.Course import org.openedx.core.domain.model.RegistrationField import org.openedx.core.extension.LinkedImageText +import org.openedx.core.extension.toastMessage import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes import org.openedx.core.ui.theme.appTypography @@ -141,40 +143,19 @@ fun StaticSearchBar( @Composable fun Toolbar( - modifier: Modifier = Modifier, - label: String -) { - Box( - modifier = modifier - .fillMaxWidth() - .height(48.dp), - ) { - Text( - modifier = Modifier - .align(Alignment.Center) - .fillMaxWidth(), - text = label, - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - ) - } -} - -@Composable -fun ToolbarWithBackBtn( modifier: Modifier = Modifier, label: String, - onBackClick: () -> Unit + canShowBackBtn: Boolean = false, + onBackClick: () -> Unit = {} ) { Box( modifier = modifier .fillMaxWidth() .height(48.dp), ) { - BackBtn(onBackClick = onBackClick) + if (canShowBackBtn) { + BackBtn(onBackClick = onBackClick) + } Text( modifier = Modifier @@ -385,8 +366,7 @@ fun HandleUIMessage( } is UIMessage.ToastMessage -> { - val message = uiMessage.message - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + context.toastMessage(uiMessage.message) } else -> {} @@ -1217,6 +1197,20 @@ private fun SearchBarPreview() { ) } +@Preview +@Composable +private fun ToolbarPreview() { + Toolbar( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.appColors.background) + .height(48.dp), + label = "Toolbar", + canShowBackBtn = true, + onBackClick = {} + ) +} + @Preview @Composable private fun AuthButtonsPanelPreview() { diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 71a7ebfcb..c116cbc48 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -72,6 +72,9 @@ Continue Leaving the app You are now leaving the %s app and opening a browser. + Enrollment Error + We are unable to enroll you in this course at this time using the %s mobile application. Please try again on your web browser. + Completed diff --git a/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt b/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt index 8bcd2c40a..e7fdc48d7 100644 --- a/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt +++ b/course/src/main/java/org/openedx/course/domain/interactor/CourseInteractor.kt @@ -3,17 +3,18 @@ package org.openedx.course.domain.interactor import org.openedx.core.BlockType import org.openedx.core.domain.model.Block import org.openedx.core.domain.model.CourseStructure +import org.openedx.core.interfaces.EnrollInCourseInteractor import org.openedx.course.data.repository.CourseRepository class CourseInteractor( private val repository: CourseRepository -) { +) : EnrollInCourseInteractor { suspend fun getCourseDetails(id: String) = repository.getCourseDetail(id) suspend fun getCourseDetailsFromCache(id: String) = repository.getCourseDetailFromCache(id) - suspend fun enrollInACourse(id: String) { + override suspend fun enrollInACourse(id: String) { repository.enrollInACourse(courseId = id) } diff --git a/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt b/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt index e4758e220..05a4f63a6 100644 --- a/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt +++ b/course/src/main/java/org/openedx/course/presentation/CourseRouter.kt @@ -3,7 +3,6 @@ package org.openedx.course.presentation import androidx.fragment.app.FragmentManager import org.openedx.core.presentation.course.CourseViewMode import org.openedx.course.presentation.handouts.HandoutsType -import java.util.Date interface CourseRouter { diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt index b461116f1..37e061722 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerFragment.kt @@ -26,20 +26,17 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { private val binding by viewBinding(FragmentCourseContainerBinding::bind) private val viewModel by viewModel { - parametersOf(requireArguments().getString(ARG_COURSE_ID, "")) + parametersOf( + requireArguments().getString(ARG_COURSE_ID, ""), + requireArguments().getString(ARG_TITLE, "") + ) } private val router by inject() - private var adapter: CourseContainerAdapter? = null - private var courseTitle = "" - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - with(requireArguments()) { - courseTitle = getString(ARG_TITLE, "") - } viewModel.preloadCourseStructure() } @@ -47,16 +44,7 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - - binding.toolbar.setContent { - CourseToolbar( - title = courseTitle, - onBackClick = { - requireActivity().supportFragmentManager.popBackStack() - } - ) - } - + setupToolbar(viewModel.courseName) observe() } @@ -68,11 +56,12 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { private fun observe() { viewModel.dataReady.observe(viewLifecycleOwner) { isReady -> if (isReady == true) { + setupToolbar(viewModel.courseName) initViewPager() } else { router.navigateToNoAccess( requireActivity().supportFragmentManager, - courseTitle + viewModel.courseName ) } } @@ -89,14 +78,25 @@ class CourseContainerFragment : Fragment(R.layout.fragment_course_container) { } } + private fun setupToolbar(courseName: String) { + binding.toolbar.setContent { + CourseToolbar( + title = courseName, + onBackClick = { + requireActivity().supportFragmentManager.popBackStack() + } + ) + } + } + private fun initViewPager() { binding.viewPager.isVisible = true binding.viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL adapter = CourseContainerAdapter(this).apply { - addFragment(CourseOutlineFragment.newInstance(viewModel.courseId, courseTitle)) - addFragment(CourseVideosFragment.newInstance(viewModel.courseId, courseTitle)) - addFragment(DiscussionTopicsFragment.newInstance(viewModel.courseId, courseTitle)) - addFragment(CourseDatesFragment.newInstance(viewModel.courseId, courseTitle)) + addFragment(CourseOutlineFragment.newInstance(viewModel.courseId, viewModel.courseName)) + addFragment(CourseVideosFragment.newInstance(viewModel.courseId, viewModel.courseName)) + addFragment(DiscussionTopicsFragment.newInstance(viewModel.courseId, viewModel.courseName)) + addFragment(CourseDatesFragment.newInstance(viewModel.courseId, viewModel.courseName)) addFragment(HandoutsFragment.newInstance(viewModel.courseId)) } binding.viewPager.offscreenPageLimit = adapter?.itemCount ?: 1 diff --git a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt index f627b09f3..393237fd2 100644 --- a/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/container/CourseContainerViewModel.kt @@ -7,21 +7,21 @@ import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.R import org.openedx.core.SingleEventLiveData +import org.openedx.core.config.Config import org.openedx.core.exception.NoCachedDataException 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.CourseCompletionSet import org.openedx.core.system.notifier.CourseNotifier import org.openedx.core.system.notifier.CourseStructureUpdated import org.openedx.course.domain.interactor.CourseInteractor import org.openedx.course.presentation.CourseAnalytics import java.util.Date -import kotlinx.coroutines.launch -import org.openedx.core.config.Config -import org.openedx.core.system.notifier.CourseCompletionSet class CourseContainerViewModel( val courseId: String, + var courseName: String, private val config: Config, private val interactor: CourseInteractor, private val resourceManager: ResourceManager, @@ -44,8 +44,6 @@ class CourseContainerViewModel( val showProgress: LiveData get() = _showProgress - private var courseName = "" - init { viewModelScope.launch { notifier.notifier.collect { event -> diff --git a/course/src/main/java/org/openedx/course/presentation/info/CourseInfoFragment.kt b/course/src/main/java/org/openedx/course/presentation/info/CourseInfoFragment.kt index e9a0bc391..0f84d2143 100644 --- a/course/src/main/java/org/openedx/course/presentation/info/CourseInfoFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/info/CourseInfoFragment.kt @@ -42,12 +42,11 @@ import androidx.fragment.app.Fragment import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.UIMessage import org.openedx.core.presentation.catalog.CatalogWebViewScreen -import org.openedx.core.presentation.catalog.WebViewLink.Authority.COURSE_INFO import org.openedx.core.presentation.dialog.alert.ActionDialogFragment import org.openedx.core.presentation.dialog.alert.InfoDialogFragment import org.openedx.core.ui.ConnectionErrorView import org.openedx.core.ui.HandleUIMessage -import org.openedx.core.ui.ToolbarWithBackBtn +import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType import org.openedx.core.ui.displayCutoutForLandscape @@ -58,6 +57,7 @@ import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.windowSizeValue import org.openedx.course.R import org.openedx.core.R as CoreR +import org.openedx.core.presentation.catalog.WebViewLink.Authority as linkAuthority class CourseInfoFragment : Fragment() { @@ -82,9 +82,9 @@ class CourseInfoFragment : Fragment() { LaunchedEffect(showAlert) { if (showAlert) { InfoDialogFragment.newInstance( - title = context.getString(R.string.course_enrollment_error), + title = context.getString(CoreR.string.course_enrollment_error), message = context.getString( - R.string.course_enrollment_error_message, + CoreR.string.course_enrollment_error_message, getString(CoreR.string.platform_name) ) ).show( @@ -107,7 +107,7 @@ class CourseInfoFragment : Fragment() { windowSize = windowSize, uiMessage = uiMessage, contentUrl = getInitialUrl(), - uriScheme = viewModel.webViewConfig.uriScheme, + uriScheme = viewModel.uriScheme, hasInternetConnection = hasInternetConnection, checkInternetConnection = { hasInternetConnection = viewModel.hasInternetConnection @@ -115,29 +115,38 @@ class CourseInfoFragment : Fragment() { onBackClick = { requireActivity().supportFragmentManager.popBackStackImmediate() }, - onEnrollClick = { courseId -> - viewModel.enrollInACourse(courseId) - }, - openExternalLink = { url -> - ActionDialogFragment.newInstance( - title = getString(CoreR.string.core_leaving_the_app), - message = getString( - CoreR.string.core_leaving_the_app_message, - getString(CoreR.string.platform_name) - ), - url = url, - ).show( - requireActivity().supportFragmentManager, - ActionDialogFragment::class.simpleName - ) - }, - onInfoCardClicked = { pathId, infoType -> - viewModel.infoCardClicked( - fragmentManager = requireActivity().supportFragmentManager, - pathId = pathId, - infoType = infoType - ) - }, + onUriClick = { param, type -> + when (type) { + linkAuthority.PROGRAM_INFO, + linkAuthority.COURSE_INFO -> { + viewModel.infoCardClicked( + fragmentManager = requireActivity().supportFragmentManager, + pathId = param, + infoType = type.name + ) + } + + linkAuthority.EXTERNAL -> { + ActionDialogFragment.newInstance( + title = getString(CoreR.string.core_leaving_the_app), + message = getString( + CoreR.string.core_leaving_the_app_message, + getString(CoreR.string.platform_name) + ), + url = param, + ).show( + requireActivity().supportFragmentManager, + ActionDialogFragment::class.simpleName + ) + } + + linkAuthority.ENROLL -> { + viewModel.enrollInACourse(param) + } + + else -> {} + } + } ) } } @@ -146,7 +155,7 @@ class CourseInfoFragment : Fragment() { private fun getInitialUrl(): String { return arguments?.let { args -> val pathId = args.getString(ARG_PATH_ID) ?: "" - val urlTemplate = if (args.getString(ARG_INFO_TYPE) == COURSE_INFO.name) { + val urlTemplate = if (args.getString(ARG_INFO_TYPE) == linkAuthority.COURSE_INFO.name) { viewModel.webViewConfig.courseUrlTemplate } else { viewModel.webViewConfig.programUrlTemplate @@ -182,9 +191,7 @@ private fun CourseInfoScreen( hasInternetConnection: Boolean, checkInternetConnection: () -> Unit, onBackClick: () -> Unit, - onEnrollClick: (String) -> Unit, - onInfoCardClicked: (String, String) -> Unit, - openExternalLink: (String) -> Unit + onUriClick: (String, linkAuthority) -> Unit, ) { val scaffoldState = rememberScaffoldState() val configuration = LocalConfiguration.current @@ -218,8 +225,9 @@ private fun CourseInfoScreen( .displayCutoutForLandscape(), horizontalAlignment = Alignment.CenterHorizontally, ) { - ToolbarWithBackBtn( + Toolbar( label = stringResource(id = R.string.course_discover), + canShowBackBtn = true, onBackClick = onBackClick ) @@ -235,9 +243,7 @@ private fun CourseInfoScreen( contentUrl = contentUrl, uriScheme = uriScheme, onWebPageLoaded = { isLoading = false }, - onEnrollClick = onEnrollClick, - onInfoCardClicked = onInfoCardClicked, - openExternalLink = openExternalLink, + onUriClick = onUriClick, ) } else { ConnectionErrorView( @@ -271,9 +277,7 @@ private fun CourseInfoWebView( contentUrl: String, uriScheme: String, onWebPageLoaded: () -> Unit, - onEnrollClick: (String) -> Unit, - onInfoCardClicked: (String, String) -> Unit, - openExternalLink: (String) -> Unit + onUriClick: (String, linkAuthority) -> Unit, ) { val webView = CatalogWebViewScreen( @@ -281,9 +285,7 @@ private fun CourseInfoWebView( uriScheme = uriScheme, isAllLinksExternal = true, onWebPageLoaded = onWebPageLoaded, - openExternalLink = openExternalLink, - onEnrollClick = onEnrollClick, - onInfoCardClicked = onInfoCardClicked, + onUriClick = onUriClick, ) AndroidView( @@ -311,9 +313,7 @@ fun CourseInfoScreenPreview() { hasInternetConnection = false, checkInternetConnection = {}, onBackClick = {}, - onEnrollClick = {}, - onInfoCardClicked = { _, _ -> }, - openExternalLink = {} + onUriClick = { _, _ -> }, ) } } diff --git a/course/src/main/java/org/openedx/course/presentation/info/CourseInfoViewModel.kt b/course/src/main/java/org/openedx/course/presentation/info/CourseInfoViewModel.kt index bfae0b277..a70411049 100644 --- a/course/src/main/java/org/openedx/course/presentation/info/CourseInfoViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/info/CourseInfoViewModel.kt @@ -43,6 +43,8 @@ class CourseInfoViewModel( val hasInternetConnection: Boolean get() = networkConnection.isOnline() + val uriScheme: String get() = config.getUriScheme() + val webViewConfig get() = config.getDiscoveryConfig().webViewConfig fun enrollInACourse(courseId: String) { @@ -80,7 +82,7 @@ class CourseInfoViewModel( router.navigateToCourseOutline( fm = fragmentManager, courseId = courseId, - courseTitle = "", + courseTitle = "" ) } } diff --git a/course/src/main/res/values/strings.xml b/course/src/main/res/values/strings.xml index cf64be401..32a6875fa 100644 --- a/course/src/main/res/values/strings.xml +++ b/course/src/main/res/values/strings.xml @@ -50,9 +50,6 @@ Dates You are already enrolled in this course. Discover - Enrollment Error - We are unable to enroll you in this course at this time using the %s mobile application. Please try again on your web browser. - Course dates are not currently available. diff --git a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt index 03695856d..06c6ebb3d 100644 --- a/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt +++ b/course/src/test/java/org/openedx/course/presentation/container/CourseContainerViewModelTest.kt @@ -81,6 +81,7 @@ class CourseContainerViewModelTest { @Test fun `preloadCourseStructure internet connection exception`() = runTest { val viewModel = CourseContainerViewModel( + "", "", config, interactor, @@ -105,6 +106,7 @@ class CourseContainerViewModelTest { @Test fun `preloadCourseStructure unknown exception`() = runTest { val viewModel = CourseContainerViewModel( + "", "", config, interactor, @@ -129,6 +131,7 @@ class CourseContainerViewModelTest { @Test fun `preloadCourseStructure success with internet`() = runTest { val viewModel = CourseContainerViewModel( + "", "", config, interactor, @@ -153,6 +156,7 @@ class CourseContainerViewModelTest { @Test fun `preloadCourseStructure success without internet`() = runTest { val viewModel = CourseContainerViewModel( + "", "", config, interactor, @@ -178,6 +182,7 @@ class CourseContainerViewModelTest { @Test fun `updateData no internet connection exception`() = runTest { val viewModel = CourseContainerViewModel( + "", "", config, interactor, @@ -201,6 +206,7 @@ class CourseContainerViewModelTest { @Test fun `updateData unknown exception`() = runTest { val viewModel = CourseContainerViewModel( + "", "", config, interactor, @@ -224,6 +230,7 @@ class CourseContainerViewModelTest { @Test fun `updateData success`() = runTest { val viewModel = CourseContainerViewModel( + "", "", config, interactor, diff --git a/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt b/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt index cf6c3a845..9e83411f5 100644 --- a/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt +++ b/dashboard/src/androidTest/java/org/openedx/dashboard/presentation/MyCoursesScreenTest.kt @@ -2,13 +2,25 @@ package org.openedx.dashboard.presentation import androidx.activity.ComponentActivity import androidx.compose.ui.semantics.ProgressBarRangeInfo -import androidx.compose.ui.test.* +import androidx.compose.ui.test.assertAny +import androidx.compose.ui.test.hasAnyChild +import androidx.compose.ui.test.hasProgressBarRangeInfo +import androidx.compose.ui.test.hasScrollAction +import androidx.compose.ui.test.hasText import androidx.compose.ui.test.junit4.createAndroidComposeRule -import org.openedx.core.domain.model.* -import org.openedx.core.ui.WindowSize -import org.openedx.core.ui.WindowType +import androidx.compose.ui.test.onChildren import org.junit.Rule import org.junit.Test +import org.openedx.core.AppUpdateState +import org.openedx.core.domain.model.Certificate +import org.openedx.core.domain.model.CourseSharingUtmParameters +import org.openedx.core.domain.model.CoursewareAccess +import org.openedx.core.domain.model.EnrolledCourse +import org.openedx.core.domain.model.EnrolledCourseData +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.dashboard.presentation.dashboard.DashboardUIState +import org.openedx.dashboard.presentation.dashboard.MyCoursesScreen import java.util.Date class MyCoursesScreenTest { @@ -60,15 +72,17 @@ class MyCoursesScreenTest { composeTestRule.setContent { MyCoursesScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - DashboardUIState.Loading, - null, + apiHostUrl = "http://localhost:8000", + state = DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)), + uiMessage = null, refreshing = false, canLoadMore = false, hasInternetConnection = true, onReloadClick = {}, onSwipeRefresh = {}, paginationCallback = {}, - onItemClick = {} + onItemClick = {}, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters(), ) } @@ -90,15 +104,17 @@ class MyCoursesScreenTest { composeTestRule.setContent { MyCoursesScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)), - null, + apiHostUrl = "http://localhost:8000", + state = DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)), + uiMessage = null, refreshing = false, canLoadMore = false, hasInternetConnection = true, onReloadClick = {}, onSwipeRefresh = {}, paginationCallback = {}, - onItemClick = {} + onItemClick = {}, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters(), ) } @@ -113,15 +129,17 @@ class MyCoursesScreenTest { composeTestRule.setContent { MyCoursesScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)), - null, + apiHostUrl = "http://localhost:8000", + state = DashboardUIState.Courses(listOf(mockCourseEnrolled, mockCourseEnrolled)), + uiMessage = null, refreshing = true, canLoadMore = false, hasInternetConnection = true, onReloadClick = {}, onSwipeRefresh = {}, paginationCallback = {}, - onItemClick = {} + onItemClick = {}, + appUpgradeParameters = AppUpdateState.AppUpgradeParameters(), ) } diff --git a/dashboard/src/main/java/org/openedx/dashboard/notifier/DashboardEvent.kt b/dashboard/src/main/java/org/openedx/dashboard/notifier/DashboardEvent.kt new file mode 100644 index 000000000..db6532218 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/dashboard/notifier/DashboardEvent.kt @@ -0,0 +1,6 @@ +package org.openedx.dashboard.notifier + +sealed class DashboardEvent { + object NavigationToDiscovery : DashboardEvent() + object UpdateEnrolledCourses : DashboardEvent() +} diff --git a/dashboard/src/main/java/org/openedx/dashboard/notifier/DashboardNotifier.kt b/dashboard/src/main/java/org/openedx/dashboard/notifier/DashboardNotifier.kt new file mode 100644 index 000000000..5e3fa6e22 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/dashboard/notifier/DashboardNotifier.kt @@ -0,0 +1,19 @@ +package org.openedx.dashboard.notifier + +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +class DashboardNotifier { + + private val channel = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + val notifier: Flow = channel.asSharedFlow() + + suspend fun send(event: DashboardEvent) = channel.emit(event) +} diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt index 42d229330..83ba221e2 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardRouter.kt @@ -10,4 +10,11 @@ interface DashboardRouter { courseTitle: String ) -} \ No newline at end of file + fun navigateToProgramInfo( + fm: FragmentManager, + pathId: String, + + ) + + fun navigateToCourseInfo(fm: FragmentManager, courseId: String, infoType: String) +} diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardAnalytics.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardAnalytics.kt similarity index 67% rename from dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardAnalytics.kt rename to dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardAnalytics.kt index 6a69e7a65..a0ce2285e 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardAnalytics.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardAnalytics.kt @@ -1,4 +1,4 @@ -package org.openedx.dashboard.presentation +package org.openedx.dashboard.presentation.dashboard interface DashboardAnalytics { fun dashboardCourseClickedEvent(courseId: String, courseName: String) diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardFragment.kt similarity index 97% rename from dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt rename to dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardFragment.kt index 97326223f..849423409 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardFragment.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardFragment.kt @@ -1,4 +1,4 @@ -package org.openedx.dashboard.presentation +package org.openedx.dashboard.presentation.dashboard import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES @@ -46,13 +46,23 @@ 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.HandleUIMessage +import org.openedx.core.ui.OfflineModeDialog +import org.openedx.core.ui.Toolbar +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.rememberWindowSize +import org.openedx.core.ui.shouldLoadMore +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 +import org.openedx.core.ui.windowSizeValue import org.openedx.core.utils.TimeUtils import org.openedx.dashboard.R +import org.openedx.dashboard.presentation.DashboardRouter import java.util.* class DashboardFragment : Fragment() { diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardUIState.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardUIState.kt similarity index 82% rename from dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardUIState.kt rename to dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardUIState.kt index 5ea81cfd0..b0c5b1daa 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardUIState.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardUIState.kt @@ -1,4 +1,4 @@ -package org.openedx.dashboard.presentation +package org.openedx.dashboard.presentation.dashboard import org.openedx.core.domain.model.EnrolledCourse diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardViewModel.kt similarity index 90% rename from dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt rename to dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardViewModel.kt index c86f200e3..9316d831f 100644 --- a/dashboard/src/main/java/org/openedx/dashboard/presentation/DashboardViewModel.kt +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/dashboard/DashboardViewModel.kt @@ -1,9 +1,12 @@ -package org.openedx.dashboard.presentation +package org.openedx.dashboard.presentation.dashboard import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.R @@ -19,6 +22,8 @@ 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 org.openedx.dashboard.notifier.DashboardEvent +import org.openedx.dashboard.notifier.DashboardNotifier class DashboardViewModel( @@ -26,7 +31,8 @@ class DashboardViewModel( private val networkConnection: NetworkConnection, private val interactor: DashboardInteractor, private val resourceManager: ResourceManager, - private val notifier: CourseNotifier, + private val courseNotifier: CourseNotifier, + private val dashboardNotifier: DashboardNotifier, private val analytics: DashboardAnalytics, private val appUpgradeNotifier: AppUpgradeNotifier ) : BaseViewModel() { @@ -63,12 +69,17 @@ class DashboardViewModel( override fun onCreate(owner: LifecycleOwner) { super.onCreate(owner) viewModelScope.launch { - notifier.notifier.collect { + courseNotifier.notifier.collect { if (it is CourseDashboardUpdate) { updateCourses() } } } + dashboardNotifier.notifier.onEach { + if (it is DashboardEvent.UpdateEnrolledCourses) { + updateCourses() + } + }.distinctUntilChanged().launchIn(viewModelScope) } init { diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramFragment.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramFragment.kt new file mode 100644 index 000000000..70d805e26 --- /dev/null +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramFragment.kt @@ -0,0 +1,339 @@ +package org.openedx.dashboard.presentation.program + +import android.content.res.Configuration +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.rememberScaffoldState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.zIndex +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import org.koin.androidx.viewmodel.ext.android.viewModel +import org.openedx.core.extension.toastMessage +import org.openedx.core.presentation.catalog.CatalogWebViewScreen +import org.openedx.core.presentation.catalog.WebViewLink +import org.openedx.core.presentation.dialog.alert.ActionDialogFragment +import org.openedx.core.presentation.dialog.alert.InfoDialogFragment +import org.openedx.core.ui.ConnectionErrorView +import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.Toolbar +import org.openedx.core.ui.WindowSize +import org.openedx.core.ui.WindowType +import org.openedx.core.ui.displayCutoutForLandscape +import org.openedx.core.ui.rememberWindowSize +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.windowSizeValue +import org.openedx.dashboard.R +import org.openedx.core.R as coreR +import org.openedx.core.presentation.catalog.WebViewLink.Authority as linkAuthority + +class ProgramFragment(private val myPrograms: Boolean = false) : Fragment() { + + private val viewModel by viewModel() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + if (myPrograms.not()) { + lifecycle.addObserver(viewModel) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + val windowSize = rememberWindowSize() + val uiState by viewModel.uiState.collectAsState(initial = ProgramUIState.Loading) + var hasInternetConnection by remember { + mutableStateOf(viewModel.hasInternetConnection) + } + + if (myPrograms.not()) { + DisposableEffect(uiState is ProgramUIState.CourseEnrolled) { + if (uiState is ProgramUIState.CourseEnrolled) { + + val courseId = (uiState as ProgramUIState.CourseEnrolled).courseId + val isEnrolled = (uiState as ProgramUIState.CourseEnrolled).isEnrolled + + if (isEnrolled) { + viewModel.onEnrolledCourseClick( + fragmentManager = requireActivity().supportFragmentManager, + courseId = courseId, + ) + context.toastMessage(getString(R.string.dashboard_enrolled_successfully)) + } else { + InfoDialogFragment.newInstance( + title = getString(coreR.string.course_enrollment_error), + message = getString(coreR.string.course_enrollment_error_message) + ).show( + requireActivity().supportFragmentManager, + InfoDialogFragment::class.simpleName + ) + } + } + onDispose {} + } + } + + ProgramInfoScreen( + windowSize = windowSize, + uiState = uiState, + contentUrl = getInitialUrl(), + canShowBackBtn = arguments?.getString(ARG_PATH_ID, "") + ?.isNotEmpty() == true, + uriScheme = viewModel.uriScheme, + hasInternetConnection = hasInternetConnection, + checkInternetConnection = { + hasInternetConnection = viewModel.hasInternetConnection + }, + onWebPageLoaded = { viewModel.showLoading(false) }, + onBackClick = { + requireActivity().supportFragmentManager.popBackStackImmediate() + }, + onUriClick = { param, type -> + when (type) { + linkAuthority.ENROLLED_COURSE_INFO -> { + viewModel.onEnrolledCourseClick( + fragmentManager = requireActivity().supportFragmentManager, + courseId = param + ) + } + + linkAuthority.ENROLLED_PROGRAM_INFO -> { + viewModel.onProgramCardClick( + fragmentManager = requireActivity().supportFragmentManager, + pathId = param + ) + } + + linkAuthority.PROGRAM_INFO, + linkAuthority.COURSE_INFO -> { + viewModel.onViewCourseClick( + fragmentManager = requireActivity().supportFragmentManager, + courseId = param, + infoType = type.name + ) + } + + linkAuthority.ENROLL -> { + viewModel.enrollInACourse(param) + } + + linkAuthority.COURSE -> { + viewModel.navigateToDiscovery() + } + + linkAuthority.EXTERNAL -> { + ActionDialogFragment.newInstance( + title = getString(coreR.string.core_leaving_the_app), + message = getString( + coreR.string.core_leaving_the_app_message, + getString(coreR.string.platform_name) + ), + url = param, + ).show( + requireActivity().supportFragmentManager, + ActionDialogFragment::class.simpleName + ) + } + + else -> {} + } + }, + refreshSessionCookie = { + viewModel.refreshCookie() + }, + ) + } + } + } + + + private fun getInitialUrl(): String { + return arguments?.let { args -> + val pathId = args.getString(ARG_PATH_ID) ?: "" + viewModel.programConfig.programDetailUrlTemplate.replace("{$ARG_PATH_ID}", pathId) + } ?: viewModel.programConfig.programUrl + } + + companion object { + private const val ARG_PATH_ID = "path_id" + + fun newInstance( + pathId: String, + ): ProgramFragment { + val fragment = ProgramFragment(false) + fragment.arguments = bundleOf( + ARG_PATH_ID to pathId, + ) + return fragment + } + } +} + +@Composable +private fun ProgramInfoScreen( + windowSize: WindowSize, + uiState: ProgramUIState?, + contentUrl: String, + uriScheme: String, + canShowBackBtn: Boolean, + hasInternetConnection: Boolean, + checkInternetConnection: () -> Unit, + onWebPageLoaded: () -> Unit, + onBackClick: () -> Unit, + onUriClick: (String, WebViewLink.Authority) -> Unit, + refreshSessionCookie: () -> Unit = {}, +) { + val scaffoldState = rememberScaffoldState() + val configuration = LocalConfiguration.current + val isLoading = uiState is ProgramUIState.Loading + + when (uiState) { + is ProgramUIState.UiMessage -> { + HandleUIMessage(uiMessage = uiState.uiMessage, scaffoldState = scaffoldState) + } + + else -> {} + } + + Scaffold( + scaffoldState = scaffoldState, + modifier = Modifier.fillMaxSize(), + backgroundColor = MaterialTheme.appColors.background + ) { + val modifierScreenWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = if (configuration.orientation == Configuration.ORIENTATION_PORTRAIT) { + Modifier.widthIn(Dp.Unspecified, 560.dp) + } else { + Modifier.widthIn(Dp.Unspecified, 650.dp) + }, + compact = Modifier.fillMaxWidth() + ) + ) + } + + Column( + modifier = modifierScreenWidth + .fillMaxSize() + .padding(it) + .statusBarsInset() + .displayCutoutForLandscape(), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Toolbar( + label = stringResource(id = R.string.dashboard_programs), + canShowBackBtn = canShowBackBtn, + onBackClick = onBackClick + ) + + Surface { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.White), + contentAlignment = Alignment.TopCenter + ) { + if (hasInternetConnection) { + val webView = CatalogWebViewScreen( + url = contentUrl, + uriScheme = uriScheme, + isAllLinksExternal = true, + onWebPageLoaded = onWebPageLoaded, + refreshSessionCookie = refreshSessionCookie, + onUriClick = onUriClick, + ) + + AndroidView( + modifier = Modifier + .background(MaterialTheme.appColors.background), + factory = { + webView + }, + update = { + webView.loadUrl(contentUrl) + } + ) + } else { + ConnectionErrorView( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .background(MaterialTheme.appColors.background) + ) { + checkInternetConnection() + } + } + if (isLoading && hasInternetConnection) { + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(1f), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } + } + } + } + } + } +} + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO) +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES) +@Composable +fun MyProgramsPreview() { + OpenEdXTheme { + ProgramInfoScreen( + windowSize = WindowSize(WindowType.Compact, WindowType.Compact), + uiState = ProgramUIState.Loading, + contentUrl = "https://www.example.com/", + uriScheme = "", + canShowBackBtn = false, + hasInternetConnection = false, + checkInternetConnection = {}, + onBackClick = {}, + onWebPageLoaded = {}, + onUriClick = { _, _ -> }, + ) + } +} diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramUIState.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramUIState.kt new file mode 100644 index 000000000..8f9b83c5c --- /dev/null +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramUIState.kt @@ -0,0 +1,12 @@ +package org.openedx.dashboard.presentation.program + +import org.openedx.core.UIMessage + +sealed class ProgramUIState { + object Loading : ProgramUIState() + object Loaded : ProgramUIState() + + class CourseEnrolled(val courseId: String, val isEnrolled: Boolean) : ProgramUIState() + + class UiMessage(val uiMessage: UIMessage) : ProgramUIState() +} diff --git a/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramViewModel.kt b/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramViewModel.kt new file mode 100644 index 000000000..da5453fdf --- /dev/null +++ b/dashboard/src/main/java/org/openedx/dashboard/presentation/program/ProgramViewModel.kt @@ -0,0 +1,105 @@ +package org.openedx.dashboard.presentation.program + +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.viewModelScope +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import org.openedx.core.BaseViewModel +import org.openedx.core.R +import org.openedx.core.UIMessage +import org.openedx.core.config.Config +import org.openedx.core.extension.isInternetError +import org.openedx.core.interfaces.EnrollInCourseInteractor +import org.openedx.core.system.AppCookieManager +import org.openedx.core.system.ResourceManager +import org.openedx.core.system.connection.NetworkConnection +import org.openedx.dashboard.notifier.DashboardEvent +import org.openedx.dashboard.notifier.DashboardNotifier +import org.openedx.dashboard.presentation.DashboardRouter + +class ProgramViewModel( + private val config: Config, + private val networkConnection: NetworkConnection, + private val router: DashboardRouter, + private val notifier: DashboardNotifier, + private val edxCookieManager: AppCookieManager, + private val resourceManager: ResourceManager, + private val courseInteractor: EnrollInCourseInteractor +) : BaseViewModel() { + val uriScheme: String get() = config.getUriScheme() + + val programConfig get() = config.getProgramConfig().webViewConfig + + val hasInternetConnection: Boolean get() = networkConnection.isOnline() + + private val _uiState = MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val uiState: SharedFlow get() = _uiState.asSharedFlow() + + fun showLoading(isLoading: Boolean) { + viewModelScope.launch { + _uiState.emit(if (isLoading) ProgramUIState.Loading else ProgramUIState.Loaded) + } + } + + fun enrollInACourse(courseId: String) { + showLoading(true) + viewModelScope.launch { + try { + courseInteractor.enrollInACourse(courseId) + _uiState.emit(ProgramUIState.CourseEnrolled(courseId, true)) + notifier.send(DashboardEvent.UpdateEnrolledCourses) + } catch (e: Exception) { + if (e.isInternetError()) { + _uiState.emit( + ProgramUIState.UiMessage( + UIMessage.SnackBarMessage(resourceManager.getString(R.string.core_error_no_connection)) + ) + ) + } else { + _uiState.emit(ProgramUIState.CourseEnrolled(courseId, false)) + } + } + } + } + + fun onProgramCardClick(fragmentManager: FragmentManager, pathId: String) { + if (pathId.isNotEmpty()) { + router.navigateToProgramInfo(fm = fragmentManager, pathId = pathId) + } + } + + fun onViewCourseClick(fragmentManager: FragmentManager, courseId: String, infoType: String) { + if (courseId.isNotEmpty() && infoType.isNotEmpty()) { + router.navigateToCourseInfo( + fm = fragmentManager, + courseId = courseId, + infoType = infoType + ) + } + } + + fun onEnrolledCourseClick(fragmentManager: FragmentManager, courseId: String) { + if (courseId.isNotEmpty()) { + router.navigateToCourseOutline( + fm = fragmentManager, + courseId = courseId, + courseTitle = "" + ) + } + } + + fun navigateToDiscovery() { + viewModelScope.launch { notifier.send(DashboardEvent.NavigationToDiscovery) } + } + + fun refreshCookie() { + viewModelScope.launch { edxCookieManager.tryToRefreshSessionCookie() } + } +} diff --git a/dashboard/src/main/res/values/strings.xml b/dashboard/src/main/res/values/strings.xml index b5f67dbeb..5eb61aadc 100644 --- a/dashboard/src/main/res/values/strings.xml +++ b/dashboard/src/main/res/values/strings.xml @@ -1,8 +1,10 @@ - + Dashboard Courses + Programs Welcome back. Let\'s keep learning. It\'s empty You are not enrolled in any courses yet. - \ No newline at end of file + You have been successfully enrolled in this course. + 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 4fa22d11a..3ade5f4c6 100644 --- a/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt +++ b/dashboard/src/test/java/org/openedx/dashboard/presentation/DashboardViewModelTest.kt @@ -8,6 +8,7 @@ import io.mockk.coEvery import io.mockk.coVerify import io.mockk.every import io.mockk.mockk +import io.mockk.spyk import io.mockk.verify import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -31,6 +32,10 @@ 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 org.openedx.dashboard.notifier.DashboardNotifier +import org.openedx.dashboard.presentation.dashboard.DashboardAnalytics +import org.openedx.dashboard.presentation.dashboard.DashboardUIState +import org.openedx.dashboard.presentation.dashboard.DashboardViewModel import java.net.UnknownHostException @OptIn(ExperimentalCoroutinesApi::class) @@ -45,7 +50,8 @@ class DashboardViewModelTest { private val resourceManager = mockk() private val interactor = mockk() private val networkConnection = mockk() - private val notifier = mockk() + private val courseNotifier = mockk() + private val dashboardNotifier = spyk() private val analytics = mockk() private val appUpgradeNotifier = mockk() @@ -73,7 +79,7 @@ class DashboardViewModelTest { @Test fun `getCourses no internet connection`() = runTest { - val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) + val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, courseNotifier, dashboardNotifier, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException() advanceUntilIdle() @@ -89,7 +95,7 @@ class DashboardViewModelTest { @Test fun `getCourses unknown error`() = runTest { - val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) + val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, courseNotifier, dashboardNotifier, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } throws Exception() advanceUntilIdle() @@ -105,7 +111,7 @@ class DashboardViewModelTest { @Test fun `getCourses from network`() = runTest { - val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) + val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, courseNotifier, dashboardNotifier, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk()) @@ -121,7 +127,7 @@ class DashboardViewModelTest { @Test fun `getCourses from network with next page`() = runTest { - val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) + val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, courseNotifier, dashboardNotifier, analytics, appUpgradeNotifier) every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList.copy( Pagination( @@ -147,7 +153,7 @@ class DashboardViewModelTest { fun `getCourses from cache`() = runTest { every { networkConnection.isOnline() } returns false coEvery { interactor.getEnrolledCoursesFromCache() } returns listOf(mockk()) - val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) + val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, courseNotifier, dashboardNotifier, analytics, appUpgradeNotifier) advanceUntilIdle() @@ -163,7 +169,7 @@ class DashboardViewModelTest { fun `updateCourses no internet error`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) + val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, courseNotifier, dashboardNotifier, analytics, appUpgradeNotifier) coEvery { interactor.getEnrolledCourses(any()) } throws UnknownHostException() viewModel.updateCourses() @@ -183,7 +189,7 @@ class DashboardViewModelTest { fun `updateCourses unknown exception`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) + val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, courseNotifier, dashboardNotifier, analytics, appUpgradeNotifier) coEvery { interactor.getEnrolledCourses(any()) } throws Exception() viewModel.updateCourses() @@ -203,7 +209,7 @@ class DashboardViewModelTest { fun `updateCourses success`() = runTest { every { networkConnection.isOnline() } returns true coEvery { interactor.getEnrolledCourses(any()) } returns dashboardCourseList - val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) + val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, courseNotifier, dashboardNotifier, analytics, appUpgradeNotifier) viewModel.updateCourses() advanceUntilIdle() @@ -221,7 +227,7 @@ 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(config, networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) + val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, courseNotifier, dashboardNotifier, analytics, appUpgradeNotifier) viewModel.updateCourses() advanceUntilIdle() @@ -237,8 +243,8 @@ class DashboardViewModelTest { @Test fun `CourseDashboardUpdate notifier test`() = runTest { - coEvery { notifier.notifier } returns flow { emit(CourseDashboardUpdate()) } - val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, notifier, analytics, appUpgradeNotifier) + coEvery { courseNotifier.notifier } returns flow { emit(CourseDashboardUpdate()) } + val viewModel = DashboardViewModel(config, networkConnection, interactor, resourceManager, courseNotifier, dashboardNotifier, analytics, appUpgradeNotifier) val mockLifeCycleOwner: LifecycleOwner = mockk() val lifecycleRegistry = LifecycleRegistry(mockLifeCycleOwner) diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 6ad264739..f074ddd9d 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -1,6 +1,7 @@ API_HOST_URL: 'http://localhost:8000' APPLICATION_ID: 'org.openedx.app' ENVIRONMENT_DISPLAY_NAME: 'Localhost' +URI_SCHEME: '' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' FAQ_URL: '' OAUTH_CLIENT_ID: 'OAUTH_CLIENT_ID' @@ -18,10 +19,15 @@ DISCOVERY: TYPE: 'native' WEBVIEW: BASE_URL: '' - URI_SCHEME: '' COURSE_DETAIL_TEMPLATE: '' PROGRAM_DETAIL_TEMPLATE: '' +PROGRAM: + TYPE: 'native' + WEBVIEW: + PROGRAM_URL: '' + PROGRAM_DETAIL_URL_TEMPLATE: '' + GOOGLE: ENABLED: false CLIENT_ID: '' diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index fa8456ce1..25df957a0 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -1,6 +1,7 @@ API_HOST_URL: 'http://localhost:8000' APPLICATION_ID: 'org.openedx.app' ENVIRONMENT_DISPLAY_NAME: 'Localhost' +URI_SCHEME: '' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' FAQ_URL: '' OAUTH_CLIENT_ID: 'OAUTH_CLIENT_ID' @@ -18,10 +19,15 @@ DISCOVERY: TYPE: 'native' WEBVIEW: BASE_URL: '' - URI_SCHEME: '' COURSE_DETAIL_TEMPLATE: '' PROGRAM_DETAIL_TEMPLATE: '' +PROGRAM: + TYPE: 'native' + WEBVIEW: + PROGRAM_URL: '' + PROGRAM_DETAIL_URL_TEMPLATE: '' + GOOGLE: ENABLED: false CLIENT_ID: '' diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index fa8456ce1..25df957a0 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -1,6 +1,7 @@ API_HOST_URL: 'http://localhost:8000' APPLICATION_ID: 'org.openedx.app' ENVIRONMENT_DISPLAY_NAME: 'Localhost' +URI_SCHEME: '' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' FAQ_URL: '' OAUTH_CLIENT_ID: 'OAUTH_CLIENT_ID' @@ -18,10 +19,15 @@ DISCOVERY: TYPE: 'native' WEBVIEW: BASE_URL: '' - URI_SCHEME: '' COURSE_DETAIL_TEMPLATE: '' PROGRAM_DETAIL_TEMPLATE: '' +PROGRAM: + TYPE: 'native' + WEBVIEW: + PROGRAM_URL: '' + PROGRAM_DETAIL_URL_TEMPLATE: '' + GOOGLE: ENABLED: false CLIENT_ID: '' diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt index c8017cb32..a30d6075d 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt @@ -46,6 +46,7 @@ import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.presentation.catalog.CatalogWebViewScreen +import org.openedx.core.presentation.catalog.WebViewLink import org.openedx.core.presentation.dialog.alert.ActionDialogFragment import org.openedx.core.ui.ConnectionErrorView import org.openedx.core.ui.Toolbar @@ -80,7 +81,7 @@ class WebViewDiscoveryFragment : Fragment() { WebViewDiscoveryScreen( windowSize = windowSize, contentUrl = viewModel.discoveryUrl, - uriScheme = viewModel.webViewConfig.uriScheme, + uriScheme = viewModel.uriScheme, hasInternetConnection = hasInternetConnection, checkInternetConnection = { hasInternetConnection = viewModel.hasInternetConnection @@ -88,26 +89,34 @@ class WebViewDiscoveryFragment : Fragment() { onWebPageUpdated = { url -> viewModel.updateDiscoveryUrl(url) }, - onInfoCardClicked = { pathId, infoType -> - viewModel.infoCardClicked( - fragmentManager = requireActivity().supportFragmentManager, - pathId = pathId, - infoType = infoType - ) - }, - openExternalLink = { url -> - ActionDialogFragment.newInstance( - title = getString(CoreR.string.core_leaving_the_app), - message = getString( - CoreR.string.core_leaving_the_app_message, - getString(CoreR.string.platform_name) - ), - url = url, - ).show( - requireActivity().supportFragmentManager, - ActionDialogFragment::class.simpleName - ) - }, + onUriClick = { param, authority -> + when (authority) { + WebViewLink.Authority.COURSE_INFO, + WebViewLink.Authority.PROGRAM_INFO -> { + viewModel.infoCardClicked( + fragmentManager = requireActivity().supportFragmentManager, + pathId = param, + infoType = authority.name + ) + } + + WebViewLink.Authority.EXTERNAL -> { + ActionDialogFragment.newInstance( + title = getString(CoreR.string.core_leaving_the_app), + message = getString( + CoreR.string.core_leaving_the_app_message, + getString(CoreR.string.platform_name) + ), + url = param, + ).show( + requireActivity().supportFragmentManager, + ActionDialogFragment::class.simpleName + ) + } + + else -> {} + } + } ) } } @@ -123,8 +132,7 @@ private fun WebViewDiscoveryScreen( hasInternetConnection: Boolean, checkInternetConnection: () -> Unit, onWebPageUpdated: (String) -> Unit, - onInfoCardClicked: (String, String) -> Unit, - openExternalLink: (String) -> Unit, + onUriClick: (String, WebViewLink.Authority) -> Unit ) { val scaffoldState = rememberScaffoldState() val configuration = LocalConfiguration.current @@ -171,8 +179,7 @@ private fun WebViewDiscoveryScreen( uriScheme = uriScheme, onWebPageLoaded = { isLoading = false }, onWebPageUpdated = onWebPageUpdated, - onInfoCardClicked = onInfoCardClicked, - openExternalLink = openExternalLink, + onUriClick = onUriClick, ) } else { ConnectionErrorView( @@ -207,16 +214,14 @@ private fun DiscoveryWebView( uriScheme: String, onWebPageLoaded: () -> Unit, onWebPageUpdated: (String) -> Unit, - onInfoCardClicked: (String, String) -> Unit, - openExternalLink: (String) -> Unit, + onUriClick: (String, WebViewLink.Authority) -> Unit, ) { val webView = CatalogWebViewScreen( url = contentUrl, uriScheme = uriScheme, onWebPageLoaded = onWebPageLoaded, onWebPageUpdated = onWebPageUpdated, - openExternalLink = openExternalLink, - onInfoCardClicked = onInfoCardClicked, + onUriClick = onUriClick, ) AndroidView( @@ -285,8 +290,7 @@ private fun WebViewDiscoveryScreenPreview() { hasInternetConnection = false, checkInternetConnection = {}, onWebPageUpdated = {}, - onInfoCardClicked = { _, _ -> }, - openExternalLink = {} + onUriClick = { _, _ -> }, ) } } diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt index f88bd3f00..a25bc801e 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt @@ -11,6 +11,8 @@ class WebViewDiscoveryViewModel( private val router: DiscoveryRouter, ) : BaseViewModel() { + val uriScheme: String get() = config.getUriScheme() + val webViewConfig get() = config.getDiscoveryConfig().webViewConfig private var _discoveryUrl = webViewConfig.baseUrl