diff --git a/core/src/main/java/org/openedx/core/extension/StringExt.kt b/core/src/main/java/org/openedx/core/extension/StringExt.kt index d383cf57f..0ecc86e1f 100644 --- a/core/src/main/java/org/openedx/core/extension/StringExt.kt +++ b/core/src/main/java/org/openedx/core/extension/StringExt.kt @@ -1,6 +1,7 @@ package org.openedx.core.extension import android.util.Patterns +import java.net.URL import java.util.Locale import java.util.regex.Pattern @@ -38,6 +39,14 @@ fun String.takeIfNotEmpty(): String? { return if (this.isEmpty().not()) this else null } +fun String?.equalsHost(host: String?): Boolean { + return try { + host?.startsWith(URL(this).host, ignoreCase = true) == true + } catch (e: Exception) { + false + } +} + fun String.toImageLink(apiHostURL: String): String = if (this.isLinkValid()) { this diff --git a/core/src/main/java/org/openedx/core/presentation/global/ErrorType.kt b/core/src/main/java/org/openedx/core/presentation/global/ErrorType.kt new file mode 100644 index 000000000..481758ecb --- /dev/null +++ b/core/src/main/java/org/openedx/core/presentation/global/ErrorType.kt @@ -0,0 +1,23 @@ +package org.openedx.core.presentation.global + +import org.openedx.core.R + +enum class ErrorType( + val iconResId: Int = 0, + val titleResId: Int = 0, + val descriptionResId: Int = 0, + val actionResId: Int = 0, +) { + CONNECTION_ERROR( + iconResId = R.drawable.core_no_internet_connection, + titleResId = R.string.core_no_internet_connection, + descriptionResId = R.string.core_no_internet_connection_description, + actionResId = R.string.core_reload, + ), + UNKNOWN_ERROR( + iconResId = R.drawable.core_ic_unknown_error, + titleResId = R.string.core_try_again, + descriptionResId = R.string.core_something_went_wrong_description, + actionResId = R.string.core_reload, + ), +} diff --git a/core/src/main/java/org/openedx/core/presentation/global/webview/WebViewUIState.kt b/core/src/main/java/org/openedx/core/presentation/global/webview/WebViewUIState.kt new file mode 100644 index 000000000..3a99afaaf --- /dev/null +++ b/core/src/main/java/org/openedx/core/presentation/global/webview/WebViewUIState.kt @@ -0,0 +1,15 @@ +package org.openedx.core.presentation.global.webview + +import org.openedx.core.presentation.global.ErrorType + +sealed class WebViewUIState { + data object Loading : WebViewUIState() + data object Loaded : WebViewUIState() + data class Error(val errorType: ErrorType) : WebViewUIState() +} + +enum class WebViewUIAction { + WEB_PAGE_LOADED, + WEB_PAGE_ERROR, + RELOAD_WEB_PAGE +} 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 3eb16bd9b..d50b05cbe 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -108,6 +108,7 @@ import org.openedx.core.domain.model.RegistrationField import org.openedx.core.extension.LinkedImageText import org.openedx.core.extension.tagId import org.openedx.core.extension.toastMessage +import org.openedx.core.presentation.global.ErrorType import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appShapes @@ -1133,25 +1134,33 @@ fun BackBtn( } @Composable -fun ConnectionErrorView( - modifier: Modifier, - onReloadClick: () -> Unit, +fun ConnectionErrorView(onReloadClick: () -> Unit) { + FullScreenErrorView(errorType = ErrorType.CONNECTION_ERROR, onReloadClick = onReloadClick) +} + +@Composable +fun FullScreenErrorView( + modifier: Modifier = Modifier, + errorType: ErrorType, + onReloadClick: () -> Unit ) { Column( - modifier = modifier, + modifier = modifier + .fillMaxSize() + .background(MaterialTheme.appColors.background), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { Icon( modifier = Modifier.size(100.dp), - painter = painterResource(id = R.drawable.core_no_internet_connection), + painter = painterResource(id = errorType.iconResId), contentDescription = null, tint = MaterialTheme.appColors.onSurface ) Spacer(Modifier.height(28.dp)) Text( modifier = Modifier.fillMaxWidth(0.8f), - text = stringResource(id = R.string.core_no_internet_connection), + text = stringResource(id = errorType.titleResId), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.titleLarge, textAlign = TextAlign.Center @@ -1159,7 +1168,7 @@ fun ConnectionErrorView( Spacer(Modifier.height(16.dp)) Text( modifier = Modifier.fillMaxWidth(0.8f), - text = stringResource(id = R.string.core_no_internet_connection_description), + text = stringResource(id = errorType.descriptionResId), color = MaterialTheme.appColors.textPrimary, style = MaterialTheme.appTypography.bodyLarge, textAlign = TextAlign.Center @@ -1168,7 +1177,7 @@ fun ConnectionErrorView( OpenEdXButton( modifier = Modifier .widthIn(Dp.Unspecified, 162.dp), - text = stringResource(id = R.string.core_reload), + text = stringResource(id = errorType.actionResId), textColor = MaterialTheme.appColors.primaryButtonText, backgroundColor = MaterialTheme.appColors.secondaryButtonBackground, onClick = onReloadClick, @@ -1369,11 +1378,7 @@ private fun IconTextPreview() { @Composable private fun ConnectionErrorViewPreview() { OpenEdXTheme(darkTheme = true) { - ConnectionErrorView( - modifier = Modifier - .fillMaxSize(), - onReloadClick = {} - ) + ConnectionErrorView(onReloadClick = {}) } } diff --git a/core/src/main/res/drawable/core_ic_unknown_error.xml b/core/src/main/res/drawable/core_ic_unknown_error.xml new file mode 100644 index 000000000..d7d2c0c02 --- /dev/null +++ b/core/src/main/res/drawable/core_ic_unknown_error.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 78263a819..b023e8845 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -75,6 +75,8 @@ We received your feedback and will use it to help improve your learning experience going forward. Thank you for sharing! No internet connection Please connect to the internet to view this content. + Try Again + Something went wrong OK Continue Leaving the app diff --git a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitFragment.kt b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitFragment.kt index db88ae6c8..b5747d4d0 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitFragment.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitFragment.kt @@ -10,6 +10,7 @@ import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup import android.webkit.JavascriptInterface +import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebSettings @@ -18,7 +19,6 @@ import android.webkit.WebViewClient import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -53,10 +53,11 @@ import kotlinx.coroutines.launch import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.extension.applyDarkModeIfEnabled +import org.openedx.core.extension.equalsHost import org.openedx.core.extension.isEmailValid import org.openedx.core.extension.loadUrl import org.openedx.core.system.AppCookieManager -import org.openedx.core.ui.ConnectionErrorView +import org.openedx.core.ui.FullScreenErrorView import org.openedx.core.ui.WindowSize import org.openedx.core.ui.rememberWindowSize import org.openedx.core.ui.roundBorderWithoutBottom @@ -96,10 +97,6 @@ class HtmlUnitFragment : Fragment() { OpenEdXTheme { val windowSize = rememberWindowSize() - var isLoading by remember { - mutableStateOf(true) - } - var hasInternetConnection by remember { mutableStateOf(viewModel.isOnline) } @@ -148,7 +145,8 @@ class HtmlUnitFragment : Fragment() { .then(border), contentAlignment = Alignment.TopCenter ) { - if (uiState.isLoadingEnabled) { + if (uiState is HtmlUnitUIState.Initialization) return@Box + if ((uiState is HtmlUnitUIState.Error).not()) { if (hasInternetConnection || fromDownloadedContent) { HTMLContentView( uiState = uiState, @@ -156,41 +154,45 @@ class HtmlUnitFragment : Fragment() { url = url, cookieManager = viewModel.cookieManager, apiHostURL = viewModel.apiHostURL, - isLoading = isLoading, + isLoading = uiState is HtmlUnitUIState.Loading, injectJSList = injectJSList, onCompletionSet = { viewModel.notifyCompletionSet() }, onWebPageLoading = { - isLoading = true + viewModel.onWebPageLoading() }, onWebPageLoaded = { - isLoading = false + if ((uiState is HtmlUnitUIState.Error).not()) { + viewModel.onWebPageLoaded() + } if (isAdded) viewModel.setWebPageLoaded(requireContext().assets) }, + onWebPageLoadError = { + viewModel.onWebPageLoadError() + }, saveXBlockProgress = { jsonProgress -> viewModel.saveXBlockProgress(jsonProgress) }, ) } else { - ConnectionErrorView( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .background(MaterialTheme.appColors.background) - ) { - hasInternetConnection = viewModel.isOnline - } + viewModel.onWebPageLoadError() } - if (isLoading && hasInternetConnection) { - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(1f), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator(color = MaterialTheme.appColors.primary) - } + } else { + val errorType = (uiState as HtmlUnitUIState.Error).errorType + FullScreenErrorView(errorType = errorType) { + hasInternetConnection = viewModel.isOnline + viewModel.onWebPageLoading() + } + } + if (uiState is HtmlUnitUIState.Loading && hasInternetConnection) { + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(1f), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) } } } @@ -239,7 +241,8 @@ private fun HTMLContentView( onCompletionSet: () -> Unit, onWebPageLoading: () -> Unit, onWebPageLoaded: () -> Unit, - saveXBlockProgress: (String) -> Unit + onWebPageLoadError: () -> Unit, + saveXBlockProgress: (String) -> Unit, ) { val coroutineScope = rememberCoroutineScope() val context = LocalContext.current @@ -333,6 +336,17 @@ private fun HTMLContentView( } super.onReceivedHttpError(view, request, errorResponse) } + + override fun onReceivedError( + view: WebView, + request: WebResourceRequest, + error: WebResourceError + ) { + if (view.url.equalsHost(request.url.host)) { + onWebPageLoadError() + } + super.onReceivedError(view, request, error) + } } with(settings) { javaScriptEnabled = true @@ -356,7 +370,7 @@ private fun HTMLContentView( update = { webView -> if (!isLoading && injectJSList.isNotEmpty()) { injectJSList.forEach { webView.evaluateJavascript(it, null) } - val jsonProgress = uiState.jsonProgress + val jsonProgress = (uiState as? HtmlUnitUIState.Loaded)?.jsonProgress if (!jsonProgress.isNullOrEmpty()) { webView.setupOfflineProgress(jsonProgress) } diff --git a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitUIState.kt b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitUIState.kt index 2dc14424c..855a7a1e9 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitUIState.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitUIState.kt @@ -1,6 +1,10 @@ package org.openedx.course.presentation.unit.html -data class HtmlUnitUIState( - val jsonProgress: String?, - val isLoadingEnabled: Boolean -) +import org.openedx.core.presentation.global.ErrorType + +sealed class HtmlUnitUIState { + data object Initialization : HtmlUnitUIState() + data object Loading : HtmlUnitUIState() + data class Loaded(val jsonProgress: String? = null) : HtmlUnitUIState() + data class Error(val errorType: ErrorType) : HtmlUnitUIState() +} diff --git a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt index f852c1f2d..bccdcd0fd 100644 --- a/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt +++ b/course/src/main/java/org/openedx/course/presentation/unit/html/HtmlUnitViewModel.kt @@ -11,6 +11,7 @@ import kotlinx.coroutines.launch import org.openedx.core.BaseViewModel import org.openedx.core.config.Config import org.openedx.core.extension.readAsText +import org.openedx.core.presentation.global.ErrorType import org.openedx.core.system.AppCookieManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseCompletionSet @@ -29,9 +30,8 @@ class HtmlUnitViewModel( private val offlineProgressSyncScheduler: OfflineProgressSyncScheduler ) : BaseViewModel() { - private val _uiState = MutableStateFlow(HtmlUnitUIState(null, false)) - val uiState: StateFlow - get() = _uiState.asStateFlow() + private val _uiState = MutableStateFlow(HtmlUnitUIState.Initialization) + val uiState = _uiState.asStateFlow() private val _injectJSList = MutableStateFlow>(listOf()) val injectJSList = _injectJSList.asStateFlow() @@ -45,6 +45,19 @@ class HtmlUnitViewModel( tryToSyncProgress() } + fun onWebPageLoading() { + _uiState.value = HtmlUnitUIState.Loading + } + + fun onWebPageLoaded() { + _uiState.value = HtmlUnitUIState.Loaded() + } + + fun onWebPageLoadError() { + _uiState.value = + HtmlUnitUIState.Error(if (networkConnection.isOnline()) ErrorType.UNKNOWN_ERROR else ErrorType.CONNECTION_ERROR) + } + fun setWebPageLoaded(assets: AssetManager) { if (_injectJSList.value.isNotEmpty()) return @@ -80,7 +93,7 @@ class HtmlUnitViewModel( } } catch (e: Exception) { } finally { - _uiState.update { it.copy(isLoadingEnabled = true) } + _uiState.value = HtmlUnitUIState.Loading } } } @@ -90,7 +103,7 @@ class HtmlUnitViewModel( if (!isOnline) { val xBlockProgress = courseInteractor.getXBlockProgress(blockId) delay(500) - _uiState.update { it.copy(jsonProgress = xBlockProgress?.jsonProgress?.toJson()) } + _uiState.value = HtmlUnitUIState.Loaded(jsonProgress = xBlockProgress?.jsonProgress?.toJson()) } } } 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 276f48574..49431ba46 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 @@ -6,14 +6,10 @@ import android.os.Handler import android.os.Looper import android.view.View import android.widget.FrameLayout -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.MaterialTheme import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Modifier import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -37,7 +33,6 @@ import org.openedx.core.presentation.global.viewBinding import org.openedx.core.ui.ConnectionErrorView import org.openedx.core.ui.WindowSize import org.openedx.core.ui.theme.OpenEdXTheme -import org.openedx.core.ui.theme.appColors import org.openedx.core.utils.LocaleUtils import org.openedx.course.R import org.openedx.course.databinding.FragmentVideoUnitBinding @@ -104,11 +99,7 @@ class VideoUnitFragment : Fragment(R.layout.fragment_video_unit) { binding.connectionError.setContent { OpenEdXTheme { - ConnectionErrorView( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.appColors.background) - ) { + ConnectionErrorView { binding.connectionError.isVisible = !viewModel.hasInternetConnection && !viewModel.isDownloaded } 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 e6b04687d..b163a7cde 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 @@ -4,14 +4,10 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material.MaterialTheme import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.ui.Modifier import androidx.core.os.bundleOf import androidx.core.view.isVisible import androidx.fragment.app.Fragment @@ -32,7 +28,6 @@ import org.openedx.core.presentation.dialog.selectorbottomsheet.SelectBottomDial import org.openedx.core.ui.ConnectionErrorView import org.openedx.core.ui.WindowSize import org.openedx.core.ui.theme.OpenEdXTheme -import org.openedx.core.ui.theme.appColors import org.openedx.core.utils.LocaleUtils import org.openedx.course.R import org.openedx.course.databinding.FragmentYoutubeVideoUnitBinding @@ -102,11 +97,7 @@ class YoutubeVideoUnitFragment : Fragment(R.layout.fragment_youtube_video_unit) binding.connectionError.setContent { OpenEdXTheme { - ConnectionErrorView( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.appColors.background) - ) { + ConnectionErrorView { binding.connectionError.isVisible = !viewModel.hasInternetConnection } } 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 fcd185efe..cf67d1a2c 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryFragment.kt @@ -24,6 +24,7 @@ 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 @@ -52,8 +53,11 @@ import androidx.lifecycle.LifecycleOwner import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.core.presentation.dialog.alert.ActionDialogFragment +import org.openedx.core.presentation.global.ErrorType +import org.openedx.core.presentation.global.webview.WebViewUIAction +import org.openedx.core.presentation.global.webview.WebViewUIState import org.openedx.core.ui.AuthButtonsPanel -import org.openedx.core.ui.ConnectionErrorView +import org.openedx.core.ui.FullScreenErrorView import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize import org.openedx.core.ui.WindowType @@ -83,18 +87,33 @@ class WebViewDiscoveryFragment : Fragment() { setContent { OpenEdXTheme { val windowSize = rememberWindowSize() + val uiState by viewModel.uiState.collectAsState() var hasInternetConnection by remember { mutableStateOf(viewModel.hasInternetConnection) } WebViewDiscoveryScreen( windowSize = windowSize, + uiState = uiState, isPreLogin = viewModel.isPreLogin, contentUrl = viewModel.discoveryUrl, uriScheme = viewModel.uriScheme, isRegistrationEnabled = viewModel.isRegistrationEnabled, hasInternetConnection = hasInternetConnection, - checkInternetConnection = { - hasInternetConnection = viewModel.hasInternetConnection + onWebViewUIAction = { action -> + when (action) { + WebViewUIAction.WEB_PAGE_LOADED -> { + viewModel.onWebPageLoaded() + } + + WebViewUIAction.WEB_PAGE_ERROR -> { + viewModel.onWebPageLoadError() + } + + WebViewUIAction.RELOAD_WEB_PAGE -> { + hasInternetConnection = viewModel.hasInternetConnection + viewModel.onWebPageLoading() + } + } }, onWebPageUpdated = { url -> viewModel.updateDiscoveryUrl(url) @@ -171,12 +190,13 @@ class WebViewDiscoveryFragment : Fragment() { @SuppressLint("SetJavaScriptEnabled") private fun WebViewDiscoveryScreen( windowSize: WindowSize, + uiState: WebViewUIState, isPreLogin: Boolean, contentUrl: String, uriScheme: String, isRegistrationEnabled: Boolean, hasInternetConnection: Boolean, - checkInternetConnection: () -> Unit, + onWebViewUIAction: (WebViewUIAction) -> Unit, onWebPageUpdated: (String) -> Unit, onUriClick: (String, WebViewLink.Authority) -> Unit, onRegisterClick: () -> Unit, @@ -186,7 +206,6 @@ private fun WebViewDiscoveryScreen( ) { val scaffoldState = rememberScaffoldState() val configuration = LocalConfiguration.current - var isLoading by remember { mutableStateOf(true) } Scaffold( scaffoldState = scaffoldState, @@ -251,25 +270,32 @@ private fun WebViewDiscoveryScreen( .background(Color.White), contentAlignment = Alignment.TopCenter ) { - if (hasInternetConnection) { - DiscoveryWebView( - contentUrl = contentUrl, - uriScheme = uriScheme, - onWebPageLoaded = { isLoading = false }, - onWebPageUpdated = onWebPageUpdated, - onUriClick = onUriClick, - ) - } else { - ConnectionErrorView( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .background(MaterialTheme.appColors.background) - ) { - checkInternetConnection() + if ((uiState is WebViewUIState.Error).not()) { + if (hasInternetConnection) { + DiscoveryWebView( + contentUrl = contentUrl, + uriScheme = uriScheme, + onWebPageLoaded = { + if ((uiState is WebViewUIState.Error).not()) { + onWebViewUIAction(WebViewUIAction.WEB_PAGE_LOADED) + } + }, + onWebPageUpdated = onWebPageUpdated, + onUriClick = onUriClick, + onWebPageLoadError = { + onWebViewUIAction(WebViewUIAction.WEB_PAGE_ERROR) + } + ) + } else { + onWebViewUIAction(WebViewUIAction.WEB_PAGE_ERROR) } } - if (isLoading && hasInternetConnection) { + if (uiState is WebViewUIState.Error) { + FullScreenErrorView(errorType = uiState.errorType) { + onWebViewUIAction(WebViewUIAction.RELOAD_WEB_PAGE) + } + } + if (uiState is WebViewUIState.Loading && hasInternetConnection) { Box( modifier = Modifier .fillMaxSize() @@ -293,6 +319,7 @@ private fun DiscoveryWebView( onWebPageLoaded: () -> Unit, onWebPageUpdated: (String) -> Unit, onUriClick: (String, WebViewLink.Authority) -> Unit, + onWebPageLoadError: () -> Unit ) { val webView = CatalogWebViewScreen( url = contentUrl, @@ -300,6 +327,7 @@ private fun DiscoveryWebView( onWebPageLoaded = onWebPageLoaded, onWebPageUpdated = onWebPageUpdated, onUriClick = onUriClick, + onWebPageLoadError = onWebPageLoadError ) AndroidView( @@ -363,18 +391,19 @@ private fun WebViewDiscoveryScreenPreview() { OpenEdXTheme { WebViewDiscoveryScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - contentUrl = "https://www.example.com/", + uiState = WebViewUIState.Error(ErrorType.CONNECTION_ERROR), isPreLogin = false, + contentUrl = "https://www.example.com/", uriScheme = "", isRegistrationEnabled = true, hasInternetConnection = false, - checkInternetConnection = {}, + onWebViewUIAction = {}, onWebPageUpdated = {}, onUriClick = { _, _ -> }, onRegisterClick = {}, onSignInClick = {}, onSettingsClick = {}, - onBackClick = {} + onBackClick = {}, ) } } 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 94f62574d..a8f8cfc45 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/WebViewDiscoveryViewModel.kt @@ -1,9 +1,14 @@ package org.openedx.discovery.presentation import androidx.fragment.app.FragmentManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import org.openedx.core.BaseViewModel import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences +import org.openedx.core.presentation.global.ErrorType +import org.openedx.core.presentation.global.webview.WebViewUIState import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.utils.UrlUtils @@ -16,6 +21,8 @@ class WebViewDiscoveryViewModel( private val analytics: DiscoveryAnalytics, ) : BaseViewModel() { + private val _uiState = MutableStateFlow(WebViewUIState.Loading) + val uiState: StateFlow = _uiState.asStateFlow() val uriScheme: String get() = config.getUriScheme() private val webViewConfig get() = config.getDiscoveryConfig().webViewConfig @@ -38,6 +45,19 @@ class WebViewDiscoveryViewModel( val hasInternetConnection: Boolean get() = networkConnection.isOnline() + fun onWebPageLoading() { + _uiState.value = WebViewUIState.Loading + } + + fun onWebPageLoaded() { + _uiState.value = WebViewUIState.Loaded + } + + fun onWebPageLoadError() { + _uiState.value = + WebViewUIState.Error(if (networkConnection.isOnline()) ErrorType.UNKNOWN_ERROR else ErrorType.CONNECTION_ERROR) + } + fun updateDiscoveryUrl(url: String) { if (url.isNotEmpty()) { _discoveryUrl = url diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/catalog/CatalogWebView.kt b/discovery/src/main/java/org/openedx/discovery/presentation/catalog/CatalogWebView.kt index 373516b0a..785e77767 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/catalog/CatalogWebView.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/catalog/CatalogWebView.kt @@ -1,6 +1,7 @@ package org.openedx.discovery.presentation.catalog import android.annotation.SuppressLint +import android.webkit.WebResourceError import android.webkit.WebResourceRequest import android.webkit.WebView import androidx.compose.foundation.isSystemInDarkTheme @@ -8,6 +9,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import org.openedx.core.extension.applyDarkModeIfEnabled +import org.openedx.core.extension.equalsHost import org.openedx.discovery.presentation.catalog.WebViewLink.Authority as linkAuthority @SuppressLint("SetJavaScriptEnabled", "ComposableNaming") @@ -20,6 +22,7 @@ fun CatalogWebViewScreen( refreshSessionCookie: () -> Unit = {}, onWebPageUpdated: (String) -> Unit = {}, onUriClick: (String, linkAuthority) -> Unit, + onWebPageLoadError: () -> Unit ): WebView { val context = LocalContext.current val isDarkTheme = isSystemInDarkTheme() @@ -81,6 +84,17 @@ fun CatalogWebViewScreen( else -> false } } + + override fun onReceivedError( + view: WebView, + request: WebResourceRequest, + error: WebResourceError + ) { + if (view.url.equalsHost(request.url.host)) { + onWebPageLoadError() + } + super.onReceivedError(view, request, error) + } } with(settings) { diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoFragment.kt index 1345ae5c6..4906e91f8 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoFragment.kt @@ -45,8 +45,10 @@ import org.koin.core.parameter.parametersOf import org.openedx.core.UIMessage import org.openedx.core.presentation.dialog.alert.ActionDialogFragment import org.openedx.core.presentation.dialog.alert.InfoDialogFragment +import org.openedx.core.presentation.global.webview.WebViewUIAction +import org.openedx.core.presentation.global.webview.WebViewUIState import org.openedx.core.ui.AuthButtonsPanel -import org.openedx.core.ui.ConnectionErrorView +import org.openedx.core.ui.FullScreenErrorView import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize @@ -85,6 +87,7 @@ class CourseInfoFragment : Fragment() { val uiMessage by viewModel.uiMessage.collectAsState(initial = null) val showAlert by viewModel.showAlert.collectAsState(initial = false) val uiState by viewModel.uiState.collectAsState() + val webViewState by viewModel.webViewState.collectAsState() val windowSize = rememberWindowSize() var hasInternetConnection by remember { mutableStateOf(viewModel.hasInternetConnection) @@ -105,26 +108,42 @@ class CourseInfoFragment : Fragment() { } } - LaunchedEffect(uiState.enrollmentSuccess.get()) { - if (uiState.enrollmentSuccess.get().isNotEmpty()) { + LaunchedEffect((uiState as CourseInfoUIState.CourseInfo).enrollmentSuccess.get()) { + if ((uiState as CourseInfoUIState.CourseInfo).enrollmentSuccess.get() + .isNotEmpty() + ) { viewModel.onSuccessfulCourseEnrollment( fragmentManager = requireActivity().supportFragmentManager, - courseId = uiState.enrollmentSuccess.get(), + courseId = (uiState as CourseInfoUIState.CourseInfo).enrollmentSuccess.get(), ) // Clear after navigation - uiState.enrollmentSuccess.set("") + (uiState as CourseInfoUIState.CourseInfo).enrollmentSuccess.set("") } } CourseInfoScreen( windowSize = windowSize, uiState = uiState, + webViewUIState = webViewState, uiMessage = uiMessage, uriScheme = viewModel.uriScheme, hasInternetConnection = hasInternetConnection, isRegistrationEnabled = viewModel.isRegistrationEnabled, - checkInternetConnection = { - hasInternetConnection = viewModel.hasInternetConnection + onWebViewUIAction = { action -> + when (action) { + WebViewUIAction.WEB_PAGE_LOADED -> { + viewModel.onWebPageLoaded() + } + + WebViewUIAction.WEB_PAGE_ERROR -> { + viewModel.onWebPageError() + } + + WebViewUIAction.RELOAD_WEB_PAGE -> { + hasInternetConnection = viewModel.hasInternetConnection + viewModel.onWebPageLoading() + } + } }, onRegisterClick = { viewModel.navigateToSignUp( @@ -180,7 +199,7 @@ class CourseInfoFragment : Fragment() { linkAuthority.ENROLL -> { viewModel.courseEnrollClickedEvent(param) - if (uiState.isPreLogin) { + if ((uiState as CourseInfoUIState.CourseInfo).isPreLogin) { viewModel.navigateToSignUp( fragmentManager = requireActivity().supportFragmentManager, courseId = viewModel.pathId, @@ -221,11 +240,12 @@ class CourseInfoFragment : Fragment() { private fun CourseInfoScreen( windowSize: WindowSize, uiState: CourseInfoUIState, + webViewUIState: WebViewUIState, uiMessage: UIMessage?, uriScheme: String, isRegistrationEnabled: Boolean, hasInternetConnection: Boolean, - checkInternetConnection: () -> Unit, + onWebViewUIAction: (WebViewUIAction) -> Unit, onRegisterClick: () -> Unit, onSignInClick: () -> Unit, onBackClick: () -> Unit, @@ -233,7 +253,6 @@ private fun CourseInfoScreen( ) { val scaffoldState = rememberScaffoldState() val configuration = LocalConfiguration.current - var isLoading by remember { mutableStateOf(true) } HandleUIMessage(uiMessage = uiMessage, scaffoldState = scaffoldState) @@ -242,7 +261,7 @@ private fun CourseInfoScreen( modifier = Modifier.fillMaxSize(), backgroundColor = MaterialTheme.appColors.background, bottomBar = { - if (uiState.isPreLogin) { + if ((uiState as CourseInfoUIState.CourseInfo).isPreLogin) { Box( modifier = Modifier .padding( @@ -294,24 +313,27 @@ private fun CourseInfoScreen( .navigationBarsPadding(), contentAlignment = Alignment.TopCenter ) { - if (hasInternetConnection) { - CourseInfoWebView( - contentUrl = uiState.initialUrl, - uriScheme = uriScheme, - onWebPageLoaded = { isLoading = false }, - onUriClick = onUriClick, - ) - } else { - ConnectionErrorView( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .background(MaterialTheme.appColors.background) - ) { - checkInternetConnection() + if ((webViewUIState is WebViewUIState.Error).not()) { + if (hasInternetConnection) { + CourseInfoWebView( + contentUrl = (uiState as CourseInfoUIState.CourseInfo).initialUrl, + uriScheme = uriScheme, + onWebPageLoaded = { onWebViewUIAction(WebViewUIAction.WEB_PAGE_LOADED) }, + onUriClick = onUriClick, + onWebPageLoadError = { + onWebViewUIAction(WebViewUIAction.WEB_PAGE_ERROR) + } + ) + } else { + onWebViewUIAction(WebViewUIAction.WEB_PAGE_ERROR) + } + } + if (webViewUIState is WebViewUIState.Error) { + FullScreenErrorView(errorType = webViewUIState.errorType) { + onWebViewUIAction(WebViewUIAction.RELOAD_WEB_PAGE) } } - if (isLoading && hasInternetConnection) { + if (webViewUIState is WebViewUIState.Loading && hasInternetConnection) { Box( modifier = Modifier .fillMaxSize() @@ -334,6 +356,7 @@ private fun CourseInfoWebView( uriScheme: String, onWebPageLoaded: () -> Unit, onUriClick: (String, linkAuthority) -> Unit, + onWebPageLoadError: () -> Unit ) { val webView = CatalogWebViewScreen( @@ -342,6 +365,7 @@ private fun CourseInfoWebView( isAllLinksExternal = true, onWebPageLoaded = onWebPageLoaded, onUriClick = onUriClick, + onWebPageLoadError = onWebPageLoadError ) AndroidView( @@ -360,7 +384,7 @@ fun CourseInfoScreenPreview() { OpenEdXTheme { CourseInfoScreen( windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - uiState = CourseInfoUIState( + uiState = CourseInfoUIState.CourseInfo( initialUrl = "https://www.example.com/", isPreLogin = false, enrollmentSuccess = AtomicReference("") @@ -369,11 +393,12 @@ fun CourseInfoScreenPreview() { uriScheme = "", isRegistrationEnabled = true, hasInternetConnection = false, - checkInternetConnection = {}, + onWebViewUIAction = {}, onRegisterClick = {}, onSignInClick = {}, onBackClick = {}, onUriClick = { _, _ -> }, + webViewUIState = WebViewUIState.Loading, ) } } diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoUIState.kt b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoUIState.kt index ffabf1daf..cd28abd2b 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoUIState.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoUIState.kt @@ -2,8 +2,10 @@ package org.openedx.discovery.presentation.info import java.util.concurrent.atomic.AtomicReference -internal data class CourseInfoUIState( - val initialUrl: String = "", - val isPreLogin: Boolean = false, - val enrollmentSuccess: AtomicReference = AtomicReference("") -) +sealed class CourseInfoUIState { + data class CourseInfo( + val initialUrl: String = "", + val isPreLogin: Boolean = false, + val enrollmentSuccess: AtomicReference = AtomicReference("") + ) : CourseInfoUIState() +} diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt index 87c64c770..65907f5cf 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/info/CourseInfoViewModel.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -17,6 +18,8 @@ import org.openedx.core.config.Config import org.openedx.core.data.storage.CorePreferences import org.openedx.core.extension.isInternetError import org.openedx.core.presentation.CoreAnalyticsKey +import org.openedx.core.presentation.global.ErrorType +import org.openedx.core.presentation.global.webview.WebViewUIState import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection import org.openedx.core.system.notifier.CourseDashboardUpdate @@ -46,13 +49,17 @@ class CourseInfoViewModel( private val _uiState = MutableStateFlow( - CourseInfoUIState( + CourseInfoUIState.CourseInfo( initialUrl = getInitialUrl(), isPreLogin = config.isPreLoginExperienceEnabled() && corePreferences.user == null ) ) internal val uiState: StateFlow = _uiState + private val _webViewUIState = MutableStateFlow(WebViewUIState.Loading) + val webViewState + get() = _webViewUIState.asStateFlow() + private val _uiMessage = MutableSharedFlow() val uiMessage: SharedFlow get() = _uiMessage.asSharedFlow() @@ -189,6 +196,19 @@ class CourseInfoViewModel( } } + fun onWebPageLoaded() { + _webViewUIState.value = WebViewUIState.Loaded + } + + fun onWebPageError() { + _webViewUIState.value = + WebViewUIState.Error(if (networkConnection.isOnline()) ErrorType.UNKNOWN_ERROR else ErrorType.CONNECTION_ERROR) + } + + fun onWebPageLoading() { + _webViewUIState.value = WebViewUIState.Loading + } + companion object { private const val ARG_PATH_ID = "path_id" } diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt index ef79e1f32..85809a9fb 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramFragment.kt @@ -50,8 +50,9 @@ import org.openedx.core.extension.takeIfNotEmpty import org.openedx.core.extension.toastMessage import org.openedx.core.presentation.dialog.alert.ActionDialogFragment import org.openedx.core.presentation.dialog.alert.InfoDialogFragment +import org.openedx.core.presentation.global.webview.WebViewUIAction import org.openedx.core.system.AppCookieManager -import org.openedx.core.ui.ConnectionErrorView +import org.openedx.core.ui.FullScreenErrorView import org.openedx.core.ui.HandleUIMessage import org.openedx.core.ui.Toolbar import org.openedx.core.ui.WindowSize @@ -133,10 +134,22 @@ class ProgramFragment : Fragment() { isNestedFragment = isNestedFragment, uriScheme = viewModel.uriScheme, hasInternetConnection = hasInternetConnection, - checkInternetConnection = { - hasInternetConnection = viewModel.hasInternetConnection + onWebViewUIAction = { action -> + when (action) { + WebViewUIAction.WEB_PAGE_LOADED -> { + viewModel.showLoading(false) + } + + WebViewUIAction.WEB_PAGE_ERROR -> { + viewModel.onPageLoadError() + } + + WebViewUIAction.RELOAD_WEB_PAGE -> { + hasInternetConnection = viewModel.hasInternetConnection + viewModel.showLoading(true) + } + } }, - onWebPageLoaded = { viewModel.showLoading(false) }, onBackClick = { requireActivity().supportFragmentManager.popBackStackImmediate() }, @@ -192,7 +205,7 @@ class ProgramFragment : Fragment() { }, onSettingsClick = { viewModel.navigateToSettings(requireActivity().supportFragmentManager) - }, + } ) } } @@ -234,8 +247,7 @@ private fun ProgramInfoScreen( canShowBackBtn: Boolean, isNestedFragment: Boolean, hasInternetConnection: Boolean, - checkInternetConnection: () -> Unit, - onWebPageLoaded: () -> Unit, + onWebViewUIAction: (WebViewUIAction) -> Unit, onSettingsClick: () -> Unit, onBackClick: () -> Unit, onUriClick: (String, WebViewLink.Authority) -> Unit, @@ -243,7 +255,6 @@ private fun ProgramInfoScreen( val scaffoldState = rememberScaffoldState() val configuration = LocalConfiguration.current val coroutineScope = rememberCoroutineScope() - val isLoading = uiState is ProgramUIState.Loading when (uiState) { is ProgramUIState.UiMessage -> { @@ -304,41 +315,44 @@ private fun ProgramInfoScreen( .background(Color.White), contentAlignment = Alignment.TopCenter ) { - if (hasInternetConnection) { - val webView = CatalogWebViewScreen( - url = contentUrl, - uriScheme = uriScheme, - isAllLinksExternal = true, - onWebPageLoaded = onWebPageLoaded, - refreshSessionCookie = { - coroutineScope.launch { - cookieManager.tryToRefreshSessionCookie() + if ((uiState is ProgramUIState.Error).not()) { + if (hasInternetConnection) { + val webView = CatalogWebViewScreen( + url = contentUrl, + uriScheme = uriScheme, + isAllLinksExternal = true, + onWebPageLoaded = { onWebViewUIAction(WebViewUIAction.WEB_PAGE_LOADED) }, + refreshSessionCookie = { + coroutineScope.launch { + cookieManager.tryToRefreshSessionCookie() + } + }, + onUriClick = onUriClick, + onWebPageLoadError = { onWebViewUIAction(WebViewUIAction.WEB_PAGE_ERROR) } + ) + + AndroidView( + modifier = Modifier + .background(MaterialTheme.appColors.background), + factory = { + webView + }, + update = { + webView.loadUrl(contentUrl, coroutineScope, cookieManager) } - }, - onUriClick = onUriClick, - ) + ) + } else { + onWebViewUIAction(WebViewUIAction.WEB_PAGE_ERROR) + } + } - AndroidView( - modifier = Modifier - .background(MaterialTheme.appColors.background), - factory = { - webView - }, - update = { - webView.loadUrl(contentUrl, coroutineScope, cookieManager) - } - ) - } else { - ConnectionErrorView( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight() - .background(MaterialTheme.appColors.background) - ) { - checkInternetConnection() + if (uiState is ProgramUIState.Error) { + FullScreenErrorView(errorType = uiState.errorType) { + onWebViewUIAction(WebViewUIAction.RELOAD_WEB_PAGE) } } - if (isLoading && hasInternetConnection) { + + if (uiState == ProgramUIState.Loading && hasInternetConnection) { Box( modifier = Modifier .fillMaxSize() @@ -368,9 +382,8 @@ fun MyProgramsPreview() { canShowBackBtn = false, isNestedFragment = false, hasInternetConnection = false, - checkInternetConnection = {}, + onWebViewUIAction = {}, onBackClick = {}, - onWebPageLoaded = {}, onSettingsClick = {}, onUriClick = { _, _ -> }, ) diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramUIState.kt b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramUIState.kt index fa7f395d7..bed418100 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramUIState.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramUIState.kt @@ -1,10 +1,12 @@ package org.openedx.discovery.presentation.program import org.openedx.core.UIMessage +import org.openedx.core.presentation.global.ErrorType sealed class ProgramUIState { data object Loading : ProgramUIState() data object Loaded : ProgramUIState() + data class Error(val errorType: ErrorType) : ProgramUIState() class CourseEnrolled(val courseId: String, val isEnrolled: Boolean) : ProgramUIState() diff --git a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt index 1bed6d2cd..59a26cba5 100644 --- a/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt +++ b/discovery/src/main/java/org/openedx/discovery/presentation/program/ProgramViewModel.kt @@ -2,16 +2,16 @@ package org.openedx.discovery.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.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow 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.presentation.global.ErrorType import org.openedx.core.system.AppCookieManager import org.openedx.core.system.ResourceManager import org.openedx.core.system.connection.NetworkConnection @@ -38,12 +38,8 @@ class ProgramViewModel( val hasInternetConnection: Boolean get() = networkConnection.isOnline() - private val _uiState = MutableSharedFlow( - replay = 0, - extraBufferCapacity = 1, - onBufferOverflow = BufferOverflow.DROP_OLDEST - ) - val uiState: SharedFlow get() = _uiState.asSharedFlow() + private val _uiState = MutableStateFlow(ProgramUIState.Loading) + val uiState: StateFlow get() = _uiState.asStateFlow() fun showLoading(isLoading: Boolean) { viewModelScope.launch { @@ -97,6 +93,9 @@ class ProgramViewModel( enrollmentMode = "" ) } + viewModelScope.launch { + _uiState.emit(ProgramUIState.Loaded) + } } fun navigateToDiscovery() { @@ -106,4 +105,10 @@ class ProgramViewModel( fun navigateToSettings(fragmentManager: FragmentManager) { router.navigateToSettings(fragmentManager) } + + fun onPageLoadError() { + viewModelScope.launch { + _uiState.emit(ProgramUIState.Error(if (networkConnection.isOnline()) ErrorType.UNKNOWN_ERROR else ErrorType.CONNECTION_ERROR)) + } + } }