diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index 0130d6b31..7ffc23676 100644 --- a/app/src/main/java/org/openedx/app/AppRouter.kt +++ b/app/src/main/java/org/openedx/app/AppRouter.kt @@ -14,6 +14,7 @@ import org.openedx.core.FragmentViewType import org.openedx.core.presentation.course.CourseViewMode import org.openedx.core.presentation.global.appupgrade.AppUpgradeRouter import org.openedx.core.presentation.global.appupgrade.UpgradeRequiredFragment +import org.openedx.core.presentation.global.webview.SSOWebContentFragment import org.openedx.core.presentation.global.webview.WebContentFragment import org.openedx.core.presentation.settings.video.VideoQualityFragment import org.openedx.core.presentation.settings.video.VideoQualityType @@ -430,6 +431,13 @@ class AppRouter : ) } + override fun navigateToSSOWebContent(fm: FragmentManager, title: String, url: String) { + replaceFragmentWithBackStack( + fm, + SSOWebContentFragment.newInstance(title = title, url = url) + ) + } + override fun navigateToManageAccount(fm: FragmentManager) { replaceFragmentWithBackStack(fm, ManageAccountFragment()) } 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 6b7692f99..a81ed68f0 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -120,6 +120,7 @@ val screenModule = module { get(), get(), get(), + get(), courseId, infoType, authCode, diff --git a/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt b/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt index 20499baf9..c0ba71a48 100644 --- a/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt +++ b/auth/src/main/java/org/openedx/auth/data/repository/AuthRepository.kt @@ -31,6 +31,18 @@ class AuthRepository( .processAuthResponse() } + suspend fun ssoLogin( + jwtToken: String + ) { + if (preferencesManager.accessToken.isBlank() || + preferencesManager.refreshToken.isBlank()){ + preferencesManager.accessToken = jwtToken + preferencesManager.refreshToken = jwtToken + } + val user = api.getProfile() + preferencesManager.user = user + } + suspend fun socialLogin(token: String?, authType: AuthType) { require(!token.isNullOrBlank()) { "Token is null" } api.exchangeAccessToken( diff --git a/auth/src/main/java/org/openedx/auth/domain/interactor/AuthInteractor.kt b/auth/src/main/java/org/openedx/auth/domain/interactor/AuthInteractor.kt index 727f77a48..864f2d8c2 100644 --- a/auth/src/main/java/org/openedx/auth/domain/interactor/AuthInteractor.kt +++ b/auth/src/main/java/org/openedx/auth/domain/interactor/AuthInteractor.kt @@ -14,6 +14,12 @@ class AuthInteractor(private val repository: AuthRepository) { repository.login(username, password) } + suspend fun ssoLogin( + jwtToken: String + ) { + repository.ssoLogin(jwtToken) + } + suspend fun loginSocial(token: String?, authType: AuthType) { repository.socialLogin(token, authType) } diff --git a/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt b/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt index 945acf02e..ac657271f 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/AuthRouter.kt @@ -27,5 +27,7 @@ interface AuthRouter { fun navigateToWebContent(fm: FragmentManager, title: String, url: String) + fun navigateToSSOWebContent(fm: FragmentManager, title: String, url: String) + fun clearBackStack(fm: FragmentManager) } diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt index e5da6fbd9..da407b2aa 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInFragment.kt @@ -11,6 +11,7 @@ import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.core.os.bundleOf import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResultListener import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.auth.data.model.AuthType @@ -47,6 +48,9 @@ class SignInFragment : Fragment() { if (viewModel.authCode != "" && !state.loginFailure && !state.loginSuccess) { viewModel.signInAuthCode(viewModel.authCode) } + setFragmentResultListener("requestKey") { requestKey, bundle -> + viewModel.ssoLogin(token = requestKey) + } LoginScreen( windowSize = windowSize, state = state, @@ -54,6 +58,7 @@ class SignInFragment : Fragment() { onEvent = { event -> when (event) { is AuthEvent.SignIn -> viewModel.login(event.login, event.password) + is AuthEvent.SsoSignIn -> viewModel.ssoClicked(parentFragmentManager) is AuthEvent.SocialSignIn -> viewModel.socialAuth( this@SignInFragment, event.authType @@ -115,6 +120,7 @@ class SignInFragment : Fragment() { internal sealed interface AuthEvent { data class SignIn(val login: String, val password: String) : AuthEvent + data class SsoSignIn(val jwtToken: String) : AuthEvent data class SocialSignIn(val authType: AuthType) : AuthEvent data class OpenLink(val links: Map, val link: String) : AuthEvent object SignInBrowser : AuthEvent diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInUIState.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInUIState.kt index c2a5f915c..fc5615963 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInUIState.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInUIState.kt @@ -13,6 +13,10 @@ import org.openedx.core.domain.model.RegistrationField * @param loginSuccess is login succeed */ internal data class SignInUIState( + val isLoginRegistrationFormEnabled: Boolean = true, + val isSSOLoginEnabled: Boolean = false, + val ssoButtonTitle: String = "", + val isSSODefaultLoginButton: Boolean = false, val isFacebookAuthEnabled: Boolean = false, val isGoogleAuthEnabled: Boolean = false, val isMicrosoftAuthEnabled: Boolean = false, diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt index f271927e1..480c7b424 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInViewModel.kt @@ -1,5 +1,6 @@ package org.openedx.auth.presentation.signin +import android.content.res.Resources import android.app.Activity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager @@ -50,6 +51,7 @@ class SignInViewModel( private val appNotifier: AppNotifier, private val analytics: AuthAnalytics, private val oAuthHelper: OAuthHelper, + private val configuration: Config, private val router: AuthRouter, private val whatsNewGlobalManager: WhatsNewGlobalManager, private val calendarPreferences: CalendarPreferences, @@ -66,6 +68,10 @@ class SignInViewModel( private val _uiState = MutableStateFlow( SignInUIState( + isLoginRegistrationFormEnabled = config.isLoginRegistrationEnabled(), + isSSOLoginEnabled = config.isSSOLoginEnabled(), + ssoButtonTitle = config.getSSOButtonTitle(key = Resources.getSystem().getConfiguration().locales[0].language.uppercase(), ""), + isSSODefaultLoginButton = config.isSSODefaultLoginButton(), isFacebookAuthEnabled = config.getFacebookConfig().isEnabled(), isGoogleAuthEnabled = config.getGoogleConfig().isEnabled(), isMicrosoftAuthEnabled = config.getMicrosoftConfig().isEnabled(), @@ -141,6 +147,50 @@ class SignInViewModel( } } + fun ssoClicked(fragmentManager: FragmentManager) { + router.navigateToSSOWebContent( + fm = fragmentManager, + title = resourceManager.getString(org.openedx.core.R.string.core_sso_sign_in), + url = configuration.getSSOURL(), + ) + } + + fun ssoLogin(token: String) { + logEvent(AuthAnalyticsEvent.USER_SIGN_IN_CLICKED) + + + _uiState.update { it.copy(showProgress = true) } + viewModelScope.launch { + try { + interactor.ssoLogin("JWT $token") + _uiState.update { it.copy(loginSuccess = true) } + + setUserId() + logEvent( + AuthAnalyticsEvent.SIGN_IN_SUCCESS, + buildMap { + put( + AuthAnalyticsKey.METHOD.key, + AuthType.PASSWORD.methodName.lowercase() + ) + } + ) + } catch (e: Exception) { + if (e is EdxError.InvalidGrantException) { + _uiMessage.value = + UIMessage.SnackBarMessage(resourceManager.getString(CoreRes.string.core_error_invalid_grant)) + } else if (e.isInternetError()) { + _uiMessage.value = + UIMessage.SnackBarMessage(resourceManager.getString(CoreRes.string.core_error_no_connection)) + } else { + _uiMessage.value = + UIMessage.SnackBarMessage(resourceManager.getString(CoreRes.string.core_error_unknown_error)) + } + } + _uiState.update { it.copy(showProgress = false) } + } + } + private fun collectAppUpgradeEvent() { viewModelScope.launch { appNotifier.notifier.collect { event -> diff --git a/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt b/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt index e182f51d7..498807fd0 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/compose/SignInView.kt @@ -46,12 +46,14 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.testTagsAsResourceId +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp @@ -65,8 +67,10 @@ import org.openedx.auth.presentation.ui.SocialAuthView import org.openedx.core.extension.TextConverter import org.openedx.core.ui.BackBtn import org.openedx.core.ui.HandleUIMessage +import org.openedx.core.ui.HorizontalLine import org.openedx.core.ui.HyperlinkText import org.openedx.core.ui.OpenEdXButton +import org.openedx.core.ui.OpenEdXOutlinedButton import org.openedx.core.ui.displayCutoutForLandscape import org.openedx.core.ui.noRippleClickable import org.openedx.core.ui.theme.OpenEdXTheme @@ -171,20 +175,23 @@ internal fun LoginScreen( .displayCutoutForLandscape() .then(contentPaddings), ) { - Text( - modifier = Modifier.testTag("txt_sign_in_title"), - text = stringResource(id = coreR.string.core_sign_in), - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.displaySmall - ) - Text( - modifier = Modifier - .testTag("txt_sign_in_description") - .padding(top = 4.dp), - text = stringResource(id = R.string.auth_welcome_back), - color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.titleSmall - ) + if (state.isLoginRegistrationFormEnabled) { + Text( + modifier = Modifier.testTag("txt_sign_in_title"), + text = stringResource(id = coreR.string.core_sign_in), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.displaySmall + ) + Text( + modifier = Modifier + .testTag("txt_sign_in_description") + .padding(top = 4.dp), + text = stringResource(id = R.string.auth_welcome_back), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleSmall + ) + } + Spacer(modifier = Modifier.height(24.dp)) AuthForm( buttonWidth, @@ -225,112 +232,182 @@ private fun AuthForm( var isEmailError by rememberSaveable { mutableStateOf(false) } var isPasswordError by rememberSaveable { mutableStateOf(false) } - Column(horizontalAlignment = Alignment.CenterHorizontally) { - if (!state.isBrowserLoginEnabled) { - LoginTextField( - modifier = Modifier - .fillMaxWidth(), - title = stringResource(id = R.string.auth_email_username), - description = stringResource(id = R.string.auth_enter_email_username), - onValueChanged = { - login = it - isEmailError = false - }, - isError = isEmailError, - errorMessages = stringResource(id = R.string.auth_error_empty_username_email) - ) - - Spacer(modifier = Modifier.height(18.dp)) - PasswordTextField( - modifier = Modifier - .fillMaxWidth(), - onValueChanged = { - password = it - isPasswordError = false - }, - onPressDone = { - keyboardController?.hide() - if (password.isNotEmpty()) { - onEvent(AuthEvent.SignIn(login = login, password = password)) - } else { - isEmailError = login.isEmpty() - isPasswordError = password.isEmpty() - } - }, - isError = isPasswordError, - ) - } else { - Spacer(modifier = Modifier.height(40.dp)) - } - - Row( - Modifier - .fillMaxWidth() - .padding(top = 20.dp, bottom = 36.dp) - ) { + if (state.isLoginRegistrationFormEnabled) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { if (!state.isBrowserLoginEnabled) { - if (state.isLogistrationEnabled.not() && state.isRegistrationEnabled) { - Text( - modifier = Modifier - .testTag("txt_register") - .noRippleClickable { - onEvent(AuthEvent.RegisterClick) - }, - text = stringResource(id = coreR.string.core_register), - color = MaterialTheme.appColors.primary, - style = MaterialTheme.appTypography.labelLarge - ) - } - Spacer(modifier = Modifier.weight(1f)) - Text( + LoginTextField( modifier = Modifier - .testTag("txt_forgot_password") - .noRippleClickable { - onEvent(AuthEvent.ForgotPasswordClick) - }, - text = stringResource(id = R.string.auth_forgot_password), - color = MaterialTheme.appColors.infoVariant, - style = MaterialTheme.appTypography.labelLarge + .fillMaxWidth(), + title = stringResource(id = R.string.auth_email_username), + description = stringResource(id = R.string.auth_enter_email_username), + onValueChanged = { + login = it + isEmailError = false + }, + isError = isEmailError, + errorMessages = stringResource(id = R.string.auth_error_empty_username_email) ) - } - } - if (state.showProgress) { - CircularProgressIndicator(color = MaterialTheme.appColors.primary) - } else { - OpenEdXButton( - modifier = buttonWidth.testTag("btn_sign_in"), - text = stringResource(id = coreR.string.core_sign_in), - textColor = MaterialTheme.appColors.primaryButtonText, - backgroundColor = MaterialTheme.appColors.secondaryButtonBackground, - onClick = { - if (state.isBrowserLoginEnabled) { - onEvent(AuthEvent.SignInBrowser) - } else { + Spacer(modifier = Modifier.height(18.dp)) + PasswordTextField( + modifier = Modifier + .fillMaxWidth(), + onValueChanged = { + password = it + isPasswordError = false + }, + onPressDone = { keyboardController?.hide() - if (login.isNotEmpty() && password.isNotEmpty()) { + if (password.isNotEmpty()) { onEvent(AuthEvent.SignIn(login = login, password = password)) } else { isEmailError = login.isEmpty() isPasswordError = password.isEmpty() } + }, + isError = isPasswordError, + ) + } else { + Spacer(modifier = Modifier.height(40.dp)) + } + + Row( + Modifier + .fillMaxWidth() + .padding(top = 20.dp, bottom = 36.dp) + ) { + if (state.isLogistrationEnabled.not()) { + if (!state.isBrowserLoginEnabled) { + if (state.isLogistrationEnabled.not() && state.isRegistrationEnabled) { + Text( + modifier = Modifier + .testTag("txt_register") + .noRippleClickable { + onEvent(AuthEvent.RegisterClick) + }, + text = stringResource(id = coreR.string.core_register), + color = MaterialTheme.appColors.primary, + style = MaterialTheme.appTypography.labelLarge + ) + } + Spacer(modifier = Modifier.weight(1f)) + Text( + modifier = Modifier + .testTag("txt_forgot_password") + .noRippleClickable { + onEvent(AuthEvent.ForgotPasswordClick) + }, + text = stringResource(id = R.string.auth_forgot_password), + color = MaterialTheme.appColors.infoVariant, + style = MaterialTheme.appTypography.labelLarge + ) } } - ) - } - if (state.isSocialAuthEnabled) { - SocialAuthView( - modifier = buttonWidth, - isGoogleAuthEnabled = state.isGoogleAuthEnabled, - isFacebookAuthEnabled = state.isFacebookAuthEnabled, - isMicrosoftAuthEnabled = state.isMicrosoftAuthEnabled, - isSignIn = true, + } + + + if (state.showProgress) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } else { + OpenEdXButton( + modifier = buttonWidth.testTag("btn_sign_in"), + text = stringResource(id = coreR.string.core_sign_in), + textColor = MaterialTheme.appColors.primaryButtonText, + backgroundColor = MaterialTheme.appColors.secondaryButtonBackground, + onClick = { + if (state.isBrowserLoginEnabled) { + onEvent(AuthEvent.SignInBrowser) + } else { + keyboardController?.hide() + if (login.isNotEmpty() && password.isNotEmpty()) { + onEvent(AuthEvent.SignIn(login = login, password = password)) + } else { + isEmailError = login.isEmpty() + isPasswordError = password.isEmpty() + } + } + } + ) + } + if (state.isSocialAuthEnabled) { + SocialAuthView( + modifier = buttonWidth, + isGoogleAuthEnabled = state.isGoogleAuthEnabled, + isFacebookAuthEnabled = state.isFacebookAuthEnabled, + isMicrosoftAuthEnabled = state.isMicrosoftAuthEnabled, + isSignIn = true, ) { keyboardController?.hide() onEvent(AuthEvent.SocialSignIn(it)) } } } + } + if (state.isSSOLoginEnabled){ + Spacer(modifier = Modifier.height(18.dp)) + if (state.isLoginRegistrationFormEnabled){ + HorizontalLine() + Spacer(modifier = Modifier.height(18.dp)) + + Text( + modifier = Modifier + .testTag("txt_sso_header") + .padding(top = 4.dp) + .fillMaxWidth(), + text = stringResource(id = coreR.string.core_sign_in_sso_heading), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.headlineSmall, + textAlign = TextAlign.Center, + ) + Spacer(modifier = Modifier.height(18.dp)) + Text( + modifier = Modifier + .testTag("txt_sso_login_title") + .padding(top = 4.dp) + .fillMaxWidth(), + text = stringResource(id = org.openedx.core.R.string.core_sign_in_sso_login_title), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.titleLarge, + textAlign = TextAlign.Center + ) + Text( + modifier = Modifier + .testTag("txt_sso_login_subtitle") + .padding(top = 4.dp) + .fillMaxWidth(), + text = stringResource(id = org.openedx.core.R.string.core_sign_in_sso_login_subtitle), + color = MaterialTheme.appColors.textPrimary, + style = MaterialTheme.appTypography.bodyLarge, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(18.dp)) + } + + + if (state.showProgress) { + CircularProgressIndicator(color = MaterialTheme.appColors.primary) + } else { + if (state.isSSODefaultLoginButton){ + OpenEdXButton( + modifier = buttonWidth.testTag("btn_sso") + .fillMaxWidth(), + text = state.ssoButtonTitle, + onClick = { + onEvent(AuthEvent.SsoSignIn(jwtToken = "")) + } + ) + }else{ + OpenEdXOutlinedButton( + modifier = buttonWidth.testTag("btn_sso") + .fillMaxWidth(), + text = stringResource(id = coreR.string.core_sso_sign_in), + borderColor = MaterialTheme.appColors.primary, + textColor = MaterialTheme.appColors.textPrimary, + onClick = { onEvent(AuthEvent.SsoSignIn(jwtToken = "")) } + ) + } + } + } } @Composable @@ -457,6 +534,9 @@ private fun SignInScreenTabletPreview() { LoginScreen( windowSize = WindowSize(WindowType.Expanded, WindowType.Expanded), state = SignInUIState().copy( + isLoginRegistrationFormEnabled = false, + isSSOLoginEnabled = true, + isSSODefaultLoginButton = true, isSocialAuthEnabled = true, isFacebookAuthEnabled = true, isGoogleAuthEnabled = true, diff --git a/build.gradle b/build.gradle index 390d02699..ab7e6b940 100644 --- a/build.gradle +++ b/build.gradle @@ -40,6 +40,8 @@ ext { extented_spans_version = "1.3.0" + webkit_version = "1.11.0" + configHelper = new ConfigHelper(projectDir, getCurrentFlavor()) zip_version = '2.6.3' 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 f240b9531..af304db9d 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -8,15 +8,13 @@ import com.google.gson.JsonParser import org.openedx.core.domain.model.AgreementUrls import java.io.InputStreamReader -@Suppress("TooManyFunctions") class Config(context: Context) { private var configProperties: JsonObject = try { val inputStream = context.assets.open("config/config.json") - val config = JsonParser.parseReader(InputStreamReader(inputStream)) + val config = Gson().fromJson(InputStreamReader(inputStream), JsonObject::class.java) config.asJsonObject } catch (e: Exception) { - e.printStackTrace() JsonObject() } @@ -28,6 +26,10 @@ class Config(context: Context) { return getString(API_HOST_URL) } + fun getSSOURL(): String { + return getString(SSO_URL, "") + } + fun getUriScheme(): String { return getString(URI_SCHEME) } @@ -108,10 +110,6 @@ class Config(context: Context) { return getObjectOrNewInstance(UI_COMPONENTS, UIConfig::class.java) } - fun isRegistrationEnabled(): Boolean { - return getBoolean(REGISTRATION_ENABLED, true) - } - fun isBrowserLoginEnabled(): Boolean { return getBoolean(BROWSER_LOGIN, false) } @@ -120,6 +118,27 @@ class Config(context: Context) { return getBoolean(BROWSER_REGISTRATION, false) } + fun isRegistrationEnabled(): Boolean { + return getBoolean(REGISTRATION_ENABLED, true) + } + + fun isLoginRegistrationEnabled(): Boolean { + return getBoolean(LOGIN_REGISTRATION_ENABLED, true) + } + + fun isSSOLoginEnabled(): Boolean { + return getBoolean(SAML_SSO_LOGIN_ENABLED, false) + } + + fun isSSODefaultLoginButton(): Boolean { + return getBoolean(SAML_SSO_DEFAULT_LOGIN_BUTTON, false) + } + + fun getSSOButtonTitle(key: String, defaultValue: String): String{ + val element = getObject(SSO_BUTTON_TITLE) + return element?.asJsonObject?.get(key)?.asString ?: defaultValue + } + private fun getString(key: String, defaultValue: String = ""): String { val element = getObject(key) return if (element != null) { @@ -143,15 +162,13 @@ class Config(context: Context) { try { cls.getDeclaredConstructor().newInstance() } catch (e: InstantiationException) { - throw ConfigParsingException(e) + throw RuntimeException(e) } catch (e: IllegalAccessException) { - throw ConfigParsingException(e) + throw RuntimeException(e) } } } - class ConfigParsingException(cause: Throwable) : Exception(cause) - private fun getObject(key: String): JsonElement? { return configProperties.get(key) } @@ -159,6 +176,12 @@ class Config(context: Context) { companion object { private const val APPLICATION_ID = "APPLICATION_ID" private const val API_HOST_URL = "API_HOST_URL" + private const val SSO_URL = "SSO_URL" + private const val SSO_FINISHED_URL = "SSO_FINISHED_URL" + private const val SSO_BUTTON_TITLE = "SSO_BUTTON_TITLE" + private const val SAML_SSO_LOGIN_ENABLED = "SAML_SSO_LOGIN_ENABLED" + private const val SAML_SSO_DEFAULT_LOGIN_BUTTON = "SAML_SSO_DEFAULT_LOGIN_BUTTON" + private const val LOGIN_REGISTRATION_ENABLED = "LOGIN_REGISTRATION_ENABLED" private const val URI_SCHEME = "URI_SCHEME" private const val OAUTH_CLIENT_ID = "OAUTH_CLIENT_ID" private const val TOKEN_TYPE = "TOKEN_TYPE" @@ -173,9 +196,9 @@ class Config(context: Context) { private const val GOOGLE = "GOOGLE" private const val MICROSOFT = "MICROSOFT" private const val PRE_LOGIN_EXPERIENCE_ENABLED = "PRE_LOGIN_EXPERIENCE_ENABLED" - private const val REGISTRATION_ENABLED = "REGISTRATION_ENABLED" private const val BROWSER_LOGIN = "BROWSER_LOGIN" private const val BROWSER_REGISTRATION = "BROWSER_REGISTRATION" + private const val REGISTRATION_ENABLED = "REGISTRATION_ENABLED" private const val DISCOVERY = "DISCOVERY" private const val PROGRAM = "PROGRAM" private const val DASHBOARD = "DASHBOARD" @@ -188,4 +211,4 @@ class Config(context: Context) { NATIVE, WEBVIEW } -} +} \ No newline at end of file diff --git a/core/src/main/java/org/openedx/core/presentation/global/webview/SSOWebContentFragment.kt b/core/src/main/java/org/openedx/core/presentation/global/webview/SSOWebContentFragment.kt new file mode 100644 index 000000000..6812a30e9 --- /dev/null +++ b/core/src/main/java/org/openedx/core/presentation/global/webview/SSOWebContentFragment.kt @@ -0,0 +1,71 @@ +package org.openedx.core.presentation.global.webview + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.core.os.bundleOf +import androidx.fragment.app.Fragment +import androidx.fragment.app.setFragmentResult +import org.koin.android.ext.android.inject +import org.openedx.core.config.Config +import org.openedx.core.ui.SSOWebContentScreen +import org.openedx.foundation.presentation.rememberWindowSize +import org.openedx.core.ui.theme.OpenEdXTheme + +class SSOWebContentFragment : Fragment() { + + private val config: Config by inject() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ) = ComposeView(requireContext()).apply { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + OpenEdXTheme { + val windowSize = rememberWindowSize() + SSOWebContentScreen( + windowSize = windowSize, + url = config.getSSOURL(), + uriScheme = requireArguments().getString(ARG_TITLE, ""), + title = "", + onBackClick = { + // use it to close the webView + requireActivity().supportFragmentManager.popBackStack() + }, + onWebPageLoaded = { + }, + onWebPageUpdated = { + val token = it + if (token.isNotEmpty()){ + setFragmentResult("requestKey", bundleOf("bundleKey" to token)) + requireActivity().supportFragmentManager.popBackStack() + } + + }) + } + } + } + +// override fun onDestroy() { +// super.onDestroy() +// CookieManager.getInstance().flush() +// } + + companion object { + private const val ARG_TITLE = "argTitle" + private const val ARG_URL = "argUrl" + + fun newInstance(title: String, url: String): SSOWebContentFragment { + val fragment = SSOWebContentFragment() + fragment.arguments = bundleOf( + ARG_TITLE to title, + ARG_URL to url, + ) + return fragment + } + } +} 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 fbbead83e..e29ad12b2 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -1080,6 +1080,15 @@ fun OfflineModeDialog( } } +@Composable +fun HorizontalLine() { + Divider( + color = Color.LightGray.copy(alpha = 0.5f), // Set the color of the line + thickness = 1.dp, // Set the thickness of the line + modifier = Modifier.fillMaxWidth() // Make it span the entire width + ) +} + @Composable fun OpenEdXButton( modifier: Modifier = Modifier.fillMaxWidth(), diff --git a/core/src/main/java/org/openedx/core/ui/SSOWebContentScreen.kt b/core/src/main/java/org/openedx/core/ui/SSOWebContentScreen.kt new file mode 100644 index 000000000..fbc6d3976 --- /dev/null +++ b/core/src/main/java/org/openedx/core/ui/SSOWebContentScreen.kt @@ -0,0 +1,187 @@ +package org.openedx.core.ui + +import android.annotation.SuppressLint +import android.os.Message +import android.webkit.CookieManager +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.zIndex +import org.openedx.core.ui.theme.appColors +import org.openedx.foundation.presentation.WindowSize +import org.openedx.foundation.presentation.windowSizeValue + + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun SSOWebContentScreen( + windowSize: WindowSize, + url: String, + uriScheme: String, + title: String, + onBackClick: () -> Unit, + onWebPageLoaded: () -> Unit, + onWebPageUpdated: (String) -> Unit = {}, +){ + val webView = SSOWebView( + url = url, + uriScheme = uriScheme, + onWebPageLoaded = onWebPageLoaded, + onWebPageUpdated = onWebPageUpdated + ) + val screenWidth by remember(key1 = windowSize) { + mutableStateOf( + windowSize.windowSizeValue( + expanded = Modifier.widthIn(Dp.Unspecified, 560.dp), + compact = Modifier.fillMaxWidth(), + ) + ) + } + + Box( + modifier = Modifier + .fillMaxWidth() + .statusBarsInset() + .displayCutoutForLandscape(), + contentAlignment = Alignment.TopCenter + ) { + Column(screenWidth) { + Box( + Modifier + .fillMaxWidth() + .zIndex(1f), + contentAlignment = Alignment.CenterStart + ) { + Toolbar( + label = title, + canShowBackBtn = true, + onBackClick = onBackClick + ) + } + Surface( + Modifier.fillMaxSize(), + color = MaterialTheme.appColors.background + ) { + + val webViewAlpha by rememberSaveable { mutableFloatStateOf(1f) } + Surface( + Modifier.alpha(webViewAlpha), + color = MaterialTheme.appColors.background + ) { + AndroidView( + modifier = Modifier + .background(MaterialTheme.appColors.background), + factory = { + webView + } + ) + } + + } + } + } + + + +} + +@SuppressLint("SetJavaScriptEnabled", "ComposableNaming") +@Composable +fun SSOWebView( + url: String, + uriScheme: String, + onWebPageLoaded: () -> Unit, + onWebPageUpdated: (String) -> Unit = {}, +): WebView { + val context = LocalContext.current + + return remember { + WebView(context).apply { + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + url?.let { + val jwtToken = getCookie(url, "edx-jwt-cookie-header-payload") + getCookie(url, "edx-jwt-cookie-signature") + onWebPageUpdated(jwtToken) + } + } + + override fun onReceivedLoginRequest( + view: WebView?, + realm: String?, + account: String?, + args: String? + ) { + super.onReceivedLoginRequest(view, realm, account, args) + } + + override fun onFormResubmission( + view: WebView?, + dontResend: Message?, + resend: Message? + ) { + super.onFormResubmission(view, dontResend, resend) + } + override fun onPageCommitVisible(view: WebView?, url: String?) { + super.onPageCommitVisible(view, url) + onWebPageLoaded() + } + + } + + with(settings) { + javaScriptEnabled = true + useWideViewPort = true + loadWithOverviewMode = true + builtInZoomControls = false + setSupportZoom(true) + loadsImagesAutomatically = true + domStorageEnabled = true + + } + isVerticalScrollBarEnabled = true + isHorizontalScrollBarEnabled = true + + loadUrl(url) + } + } +} + +fun getCookie(siteName: String?, cookieName: String?): String? { + var cookieValue: String? = "" + + val cookieManager = CookieManager.getInstance() + val cookies = cookieManager.getCookie(siteName) + val temp = cookies.split(";".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + for (ar1 in temp) { + if (ar1.contains(cookieName!!)) { + val temp1 = ar1.split("=".toRegex()).dropLastWhile { it.isEmpty() } + .toTypedArray() + cookieValue = temp1[1] + break + } + } + return cookieValue +} \ No newline at end of file diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index c8d529afa..0744e05c6 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -187,4 +187,8 @@ Not Synced Syncing to calendar… Next + Sign in with SSO + OR + Sign in + Sign in through the national unified sign-on service diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index 4d1d694ec..bf24d89cc 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -1,10 +1,19 @@ API_HOST_URL: 'http://localhost:8000' +SSO_URL: 'http://localhost:8000' +SSO_FINISHED_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' +LOGIN_REGISTRATION_ENABLED: false +SAML_SSO_LOGIN_ENABLED: true +SAML_SSO_DEFAULT_LOGIN_BUTTON: true + +SSO_BUTTON_TITLE: + AR: "الدخول عبر SSO" + EN: "Sign in with SSO" # Keep empty to hide setting AGREEMENT_URLS: diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index 4d1d694ec..eeaf3dfa9 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -1,10 +1,19 @@ API_HOST_URL: 'http://localhost:8000' +SSO_URL: 'http://localhost:8000' +SSO_FINISHED_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' +LOGIN_REGISTRATION_ENABLED: true +SAML_SSO_LOGIN_ENABLED: false +SAML_SSO_DEFAULT_LOGIN_BUTTON: false + +SSO_BUTTON_TITLE: + AR: "الدخول عبر SSO" + EN: "Sign in with SSO" # Keep empty to hide setting AGREEMENT_URLS: @@ -86,3 +95,4 @@ UI_COMPONENTS: COURSE_DROPDOWN_NAVIGATION_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false COURSE_DOWNLOAD_QUEUE_SCREEN: false + diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index 4d1d694ec..eeaf3dfa9 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -1,10 +1,19 @@ API_HOST_URL: 'http://localhost:8000' +SSO_URL: 'http://localhost:8000' +SSO_FINISHED_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' +LOGIN_REGISTRATION_ENABLED: true +SAML_SSO_LOGIN_ENABLED: false +SAML_SSO_DEFAULT_LOGIN_BUTTON: false + +SSO_BUTTON_TITLE: + AR: "الدخول عبر SSO" + EN: "Sign in with SSO" # Keep empty to hide setting AGREEMENT_URLS: @@ -86,3 +95,4 @@ UI_COMPONENTS: COURSE_DROPDOWN_NAVIGATION_ENABLED: false COURSE_UNIT_PROGRESS_ENABLED: false COURSE_DOWNLOAD_QUEUE_SCREEN: false + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ec34fd6a7..c7a3bea0a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,4 +1,4 @@ -#Fri May 03 13:24:00 EEST 2024 +#Sun Feb 09 11:58:34 EET 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip