From eb3f8a976dc82406bc62f6367d719fa9c0f3135e Mon Sep 17 00:00:00 2001 From: RawanMatar89 <41669180+RawanMatar89@users.noreply.github.com> Date: Mon, 14 Apr 2025 22:29:00 +0300 Subject: [PATCH 1/6] revert: Support for login and registration via a browser custom tab (#371) This reverts commit 4566b1a0 --- app/src/main/AndroidManifest.xml | 6 - .../main/java/org/openedx/app/AppActivity.kt | 22 +-- .../main/java/org/openedx/app/di/AppModule.kt | 2 - .../java/org/openedx/app/di/ScreenModule.kt | 5 +- auth/build.gradle | 1 - .../java/org/openedx/auth/data/api/AuthApi.kt | 11 -- .../org/openedx/auth/data/model/AuthType.kt | 1 - .../auth/data/repository/AuthRepository.kt | 10 -- .../auth/domain/interactor/AuthInteractor.kt | 4 - .../logistration/LogistrationFragment.kt | 18 +-- .../logistration/LogistrationViewModel.kt | 21 --- .../presentation/signin/SignInFragment.kt | 17 +-- .../auth/presentation/signin/SignInUIState.kt | 3 - .../presentation/signin/SignInViewModel.kt | 38 +---- .../presentation/signin/compose/SignInView.kt | 132 +++++++----------- .../presentation/signup/SignUpFragment.kt | 1 - .../presentation/sso/BrowserAuthHelper.kt | 35 ----- .../auth/presentation/sso/OAuthHelper.kt | 1 - .../signin/SignInViewModelTest.kt | 36 ++--- .../java/org/openedx/core/ApiConstants.kt | 11 -- .../java/org/openedx/core/config/Config.kt | 10 -- default_config/dev/config.yaml | 4 - default_config/prod/config.yaml | 4 - default_config/stage/config.yaml | 4 - .../0001-strategy-for-data-streams.rst | 0 docs/how-tos/auth-using-browser.rst | 48 ------- docs/how-tos/index.rst | 8 -- 27 files changed, 69 insertions(+), 384 deletions(-) delete mode 100644 auth/src/main/java/org/openedx/auth/presentation/sso/BrowserAuthHelper.kt rename docs/{decisions => }/0001-strategy-for-data-streams.rst (100%) delete mode 100644 docs/how-tos/auth-using-browser.rst delete mode 100644 docs/how-tos/index.rst diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 65c64e538..831fe4a86 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -48,12 +48,6 @@ - - - - - - diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index cbb496501..5d065d6fe 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -3,7 +3,6 @@ package org.openedx.app import android.content.Intent import android.content.res.Configuration import android.graphics.Color -import android.net.Uri import android.os.Bundle import android.view.View import android.view.WindowManager @@ -25,7 +24,6 @@ import org.openedx.app.databinding.ActivityAppBinding import org.openedx.app.deeplink.DeepLink import org.openedx.auth.presentation.logistration.LogistrationFragment import org.openedx.auth.presentation.signin.SignInFragment -import org.openedx.core.ApiConstants import org.openedx.core.data.storage.CorePreferences import org.openedx.core.presentation.dialog.downloaddialog.DownloadDialogManager import org.openedx.core.presentation.global.InsetHolder @@ -66,18 +64,6 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { private var _insetCutout = 0 private var _windowSize = WindowSize(WindowType.Compact, WindowType.Compact) - private val authCode: String? - get() { - val data = intent?.data - if ( - data is Uri && - data.scheme == BuildConfig.APPLICATION_ID && - data.host == ApiConstants.BrowserLogin.REDIRECT_HOST - ) { - return data.getQueryParameter(ApiConstants.BrowserLogin.CODE_QUERY_PARAM) - } - return null - } private val branchCallback = BranchUniversalReferralInitListener { branchUniversalObject, _, error -> @@ -168,10 +154,10 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { if (savedInstanceState == null) { when { corePreferencesManager.user == null -> { - val fragment = if (viewModel.isLogistrationEnabled && authCode == null) { + val fragment = if (viewModel.isLogistrationEnabled) { LogistrationFragment() } else { - SignInFragment.newInstance(null, null, authCode = authCode) + SignInFragment() } addFragment(fragment) } @@ -218,10 +204,6 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { super.onNewIntent(intent) this.intent = intent - if (authCode != null) { - addFragment(SignInFragment.newInstance(null, null, authCode = authCode)) - } - val extras = intent?.extras if (extras?.containsKey(DeepLink.Keys.NOTIFICATION_TYPE.value) == true) { handlePushNotification(extras) 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 b4633cc27..bf85f48d8 100644 --- a/app/src/main/java/org/openedx/app/di/AppModule.kt +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt @@ -23,7 +23,6 @@ import org.openedx.app.room.DatabaseManager import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics import org.openedx.auth.presentation.AuthRouter -import org.openedx.auth.presentation.sso.BrowserAuthHelper import org.openedx.auth.presentation.sso.FacebookAuthHelper import org.openedx.auth.presentation.sso.GoogleAuthHelper import org.openedx.auth.presentation.sso.MicrosoftAuthHelper @@ -214,7 +213,6 @@ val appModule = module { factory { FacebookAuthHelper() } factory { GoogleAuthHelper(get()) } factory { MicrosoftAuthHelper() } - factory { BrowserAuthHelper(get()) } factory { OAuthHelper(get(), get(), get()) } factory { FileUtil(get(), get().getString(R.string.app_name)) } 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 464007259..8c27020d5 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -103,11 +103,10 @@ val screenModule = module { get(), get(), get(), - get(), ) } - viewModel { (courseId: String?, infoType: String?, authCode: String) -> + viewModel { (courseId: String?, infoType: String?) -> SignInViewModel( get(), get(), @@ -122,10 +121,8 @@ val screenModule = module { get(), get(), get(), - get(), courseId, infoType, - authCode, ) } diff --git a/auth/build.gradle b/auth/build.gradle index 6b11037a2..470174991 100644 --- a/auth/build.gradle +++ b/auth/build.gradle @@ -54,7 +54,6 @@ android { dependencies { implementation project(path: ':core') - implementation 'androidx.browser:browser:1.7.0' implementation "androidx.credentials:credentials:1.3.0" implementation "androidx.credentials:credentials-play-services-auth:1.3.0" implementation "com.facebook.android:facebook-login:16.2.0" diff --git a/auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt b/auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt index b837648fe..673168c57 100644 --- a/auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt +++ b/auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt @@ -37,17 +37,6 @@ interface AuthApi { @Field("asymmetric_jwt") isAsymmetricJwt: Boolean = true, ): AuthResponse - @FormUrlEncoded - @POST(ApiConstants.URL_ACCESS_TOKEN) - suspend fun getAccessTokenFromCode( - @Field("grant_type") grantType: String, - @Field("client_id") clientId: String, - @Field("code") code: String, - @Field("redirect_uri") redirectUri: String, - @Field("token_type") tokenType: String, - @Field("asymmetric_jwt") isAsymmetricJwt: Boolean = true, - ): AuthResponse - @FormUrlEncoded @POST(ApiConstants.URL_ACCESS_TOKEN) fun refreshAccessToken( diff --git a/auth/src/main/java/org/openedx/auth/data/model/AuthType.kt b/auth/src/main/java/org/openedx/auth/data/model/AuthType.kt index c56ba0cf1..5addd621c 100644 --- a/auth/src/main/java/org/openedx/auth/data/model/AuthType.kt +++ b/auth/src/main/java/org/openedx/auth/data/model/AuthType.kt @@ -13,5 +13,4 @@ enum class AuthType(val postfix: String, val methodName: String) { GOOGLE(ApiConstants.AUTH_TYPE_GOOGLE, "Google"), FACEBOOK(ApiConstants.AUTH_TYPE_FB, "Facebook"), MICROSOFT(ApiConstants.AUTH_TYPE_MICROSOFT, "Microsoft"), - BROWSER(ApiConstants.AUTH_TYPE_BROWSER, "Browser") } 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..617006afe 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 @@ -43,16 +43,6 @@ class AuthRepository( .processAuthResponse() } - suspend fun browserAuthCodeLogin(code: String) { - api.getAccessTokenFromCode( - grantType = ApiConstants.GRANT_TYPE_CODE, - clientId = config.getOAuthClientId(), - code = code, - redirectUri = "${config.getAppId()}://${ApiConstants.BrowserLogin.REDIRECT_HOST}", - tokenType = config.getAccessTokenType(), - ).mapToDomain().processAuthResponse() - } - suspend fun getRegistrationFields(): List { return api.getRegistrationFields().fields?.map { it.mapToDomain() } ?: emptyList() } 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..cdce0dbdf 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 @@ -18,10 +18,6 @@ class AuthInteractor(private val repository: AuthRepository) { repository.socialLogin(token, authType) } - suspend fun loginAuthCode(authCode: String) { - repository.browserAuthCodeLogin(authCode) - } - suspend fun getRegistrationFields(): List { return repository.getRegistrationFields() } diff --git a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt index f8dbba635..a05951ca4 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt @@ -42,7 +42,6 @@ import androidx.fragment.app.Fragment import org.koin.androidx.viewmodel.ext.android.viewModel import org.koin.core.parameter.parametersOf import org.openedx.auth.R -import org.openedx.core.ApiConstants import org.openedx.core.ui.AuthButtonsPanel import org.openedx.core.ui.SearchBar import org.openedx.core.ui.displayCutoutForLandscape @@ -51,7 +50,6 @@ import org.openedx.core.ui.theme.OpenEdXTheme import org.openedx.core.ui.theme.appColors import org.openedx.core.ui.theme.appTypography import org.openedx.core.ui.theme.compose.LogistrationLogoView -import org.openedx.foundation.utils.UrlUtils class LogistrationFragment : Fragment() { @@ -69,22 +67,10 @@ class LogistrationFragment : Fragment() { OpenEdXTheme { LogistrationScreen( onSignInClick = { - if (viewModel.isBrowserLoginEnabled) { - viewModel.signInBrowser(requireActivity()) - } else { - viewModel.navigateToSignIn(parentFragmentManager) - } + viewModel.navigateToSignIn(parentFragmentManager) }, onRegisterClick = { - if (viewModel.isBrowserRegistrationEnabled) { - UrlUtils.openInBrowser( - activity = context, - apiHostUrl = viewModel.apiHostUrl, - url = ApiConstants.URL_REGISTER_BROWSER, - ) - } else { - viewModel.navigateToSignUp(parentFragmentManager) - } + viewModel.navigateToSignUp(parentFragmentManager) }, onSearchClick = { querySearch -> viewModel.navigateToDiscovery(parentFragmentManager, querySearch) diff --git a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt index d7ca6e894..2b9ca07e2 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationViewModel.kt @@ -1,16 +1,11 @@ package org.openedx.auth.presentation.logistration -import android.app.Activity import androidx.fragment.app.FragmentManager -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.launch import org.openedx.auth.presentation.AuthAnalytics import org.openedx.auth.presentation.AuthAnalyticsEvent import org.openedx.auth.presentation.AuthAnalyticsKey import org.openedx.auth.presentation.AuthRouter -import org.openedx.auth.presentation.sso.BrowserAuthHelper import org.openedx.core.config.Config -import org.openedx.core.utils.Logger import org.openedx.foundation.extension.takeIfNotEmpty import org.openedx.foundation.presentation.BaseViewModel @@ -19,16 +14,10 @@ class LogistrationViewModel( private val router: AuthRouter, private val config: Config, private val analytics: AuthAnalytics, - private val browserAuthHelper: BrowserAuthHelper, ) : BaseViewModel() { - private val logger = Logger("LogistrationViewModel") - private val discoveryTypeWebView get() = config.getDiscoveryConfig().isViewTypeWebView() val isRegistrationEnabled get() = config.isRegistrationEnabled() - val isBrowserRegistrationEnabled get() = config.isBrowserRegistrationEnabled() - val isBrowserLoginEnabled get() = config.isBrowserLoginEnabled() - val apiHostUrl get() = config.getApiHostURL() init { logLogistrationScreenEvent() @@ -39,16 +28,6 @@ class LogistrationViewModel( logEvent(AuthAnalyticsEvent.SIGN_IN_CLICKED) } - fun signInBrowser(activityContext: Activity) { - viewModelScope.launch { - runCatching { - browserAuthHelper.signIn(activityContext) - }.onFailure { - logger.e { "Browser auth error: $it" } - } - } - } - fun navigateToSignUp(parentFragmentManager: FragmentManager) { router.navigateToSignUp(parentFragmentManager, courseId, null) logEvent(AuthAnalyticsEvent.REGISTER_CLICKED) 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..8f55d334b 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 @@ -25,8 +25,7 @@ class SignInFragment : Fragment() { private val viewModel: SignInViewModel by viewModel { parametersOf( requireArguments().getString(ARG_COURSE_ID, ""), - requireArguments().getString(ARG_INFO_TYPE, ""), - requireArguments().getString(ARG_AUTH_CODE, ""), + requireArguments().getString(ARG_INFO_TYPE, "") ) } @@ -44,9 +43,6 @@ class SignInFragment : Fragment() { val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState(null) if (appUpgradeEvent == null) { - if (viewModel.authCode != "" && !state.loginFailure && !state.loginSuccess) { - viewModel.signInAuthCode(viewModel.authCode) - } LoginScreen( windowSize = windowSize, state = state, @@ -63,10 +59,6 @@ class SignInFragment : Fragment() { viewModel.navigateToForgotPassword(parentFragmentManager) } - AuthEvent.SignInBrowser -> { - viewModel.signInBrowser(requireActivity()) - } - AuthEvent.RegisterClick -> { viewModel.navigateToSignUp(parentFragmentManager) } @@ -100,13 +92,11 @@ class SignInFragment : Fragment() { companion object { private const val ARG_COURSE_ID = "courseId" private const val ARG_INFO_TYPE = "info_type" - private const val ARG_AUTH_CODE = "auth_code" - fun newInstance(courseId: String?, infoType: String?, authCode: String? = null): SignInFragment { + fun newInstance(courseId: String?, infoType: String?): SignInFragment { val fragment = SignInFragment() fragment.arguments = bundleOf( ARG_COURSE_ID to courseId, - ARG_INFO_TYPE to infoType, - ARG_AUTH_CODE to authCode, + ARG_INFO_TYPE to infoType ) return fragment } @@ -117,7 +107,6 @@ internal sealed interface AuthEvent { data class SignIn(val login: String, val password: String) : AuthEvent data class SocialSignIn(val authType: AuthType) : AuthEvent data class OpenLink(val links: Map, val link: String) : AuthEvent - object SignInBrowser : AuthEvent object RegisterClick : AuthEvent object ForgotPasswordClick : AuthEvent object BackClick : 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..7d472882f 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 @@ -17,12 +17,9 @@ internal data class SignInUIState( val isGoogleAuthEnabled: Boolean = false, val isMicrosoftAuthEnabled: Boolean = false, val isSocialAuthEnabled: Boolean = false, - val isBrowserLoginEnabled: Boolean = false, - val isBrowserRegistrationEnabled: Boolean = false, val isLogistrationEnabled: Boolean = false, val isRegistrationEnabled: Boolean = true, val showProgress: Boolean = false, val loginSuccess: Boolean = false, val agreement: RegistrationField? = null, - val loginFailure: 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..5cc08b47e 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,6 +1,5 @@ package org.openedx.auth.presentation.signin -import android.app.Activity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.LiveData @@ -21,7 +20,6 @@ import org.openedx.auth.presentation.AuthAnalytics import org.openedx.auth.presentation.AuthAnalyticsEvent import org.openedx.auth.presentation.AuthAnalyticsKey import org.openedx.auth.presentation.AuthRouter -import org.openedx.auth.presentation.sso.BrowserAuthHelper import org.openedx.auth.presentation.sso.OAuthHelper import org.openedx.core.Validator import org.openedx.core.config.Config @@ -55,11 +53,9 @@ class SignInViewModel( private val calendarPreferences: CalendarPreferences, private val calendarInteractor: CalendarInteractor, agreementProvider: AgreementProvider, - private val browserAuthHelper: BrowserAuthHelper, - val config: Config, + config: Config, val courseId: String?, val infoType: String?, - val authCode: String, ) : BaseViewModel() { private val logger = Logger("SignInViewModel") @@ -69,8 +65,6 @@ class SignInViewModel( isFacebookAuthEnabled = config.getFacebookConfig().isEnabled(), isGoogleAuthEnabled = config.getGoogleConfig().isEnabled(), isMicrosoftAuthEnabled = config.getMicrosoftConfig().isEnabled(), - isBrowserLoginEnabled = config.isBrowserLoginEnabled(), - isBrowserRegistrationEnabled = config.isBrowserRegistrationEnabled(), isSocialAuthEnabled = config.isSocialAuthEnabled(), isLogistrationEnabled = config.isPreLoginExperienceEnabled(), isRegistrationEnabled = config.isRegistrationEnabled(), @@ -164,41 +158,11 @@ class SignInViewModel( } } - fun signInBrowser(activityContext: Activity) { - _uiState.update { it.copy(showProgress = true) } - viewModelScope.launch { - runCatching { - browserAuthHelper.signIn(activityContext) - }.onFailure { - logger.e { "Browser auth error: $it" } - } - } - } - fun navigateToSignUp(parentFragmentManager: FragmentManager) { router.navigateToSignUp(parentFragmentManager, null, null) logEvent(AuthAnalyticsEvent.REGISTER_CLICKED) } - fun signInAuthCode(authCode: String) { - _uiState.update { it.copy(showProgress = true) } - viewModelScope.launch { - runCatching { - interactor.loginAuthCode(authCode) - } - .onFailure { - logger.e { "OAuth2 code error: $it" } - onUnknownError() - _uiState.update { it.copy(loginFailure = true) } - }.onSuccess { - _uiState.update { it.copy(loginSuccess = true) } - setUserId() - appNotifier.send(SignInEvent()) - _uiState.update { it.copy(showProgress = false) } - } - } - } - fun navigateToForgotPassword(parentFragmentManager: FragmentManager) { router.navigateToRestorePassword(parentFragmentManager) logEvent(AuthAnalyticsEvent.FORGOT_PASSWORD_CLICKED) 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..d4608e4f8 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 @@ -226,73 +226,67 @@ private fun AuthForm( 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) - ) + 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)) - } + 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, + ) Row( Modifier .fillMaxWidth() .padding(top = 20.dp, bottom = 36.dp) ) { - 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)) + if (state.isLogistrationEnabled.not() && state.isRegistrationEnabled) { Text( modifier = Modifier - .testTag("txt_forgot_password") + .testTag("txt_register") .noRippleClickable { - onEvent(AuthEvent.ForgotPasswordClick) + onEvent(AuthEvent.RegisterClick) }, - text = stringResource(id = R.string.auth_forgot_password), - color = MaterialTheme.appColors.infoVariant, + 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.showProgress) { @@ -304,16 +298,12 @@ private fun AuthForm( textColor = MaterialTheme.appColors.primaryButtonText, backgroundColor = MaterialTheme.appColors.secondaryButtonBackground, onClick = { - if (state.isBrowserLoginEnabled) { - onEvent(AuthEvent.SignInBrowser) + keyboardController?.hide() + if (login.isNotEmpty() && password.isNotEmpty()) { + onEvent(AuthEvent.SignIn(login = login, password = password)) } else { - keyboardController?.hide() - if (login.isNotEmpty() && password.isNotEmpty()) { - onEvent(AuthEvent.SignIn(login = login, password = password)) - } else { - isEmailError = login.isEmpty() - isPasswordError = password.isEmpty() - } + isEmailError = login.isEmpty() + isPasswordError = password.isEmpty() } } ) @@ -431,24 +421,6 @@ private fun SignInScreenPreview() { } } -@Preview(uiMode = UI_MODE_NIGHT_NO) -@Preview(uiMode = UI_MODE_NIGHT_YES) -@Preview(name = "NEXUS_5_Light", device = Devices.NEXUS_5, uiMode = UI_MODE_NIGHT_NO) -@Preview(name = "NEXUS_5_Dark", device = Devices.NEXUS_5, uiMode = UI_MODE_NIGHT_YES) -@Composable -private fun SignInUsingBrowserScreenPreview() { - OpenEdXTheme { - LoginScreen( - windowSize = WindowSize(WindowType.Compact, WindowType.Compact), - state = SignInUIState().copy( - isBrowserLoginEnabled = true, - ), - uiMessage = null, - onEvent = {}, - ) - } -} - @Preview(name = "NEXUS_9_Light", device = Devices.NEXUS_9, uiMode = UI_MODE_NIGHT_NO) @Preview(name = "NEXUS_9_Night", device = Devices.NEXUS_9, uiMode = UI_MODE_NIGHT_YES) @Composable diff --git a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt index a87ffef3e..150eacb1a 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/signup/SignUpFragment.kt @@ -66,7 +66,6 @@ class SignUpFragment : Fragment() { this@SignUpFragment, authType ) - AuthType.BROWSER -> null } }, onFieldUpdated = { key, value -> diff --git a/auth/src/main/java/org/openedx/auth/presentation/sso/BrowserAuthHelper.kt b/auth/src/main/java/org/openedx/auth/presentation/sso/BrowserAuthHelper.kt deleted file mode 100644 index 1022da676..000000000 --- a/auth/src/main/java/org/openedx/auth/presentation/sso/BrowserAuthHelper.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.openedx.auth.presentation.sso - -import android.app.Activity -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.net.Uri -import androidx.annotation.WorkerThread -import androidx.browser.customtabs.CustomTabsIntent -import org.openedx.core.ApiConstants -import org.openedx.core.config.Config -import org.openedx.core.utils.Logger - -class BrowserAuthHelper(private val config: Config) { - - private val logger = Logger(TAG) - - @WorkerThread - suspend fun signIn(activityContext: Activity) { - logger.d { "Browser-based auth initiated" } - val uri = Uri.parse("${config.getApiHostURL()}${ApiConstants.URL_AUTHORIZE}").buildUpon() - .appendQueryParameter("client_id", config.getOAuthClientId()) - .appendQueryParameter( - "redirect_uri", - "${activityContext.packageName}://${ApiConstants.BrowserLogin.REDIRECT_HOST}" - ) - .appendQueryParameter("response_type", ApiConstants.BrowserLogin.RESPONSE_TYPE).build() - val intent = - CustomTabsIntent.Builder().setUrlBarHidingEnabled(true).setShowTitle(true).build() - intent.intent.setFlags(FLAG_ACTIVITY_NEW_TASK) - intent.launchUrl(activityContext, uri) - } - - private companion object { - const val TAG = "BrowserAuthHelper" - } -} diff --git a/auth/src/main/java/org/openedx/auth/presentation/sso/OAuthHelper.kt b/auth/src/main/java/org/openedx/auth/presentation/sso/OAuthHelper.kt index ccb094fae..776df7c46 100644 --- a/auth/src/main/java/org/openedx/auth/presentation/sso/OAuthHelper.kt +++ b/auth/src/main/java/org/openedx/auth/presentation/sso/OAuthHelper.kt @@ -21,7 +21,6 @@ class OAuthHelper( AuthType.GOOGLE -> googleAuthHelper.socialAuth(fragment.requireActivity()) AuthType.FACEBOOK -> facebookAuthHelper.socialAuth(fragment) AuthType.MICROSOFT -> microsoftAuthHelper.socialAuth(fragment.requireActivity()) - AuthType.BROWSER -> null } } diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt index 52c9e96a7..48480e310 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt @@ -26,7 +26,6 @@ import org.openedx.auth.domain.interactor.AuthInteractor import org.openedx.auth.presentation.AgreementProvider import org.openedx.auth.presentation.AuthAnalytics import org.openedx.auth.presentation.AuthRouter -import org.openedx.auth.presentation.sso.BrowserAuthHelper import org.openedx.auth.presentation.sso.OAuthHelper import org.openedx.core.Validator import org.openedx.core.config.Config @@ -67,7 +66,6 @@ class SignInViewModelTest { private val whatsNewGlobalManager = mockk() private val calendarInteractor = mockk() private val calendarPreferences = mockk() - private val browserAuthHelper = mockk() private val invalidCredential = "Invalid credentials" private val noInternet = "Slow or no internet connection" @@ -97,8 +95,6 @@ class SignInViewModelTest { coEvery { calendarInteractor.clearCalendarCachedData() } returns Unit every { analytics.logScreenEvent(any(), any()) } returns Unit every { config.isRegistrationEnabled() } returns true - every { config.isBrowserLoginEnabled() } returns false - every { config.isBrowserRegistrationEnabled() } returns false } @After @@ -124,12 +120,10 @@ class SignInViewModelTest { config = config, router = router, whatsNewGlobalManager = whatsNewGlobalManager, - browserAuthHelper = browserAuthHelper, courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences, - authCode = "", + calendarPreferences = calendarPreferences ) viewModel.login("", "") coVerify(exactly = 0) { interactor.login(any(), any()) } @@ -162,12 +156,10 @@ class SignInViewModelTest { config = config, router = router, whatsNewGlobalManager = whatsNewGlobalManager, - browserAuthHelper = browserAuthHelper, courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences, - authCode = "", + calendarPreferences = calendarPreferences ) viewModel.login("acc@test.o", "") coVerify(exactly = 0) { interactor.login(any(), any()) } @@ -200,12 +192,10 @@ class SignInViewModelTest { config = config, router = router, whatsNewGlobalManager = whatsNewGlobalManager, - browserAuthHelper = browserAuthHelper, courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences, - authCode = "", + calendarPreferences = calendarPreferences ) viewModel.login("acc@test.org", "") @@ -237,12 +227,10 @@ class SignInViewModelTest { config = config, router = router, whatsNewGlobalManager = whatsNewGlobalManager, - browserAuthHelper = browserAuthHelper, courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences, - authCode = "", + calendarPreferences = calendarPreferences ) viewModel.login("acc@test.org", "ed") @@ -278,12 +266,10 @@ class SignInViewModelTest { config = config, router = router, whatsNewGlobalManager = whatsNewGlobalManager, - browserAuthHelper = browserAuthHelper, courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences, - authCode = "", + calendarPreferences = calendarPreferences ) coEvery { interactor.login("acc@test.org", "edx") } returns Unit viewModel.login("acc@test.org", "edx") @@ -319,12 +305,10 @@ class SignInViewModelTest { config = config, router = router, whatsNewGlobalManager = whatsNewGlobalManager, - browserAuthHelper = browserAuthHelper, courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences, - authCode = "", + calendarPreferences = calendarPreferences ) coEvery { interactor.login("acc@test.org", "edx") } throws UnknownHostException() viewModel.login("acc@test.org", "edx") @@ -362,12 +346,10 @@ class SignInViewModelTest { config = config, router = router, whatsNewGlobalManager = whatsNewGlobalManager, - browserAuthHelper = browserAuthHelper, courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences, - authCode = "", + calendarPreferences = calendarPreferences ) coEvery { interactor.login("acc@test.org", "edx") } throws EdxError.InvalidGrantException() viewModel.login("acc@test.org", "edx") @@ -405,12 +387,10 @@ class SignInViewModelTest { config = config, router = router, whatsNewGlobalManager = whatsNewGlobalManager, - browserAuthHelper = browserAuthHelper, courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences, - authCode = "", + calendarPreferences = calendarPreferences ) coEvery { interactor.login("acc@test.org", "edx") } throws IllegalStateException() viewModel.login("acc@test.org", "edx") diff --git a/core/src/main/java/org/openedx/core/ApiConstants.kt b/core/src/main/java/org/openedx/core/ApiConstants.kt index 959d3c224..786d63cc4 100644 --- a/core/src/main/java/org/openedx/core/ApiConstants.kt +++ b/core/src/main/java/org/openedx/core/ApiConstants.kt @@ -2,7 +2,6 @@ package org.openedx.core object ApiConstants { const val URL_LOGIN = "/oauth2/login/" - const val URL_AUTHORIZE = "/oauth2/authorize/" const val URL_ACCESS_TOKEN = "/oauth2/access_token/" const val URL_EXCHANGE_TOKEN = "/oauth2/exchange_access_token/{auth_type}/" const val GET_USER_PROFILE = "/api/mobile/v0.5/my_user_info" @@ -10,18 +9,15 @@ object ApiConstants { const val URL_REGISTRATION_FIELDS = "/user_api/v1/account/registration" const val URL_VALIDATE_REGISTRATION_FIELDS = "/api/user/v1/validation/registration" const val URL_REGISTER = "/api/user/v1/account/registration/" - const val URL_REGISTER_BROWSER = "/register" const val URL_PASSWORD_RESET = "/password_reset/" const val GRANT_TYPE_PASSWORD = "password" - const val GRANT_TYPE_CODE = "authorization_code" const val TOKEN_TYPE_BEARER = "Bearer" const val TOKEN_TYPE_JWT = "jwt" const val TOKEN_TYPE_REFRESH = "refresh_token" const val ACCESS_TOKEN = "access_token" - const val CLIENT_ID = "client_id" const val EMAIL = "email" const val NAME = "name" @@ -31,7 +27,6 @@ object ApiConstants { const val AUTH_TYPE_GOOGLE = "google-oauth2" const val AUTH_TYPE_FB = "facebook" const val AUTH_TYPE_MICROSOFT = "azuread-oauth2" - const val AUTH_TYPE_BROWSER = "browser" const val COURSE_KEY = "course_key" @@ -39,10 +34,4 @@ object ApiConstants { const val HONOR_CODE = "honor_code" const val MARKETING_EMAILS = "marketing_emails_opt_in" } - - object BrowserLogin { - const val REDIRECT_HOST = "oauth2Callback" - const val CODE_QUERY_PARAM = "code" - const val RESPONSE_TYPE = "code" - } } 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 d26741699..bfdc38402 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -116,14 +116,6 @@ class Config(context: Context) { return getBoolean(REGISTRATION_ENABLED, true) } - fun isBrowserLoginEnabled(): Boolean { - return getBoolean(BROWSER_LOGIN, false) - } - - fun isBrowserRegistrationEnabled(): Boolean { - return getBoolean(BROWSER_REGISTRATION, false) - } - private fun getExperimentalFeaturesConfig(): ExperimentalFeaturesConfig { return getObjectOrNewInstance(EXPERIMENTAL_FEATURES, ExperimentalFeaturesConfig::class.java) } @@ -182,8 +174,6 @@ class Config(context: Context) { 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 DISCOVERY = "DISCOVERY" private const val PROGRAM = "PROGRAM" private const val DASHBOARD = "DASHBOARD" diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml index a7f265a45..a9a0abe1f 100644 --- a/default_config/dev/config.yaml +++ b/default_config/dev/config.yaml @@ -81,10 +81,6 @@ WHATS_NEW_ENABLED: false SOCIAL_AUTH_ENABLED: false #feature flag to enable registration from app REGISTRATION_ENABLED: true -#feature flag to do the authentication flow in the browser to log in -BROWSER_LOGIN: false -#feature flag to do the registration for in the browser -BROWSER_REGISTRATION: false #Course navigation feature flags UI_COMPONENTS: COURSE_DROPDOWN_NAVIGATION_ENABLED: false diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index a7f265a45..a9a0abe1f 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -81,10 +81,6 @@ WHATS_NEW_ENABLED: false SOCIAL_AUTH_ENABLED: false #feature flag to enable registration from app REGISTRATION_ENABLED: true -#feature flag to do the authentication flow in the browser to log in -BROWSER_LOGIN: false -#feature flag to do the registration for in the browser -BROWSER_REGISTRATION: false #Course navigation feature flags UI_COMPONENTS: COURSE_DROPDOWN_NAVIGATION_ENABLED: false diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml index a7f265a45..a9a0abe1f 100644 --- a/default_config/stage/config.yaml +++ b/default_config/stage/config.yaml @@ -81,10 +81,6 @@ WHATS_NEW_ENABLED: false SOCIAL_AUTH_ENABLED: false #feature flag to enable registration from app REGISTRATION_ENABLED: true -#feature flag to do the authentication flow in the browser to log in -BROWSER_LOGIN: false -#feature flag to do the registration for in the browser -BROWSER_REGISTRATION: false #Course navigation feature flags UI_COMPONENTS: COURSE_DROPDOWN_NAVIGATION_ENABLED: false diff --git a/docs/decisions/0001-strategy-for-data-streams.rst b/docs/0001-strategy-for-data-streams.rst similarity index 100% rename from docs/decisions/0001-strategy-for-data-streams.rst rename to docs/0001-strategy-for-data-streams.rst diff --git a/docs/how-tos/auth-using-browser.rst b/docs/how-tos/auth-using-browser.rst deleted file mode 100644 index 49a23603b..000000000 --- a/docs/how-tos/auth-using-browser.rst +++ /dev/null @@ -1,48 +0,0 @@ -How to use Browser-based Login and Registration -=============================================== - -Introduction ------------- - -If your Open edX instance is set up with a custom authentication system that requires logging in -via the browser, you can use the ``BROWSER_LOGIN`` and ``BROWSER_REGISTRATION`` flags to redirect -login and registration to the browser. - -The ``BROWSER_LOGIN`` flag is used to redirect login to the browser. In this case clicking on the -login button will open the authorization flow in an Android custom browser tab and redirect back to -the application. - -The ``BROWSER_REGISTRATION`` flag is used to redirect registration to the browser. In this case -clicking on the registration button will open the registration page in a regular browser tab. Once -registered, the user will as of writing this document **not** be automatically redirected to the -application. - -Usage ------ - -In order to use the ``BROWSER_LOGIN`` feature, you need to set up an OAuth2 provider via -``/admin/oauth2_provider/application/`` that has a redirect URL with the following format - - ``://oauth2Callback`` - -Here application ID is the ID for the Android application and defaults to ``"org.openedx.app"``. This -URI scheme is handled by the application and will be used by the app to get the OAuth2 token for -using the APIs. - -Note that normally the Django OAuth Toolkit doesn't allow custom schemes like the above as redirect -URIs, so you will need to explicitly allow the by adding this URI scheme to -``ALLOWED_REDIRECT_URI_SCHEMES`` in the Django OAuth Toolkit settings in ``OAUTH2_PROVIDER``. You -can add the following line to your django settings python file: - -.. code-block:: python - - OAUTH2_PROVIDER["ALLOWED_REDIRECT_URI_SCHEMES"] = ["https", "org.openedx.app"] - -Replace ``"org.openedx.app"`` with the correct id for your application. You must list all allowed -schemes here, including ``"https"`` and ``"http"``. - -The authentication will then redirect to the browser in a custom tab that redirects back to the app. - -..note:: - - If a user logs out from the application, they might still be logged in, in the browser. \ No newline at end of file diff --git a/docs/how-tos/index.rst b/docs/how-tos/index.rst deleted file mode 100644 index 202bad08b..000000000 --- a/docs/how-tos/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -"How-To" Guides -############### - - -.. toctree:: -:glob: - -* From 135a30504fbe3936fff991eba6aa6256ce8e67f8 Mon Sep 17 00:00:00 2001 From: RawanMatar89 <41669180+RawanMatar89@users.noreply.github.com> Date: Sun, 9 Feb 2025 12:17:26 +0200 Subject: [PATCH 2/6] feat: add saml sso --- .../main/java/org/openedx/app/AppRouter.kt | 8 + .../java/org/openedx/app/di/ScreenModule.kt | 1 + .../auth/data/repository/AuthRepository.kt | 12 + .../auth/domain/interactor/AuthInteractor.kt | 6 + .../openedx/auth/presentation/AuthRouter.kt | 2 + .../presentation/signin/SignInFragment.kt | 6 + .../auth/presentation/signin/SignInUIState.kt | 4 + .../presentation/signin/SignInViewModel.kt | 50 +++ .../presentation/signin/compose/SignInView.kt | 421 +++++++++++------- build.gradle | 9 + .../java/org/openedx/core/config/Config.kt | 27 ++ .../global/webview/SSOWebContentFragment.kt | 71 +++ .../java/org/openedx/core/ui/ComposeCommon.kt | 9 + .../openedx/core/ui/SSOWebContentScreen.kt | 187 ++++++++ core/src/main/res/values/strings.xml | 4 + default_config/dev/config.yaml | 9 + default_config/prod/config.yaml | 10 + default_config/stage/config.yaml | 10 + gradle/wrapper/gradle-wrapper.properties | 2 +- 19 files changed, 683 insertions(+), 165 deletions(-) create mode 100644 core/src/main/java/org/openedx/core/presentation/global/webview/SSOWebContentFragment.kt create mode 100644 core/src/main/java/org/openedx/core/ui/SSOWebContentScreen.kt diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt index cfe1ecc44..fcec4400b 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 @@ -432,6 +433,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 8c27020d5..4dc1c94a8 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -121,6 +121,7 @@ val screenModule = module { get(), get(), get(), + get(), courseId, infoType, ) 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 617006afe..c11284276 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 cdce0dbdf..9b12359bb 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 8f55d334b..ff3aae707 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 @@ -43,6 +44,9 @@ class SignInFragment : Fragment() { val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState(null) if (appUpgradeEvent == null) { + setFragmentResultListener("requestKey") { requestKey, bundle -> + viewModel.ssoLogin(token = requestKey) + } LoginScreen( windowSize = windowSize, state = state, @@ -50,6 +54,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 @@ -105,6 +110,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 RegisterClick : 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 7d472882f..f7b56084c 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 5cc08b47e..5868fa1a9 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 androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.LiveData @@ -48,6 +49,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, @@ -62,6 +64,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(), @@ -135,6 +141,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 d4608e4f8..d5dd976a0 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 @@ -51,6 +51,7 @@ 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.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.tooling.preview.Devices import androidx.compose.ui.tooling.preview.Preview @@ -65,8 +66,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 @@ -93,60 +96,64 @@ internal fun LoginScreen( Scaffold( scaffoldState = scaffoldState, - modifier = Modifier - .semantics { - testTagsAsResourceId = true - } - .fillMaxSize() - .navigationBarsPadding(), - backgroundColor = MaterialTheme.appColors.background + modifier = + Modifier + .semantics { + testTagsAsResourceId = true + }.fillMaxSize() + .navigationBarsPadding(), + backgroundColor = MaterialTheme.appColors.background, ) { val contentPaddings by remember { mutableStateOf( windowSize.windowSizeValue( - expanded = Modifier - .widthIn(Dp.Unspecified, 420.dp) - .padding( - top = 32.dp, - bottom = 40.dp - ), - compact = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp, vertical = 28.dp) - ) + expanded = + Modifier + .widthIn(Dp.Unspecified, 420.dp) + .padding( + top = 32.dp, + bottom = 40.dp, + ), + compact = + Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp, vertical = 28.dp), + ), ) } val buttonWidth by remember(key1 = windowSize) { mutableStateOf( windowSize.windowSizeValue( expanded = Modifier.widthIn(232.dp, Dp.Unspecified), - compact = Modifier.fillMaxWidth() - ) + compact = Modifier.fillMaxWidth(), + ), ) } Image( - modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(fraction = 0.3f), + modifier = + Modifier + .fillMaxWidth() + .fillMaxHeight(fraction = 0.3f), painter = painterResource(id = coreR.drawable.core_top_header), contentScale = ContentScale.FillBounds, - contentDescription = null + contentDescription = null, ) HandleUIMessage( uiMessage = uiMessage, - scaffoldState = scaffoldState + scaffoldState = scaffoldState, ) if (state.isLogistrationEnabled) { Box( - modifier = Modifier - .statusBarsPadding() - .fillMaxWidth(), - contentAlignment = Alignment.CenterStart + modifier = + Modifier + .statusBarsPadding() + .fillMaxWidth(), + contentAlignment = Alignment.CenterStart, ) { BackBtn( modifier = Modifier.padding(end = 16.dp), - tint = Color.White + tint = Color.White, ) { onEvent(AuthEvent.BackClick) } @@ -154,37 +161,43 @@ internal fun LoginScreen( } Column( Modifier.padding(it), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, ) { SignInLogoView() Surface( color = MaterialTheme.appColors.background, shape = MaterialTheme.appShapes.screenBackgroundShape, - modifier = Modifier - .fillMaxSize() + modifier = + Modifier + .fillMaxSize(), ) { Box(contentAlignment = Alignment.TopCenter) { Column( - modifier = Modifier - .background(MaterialTheme.appColors.background) - .verticalScroll(scrollState) - .displayCutoutForLandscape() - .then(contentPaddings), + modifier = + Modifier + .background(MaterialTheme.appColors.background) + .verticalScroll(scrollState) + .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, @@ -224,100 +237,170 @@ private fun AuthForm( val keyboardController = LocalSoftwareKeyboardController.current var isEmailError by rememberSaveable { mutableStateOf(false) } var isPasswordError by rememberSaveable { mutableStateOf(false) } + if (state.isLoginRegistrationFormEnabled) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + 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) + ) - Column(horizontalAlignment = Alignment.CenterHorizontally) { - 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, + ) - 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() + Row( + Modifier + .fillMaxWidth() + .padding(top = 20.dp, bottom = 36.dp) + ) { + 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 + ) } - }, - isError = isPasswordError, - ) - - Row( - Modifier - .fillMaxWidth() - .padding(top = 20.dp, bottom = 36.dp) - ) { - if (state.isLogistrationEnabled.not() && state.isRegistrationEnabled) { + Spacer(modifier = Modifier.weight(1f)) Text( modifier = Modifier - .testTag("txt_register") + .testTag("txt_forgot_password") .noRippleClickable { - onEvent(AuthEvent.RegisterClick) + onEvent(AuthEvent.ForgotPasswordClick) }, - text = stringResource(id = coreR.string.core_register), - color = MaterialTheme.appColors.primary, + text = stringResource(id = R.string.auth_forgot_password), + color = MaterialTheme.appColors.infoVariant, style = MaterialTheme.appTypography.labelLarge ) } - Spacer(modifier = Modifier.weight(1f)) + + 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 = { + 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) { 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 + 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)) + HorizontalLine() + 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 { - OpenEdXButton( - modifier = buttonWidth.testTag("btn_sign_in"), - text = stringResource(id = coreR.string.core_sign_in), - textColor = MaterialTheme.appColors.primaryButtonText, - backgroundColor = MaterialTheme.appColors.secondaryButtonBackground, - onClick = { - 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.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 = "")) }, + ) } } } @@ -337,12 +420,13 @@ private fun PasswordTextField( val focusManager = LocalFocusManager.current Text( - modifier = Modifier - .testTag("txt_password_label") - .fillMaxWidth(), + modifier = + Modifier + .testTag("txt_password_label") + .fillMaxWidth(), text = stringResource(id = coreR.string.core_password), color = MaterialTheme.appColors.textPrimary, - style = MaterialTheme.appTypography.labelLarge + style = MaterialTheme.appTypography.labelLarge, ) Spacer(modifier = Modifier.height(8.dp)) @@ -354,50 +438,55 @@ private fun PasswordTextField( passwordTextFieldValue = it onValueChanged(it.text.trim()) }, - colors = TextFieldDefaults.outlinedTextFieldColors( - textColor = MaterialTheme.appColors.textFieldText, - backgroundColor = MaterialTheme.appColors.textFieldBackground, - unfocusedBorderColor = MaterialTheme.appColors.textFieldBorder, - cursorColor = MaterialTheme.appColors.textFieldText, - ), + colors = + TextFieldDefaults.outlinedTextFieldColors( + textColor = MaterialTheme.appColors.textFieldText, + backgroundColor = MaterialTheme.appColors.textFieldBackground, + unfocusedBorderColor = MaterialTheme.appColors.textFieldBorder, + cursorColor = MaterialTheme.appColors.textFieldText, + ), shape = MaterialTheme.appShapes.textFieldShape, placeholder = { Text( modifier = Modifier.testTag("txt_password_placeholder"), text = stringResource(id = R.string.auth_enter_password), color = MaterialTheme.appColors.textFieldHint, - style = MaterialTheme.appTypography.bodyMedium + style = MaterialTheme.appTypography.bodyMedium, ) }, trailingIcon = { PasswordVisibilityIcon( isPasswordVisible = isPasswordVisible, - onClick = { isPasswordVisible = !isPasswordVisible } + onClick = { isPasswordVisible = !isPasswordVisible }, ) }, - keyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Password, - imeAction = ImeAction.Done - ), - visualTransformation = if (isPasswordVisible) { - VisualTransformation.None - } else { - PasswordVisualTransformation() - }, - keyboardActions = KeyboardActions { - focusManager.clearFocus() - onPressDone() - }, + keyboardOptions = + KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Done, + ), + visualTransformation = + if (isPasswordVisible) { + VisualTransformation.None + } else { + PasswordVisualTransformation() + }, + keyboardActions = + KeyboardActions { + focusManager.clearFocus() + onPressDone() + }, isError = isError, textStyle = MaterialTheme.appTypography.bodyMedium, singleLine = true, ) if (isError) { Text( - modifier = Modifier - .testTag("txt_password_error") - .fillMaxWidth() - .padding(top = 4.dp), + modifier = + Modifier + .testTag("txt_password_error") + .fillMaxWidth() + .padding(top = 4.dp), text = stringResource(id = R.string.auth_error_empty_password), style = MaterialTheme.appTypography.bodySmall, color = MaterialTheme.appColors.error, @@ -428,12 +517,16 @@ private fun SignInScreenTabletPreview() { OpenEdXTheme { LoginScreen( windowSize = WindowSize(WindowType.Expanded, WindowType.Expanded), - state = SignInUIState().copy( - isSocialAuthEnabled = true, - isFacebookAuthEnabled = true, - isGoogleAuthEnabled = true, - isMicrosoftAuthEnabled = true, - ), + state = + SignInUIState().copy( + isLoginRegistrationFormEnabled = false, + isSSOLoginEnabled = true, + isSSODefaultLoginButton = true, + isSocialAuthEnabled = true, + isFacebookAuthEnabled = true, + isGoogleAuthEnabled = true, + isMicrosoftAuthEnabled = true, + ), uiMessage = null, onEvent = {}, ) diff --git a/build.gradle b/build.gradle index f7fb3cf91..1be6127d5 100644 --- a/build.gradle +++ b/build.gradle @@ -34,12 +34,21 @@ ext { firebase_version = "33.0.0" + retrofit_version = '2.9.0' + logginginterceptor_version = '4.9.1' + + koin_version = '3.2.0' + + coil_version = '2.3.0' + jsoup_version = '1.13.1' in_app_review = '2.0.1' 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 bfdc38402..3359c3129 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -28,6 +28,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,6 +112,23 @@ class Config(context: Context) { return getBoolean(PRE_LOGIN_EXPERIENCE_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 + } + fun getCourseUIConfig(): UIConfig { return getObjectOrNewInstance(UI_COMPONENTS, UIConfig::class.java) } @@ -159,6 +180,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" 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 3cf6eb1fc..bb7457f97 100644 --- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt +++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt @@ -981,6 +981,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 99df5b3d4..ff1d443a4 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -229,4 +229,8 @@ Authorization Please enter the system to continue with course enrollment. + Sign in with SSO + Start today to build your career with confidence + 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 a9a0abe1f..a71b85dfe 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: 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: diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index a9a0abe1f..088e6f6e9 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 a9a0abe1f..088e6f6e9 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 From fa5f13a7c549a9adbb62c0ad359545bd4b4643b0 Mon Sep 17 00:00:00 2001 From: RawanMatar89 <41669180+RawanMatar89@users.noreply.github.com> Date: Mon, 29 Sep 2025 09:32:45 +0300 Subject: [PATCH 3/6] fix: resolve conflicts and unit test --- .../org.openedx.app.room.AppDatabase/4.json | 268 ++++++------------ .../main/java/org/openedx/app/AppActivity.kt | 5 - .../java/org/openedx/app/di/ScreenModule.kt | 3 + .../presentation/signin/SignInViewModel.kt | 3 +- .../presentation/sso/BrowserAuthHelper.kt | 35 --- .../signin/SignInViewModelTest.kt | 35 ++- 6 files changed, 125 insertions(+), 224 deletions(-) delete mode 100644 auth/src/main/java/org/openedx/auth/presentation/sso/BrowserAuthHelper.kt diff --git a/app/schemas/org.openedx.app.room.AppDatabase/4.json b/app/schemas/org.openedx.app.room.AppDatabase/4.json index 0f1e1c17b..276f86d72 100644 --- a/app/schemas/org.openedx.app.room.AppDatabase/4.json +++ b/app/schemas/org.openedx.app.room.AppDatabase/4.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 4, - "identityHash": "488bd2b78e977fef626afb28014c80f2", + "identityHash": "ad2d11c09b3d243a97daf995a50b761f", "entities": [ { "tableName": "course_discovery_table", @@ -131,26 +131,22 @@ { "fieldPath": "media.bannerImage", "columnName": "bannerImage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "media.courseImage", "columnName": "courseImage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "media.courseVideo", "columnName": "courseVideo", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "media.image", "columnName": "image", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" } ], "primaryKey": { @@ -158,9 +154,7 @@ "columnNames": [ "id" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "course_enrolled_table", @@ -301,62 +295,52 @@ { "fieldPath": "course.coursewareAccess.hasAccess", "columnName": "hasAccess", - "affinity": "INTEGER", - "notNull": false + "affinity": "INTEGER" }, { "fieldPath": "course.coursewareAccess.errorCode", "columnName": "errorCode", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "course.coursewareAccess.developerMessage", "columnName": "developerMessage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "course.coursewareAccess.userMessage", "columnName": "userMessage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "course.coursewareAccess.additionalContextUserMessage", "columnName": "additionalContextUserMessage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "course.coursewareAccess.userFragment", "columnName": "userFragment", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "course.media.bannerImage", "columnName": "bannerImage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "course.media.courseImage", "columnName": "courseImage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "course.media.courseVideo", "columnName": "courseVideo", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "course.media.image", "columnName": "image", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "course.courseSharingUtmParameters.facebook", @@ -373,8 +357,7 @@ { "fieldPath": "certificate.certificateURL", "columnName": "certificateURL", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "progress.assignmentsCompleted", @@ -391,38 +374,32 @@ { "fieldPath": "courseStatus.lastVisitedModuleId", "columnName": "lastVisitedModuleId", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseStatus.lastVisitedModulePath", "columnName": "lastVisitedModulePath", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseStatus.lastVisitedBlockId", "columnName": "lastVisitedBlockId", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseStatus.lastVisitedUnitDisplayName", "columnName": "lastVisitedUnitDisplayName", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseAssignments.futureAssignments", "columnName": "futureAssignments", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseAssignments.pastAssignments", "columnName": "pastAssignments", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" } ], "primaryKey": { @@ -430,9 +407,7 @@ "columnNames": [ "courseId" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "course_structure_table", @@ -477,8 +452,7 @@ { "fieldPath": "start", "columnName": "start", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "startDisplay", @@ -495,8 +469,7 @@ { "fieldPath": "end", "columnName": "end", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "isSelfPaced", @@ -507,68 +480,57 @@ { "fieldPath": "coursewareAccess.hasAccess", "columnName": "hasAccess", - "affinity": "INTEGER", - "notNull": false + "affinity": "INTEGER" }, { "fieldPath": "coursewareAccess.errorCode", "columnName": "errorCode", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "coursewareAccess.developerMessage", "columnName": "developerMessage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "coursewareAccess.userMessage", "columnName": "userMessage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "coursewareAccess.additionalContextUserMessage", "columnName": "additionalContextUserMessage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "coursewareAccess.userFragment", "columnName": "userFragment", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "media.bannerImage", "columnName": "bannerImage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "media.courseImage", "columnName": "courseImage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "media.courseVideo", "columnName": "courseVideo", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "media.image", "columnName": "image", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "certificate.certificateURL", "columnName": "certificateURL", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "progress.assignmentsCompleted", @@ -588,9 +550,7 @@ "columnNames": [ "id" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "download_model", @@ -647,8 +607,7 @@ { "fieldPath": "lastModified", "columnName": "lastModified", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" } ], "primaryKey": { @@ -656,9 +615,7 @@ "columnNames": [ "id" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "offline_x_block_progress_table", @@ -700,9 +657,7 @@ "columnNames": [ "id" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "course_calendar_event_table", @@ -726,9 +681,7 @@ "columnNames": [ "event_id" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "course_calendar_state_table", @@ -758,9 +711,7 @@ "columnNames": [ "course_id" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "download_course_preview_table", @@ -775,20 +726,17 @@ { "fieldPath": "name", "columnName": "course_name", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "image", "columnName": "course_image", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "totalSize", "columnName": "total_size", - "affinity": "INTEGER", - "notNull": false + "affinity": "INTEGER" } ], "primaryKey": { @@ -796,13 +744,11 @@ "columnNames": [ "course_id" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "course_enrollment_details_table", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `courseUpdates` TEXT NOT NULL, `courseHandouts` TEXT NOT NULL, `discussionUrl` TEXT NOT NULL, `hasUnmetPrerequisites` INTEGER NOT NULL, `isTooEarly` INTEGER NOT NULL, `isStaff` INTEGER NOT NULL, `auditAccessExpires` TEXT, `hasAccess` INTEGER, `errorCode` TEXT, `developerMessage` TEXT, `userMessage` TEXT, `additionalContextUserMessage` TEXT, `userFragment` TEXT, `certificateURL` TEXT, `created` TEXT, `mode` TEXT, `isActive` INTEGER NOT NULL, `upgradeDeadline` TEXT, `name` TEXT NOT NULL, `number` TEXT NOT NULL, `org` TEXT NOT NULL, `startDisplay` TEXT NOT NULL, `startType` TEXT NOT NULL, `isSelfPaced` INTEGER NOT NULL, `courseAbout` TEXT NOT NULL, `bannerImage` TEXT, `courseImage` TEXT, `courseVideo` TEXT, `image` TEXT, `facebook` TEXT NOT NULL, `twitter` TEXT NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `courseUpdates` TEXT NOT NULL, `courseHandouts` TEXT NOT NULL, `discussionUrl` TEXT NOT NULL, `hasUnmetPrerequisites` INTEGER NOT NULL, `isTooEarly` INTEGER NOT NULL, `isStaff` INTEGER NOT NULL, `auditAccessExpires` TEXT, `hasAccess` INTEGER, `errorCode` TEXT, `developerMessage` TEXT, `userMessage` TEXT, `additionalContextUserMessage` TEXT, `userFragment` TEXT, `certificateURL` TEXT, `created` TEXT, `mode` TEXT, `isActive` INTEGER NOT NULL, `upgradeDeadline` TEXT, `name` TEXT NOT NULL, `number` TEXT NOT NULL, `org` TEXT NOT NULL, `startDisplay` TEXT NOT NULL, `startType` TEXT NOT NULL, `isSelfPaced` INTEGER NOT NULL, `courseAbout` TEXT NOT NULL, `fastTime` INTEGER, `cdate` TEXT, `fastTime` INTEGER, `cdate` TEXT, `bannerImage` TEXT, `courseImage` TEXT, `courseVideo` TEXT, `image` TEXT, `facebook` TEXT NOT NULL, `twitter` TEXT NOT NULL, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -849,62 +795,52 @@ { "fieldPath": "courseAccessDetails.auditAccessExpires", "columnName": "auditAccessExpires", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseAccessDetails.coursewareAccess.hasAccess", "columnName": "hasAccess", - "affinity": "INTEGER", - "notNull": false + "affinity": "INTEGER" }, { "fieldPath": "courseAccessDetails.coursewareAccess.errorCode", "columnName": "errorCode", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseAccessDetails.coursewareAccess.developerMessage", "columnName": "developerMessage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseAccessDetails.coursewareAccess.userMessage", "columnName": "userMessage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseAccessDetails.coursewareAccess.additionalContextUserMessage", "columnName": "additionalContextUserMessage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseAccessDetails.coursewareAccess.userFragment", "columnName": "userFragment", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "certificate.certificateURL", "columnName": "certificateURL", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "enrollmentDetails.created", "columnName": "created", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "enrollmentDetails.mode", "columnName": "mode", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "enrollmentDetails.isActive", @@ -915,8 +851,7 @@ { "fieldPath": "enrollmentDetails.upgradeDeadline", "columnName": "upgradeDeadline", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseInfoOverview.name", @@ -960,29 +895,35 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "courseInfoOverview.end.fastTime", + "columnName": "fastTime", + "affinity": "INTEGER" + }, + { + "fieldPath": "courseInfoOverview.end.cdate", + "columnName": "cdate", + "affinity": "TEXT" + }, { "fieldPath": "courseInfoOverview.media.bannerImage", "columnName": "bannerImage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseInfoOverview.media.courseImage", "columnName": "courseImage", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseInfoOverview.media.courseVideo", "columnName": "courseVideo", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseInfoOverview.media.image", "columnName": "image", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseInfoOverview.courseSharingUtmParameters.facebook", @@ -1002,9 +943,7 @@ "columnNames": [ "id" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "video_progress_table", @@ -1040,9 +979,7 @@ "columnNames": [ "block_id" ] - }, - "indices": [], - "foreignKeys": [] + } }, { "tableName": "course_progress_table", @@ -1123,98 +1060,82 @@ { "fieldPath": "certificateData.certStatus", "columnName": "certificate_certStatus", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "certificateData.certWebViewUrl", "columnName": "certificate_certWebViewUrl", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "certificateData.downloadUrl", "columnName": "certificate_downloadUrl", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "certificateData.certificateAvailableDate", "columnName": "certificate_certificateAvailableDate", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "completionSummary.completeCount", "columnName": "completion_completeCount", - "affinity": "INTEGER", - "notNull": false + "affinity": "INTEGER" }, { "fieldPath": "completionSummary.incompleteCount", "columnName": "completion_incompleteCount", - "affinity": "INTEGER", - "notNull": false + "affinity": "INTEGER" }, { "fieldPath": "completionSummary.lockedCount", "columnName": "completion_lockedCount", - "affinity": "INTEGER", - "notNull": false + "affinity": "INTEGER" }, { "fieldPath": "courseGrade.letterGrade", "columnName": "grade_letterGrade", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "courseGrade.percent", "columnName": "grade_percent", - "affinity": "REAL", - "notNull": false + "affinity": "REAL" }, { "fieldPath": "courseGrade.isPassing", "columnName": "grade_isPassing", - "affinity": "INTEGER", - "notNull": false + "affinity": "INTEGER" }, { "fieldPath": "gradingPolicy.assignmentPolicies", "columnName": "grading_assignmentPolicies", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "gradingPolicy.gradeRange", "columnName": "grading_gradeRange", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "gradingPolicy.assignmentColors", "columnName": "grading_assignmentColors", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "verificationData.link", "columnName": "verification_link", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "verificationData.status", "columnName": "verification_status", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" }, { "fieldPath": "verificationData.statusDate", "columnName": "verification_statusDate", - "affinity": "TEXT", - "notNull": false + "affinity": "TEXT" } ], "primaryKey": { @@ -1222,15 +1143,12 @@ "columnNames": [ "courseId" ] - }, - "indices": [], - "foreignKeys": [] + } } ], - "views": [], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '488bd2b78e977fef626afb28014c80f2')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ad2d11c09b3d243a97daf995a50b761f')" ] } } \ No newline at end of file diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index ed4840bf0..69429694a 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -2,7 +2,6 @@ package org.openedx.app import android.content.Intent import android.content.res.Configuration -import android.net.Uri import android.os.Bundle import android.view.View import android.view.WindowManager @@ -204,10 +203,6 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { super.onNewIntent(intent) this.intent = intent - if (authCode != null) { - addFragment(SignInFragment.newInstance(null, null, authCode = authCode)) - } - val extras = intent.extras if (extras?.containsKey(DeepLink.Keys.NOTIFICATION_TYPE.value) == true) { handlePushNotification(extras) 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 1a429af22..8e929c284 100644 --- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt +++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt @@ -77,6 +77,7 @@ import org.openedx.profile.presentation.profile.ProfileViewModel import org.openedx.profile.presentation.settings.SettingsViewModel import org.openedx.profile.presentation.video.VideoSettingsViewModel import org.openedx.whatsnew.presentation.whatsnew.WhatsNewViewModel +import android.content.res.Resources val screenModule = module { @@ -109,6 +110,7 @@ val screenModule = module { ) } + val lang = Resources.getSystem().configuration.locales[0].language viewModel { (courseId: String?, infoType: String?) -> SignInViewModel( get(), @@ -127,6 +129,7 @@ val screenModule = module { get(), courseId, infoType, + lang ) } 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 5868fa1a9..3a72de661 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 @@ -58,6 +58,7 @@ class SignInViewModel( config: Config, val courseId: String?, val infoType: String?, + currentLang: String, ) : BaseViewModel() { private val logger = Logger("SignInViewModel") @@ -66,7 +67,7 @@ class SignInViewModel( SignInUIState( isLoginRegistrationFormEnabled = config.isLoginRegistrationEnabled(), isSSOLoginEnabled = config.isSSOLoginEnabled(), - ssoButtonTitle = config.getSSOButtonTitle(key = Resources.getSystem().getConfiguration().locales[0].language.uppercase(), ""), + ssoButtonTitle = currentLang, isSSODefaultLoginButton = config.isSSODefaultLoginButton(), isFacebookAuthEnabled = config.getFacebookConfig().isEnabled(), isGoogleAuthEnabled = config.getGoogleConfig().isEnabled(), diff --git a/auth/src/main/java/org/openedx/auth/presentation/sso/BrowserAuthHelper.kt b/auth/src/main/java/org/openedx/auth/presentation/sso/BrowserAuthHelper.kt deleted file mode 100644 index cd3233b39..000000000 --- a/auth/src/main/java/org/openedx/auth/presentation/sso/BrowserAuthHelper.kt +++ /dev/null @@ -1,35 +0,0 @@ -package org.openedx.auth.presentation.sso - -import android.app.Activity -import android.content.Intent.FLAG_ACTIVITY_NEW_TASK -import android.net.Uri -import androidx.annotation.WorkerThread -import androidx.browser.customtabs.CustomTabsIntent -import org.openedx.core.ApiConstants -import org.openedx.core.config.Config -import org.openedx.core.utils.Logger - -class BrowserAuthHelper(private val config: Config) { - - private val logger = Logger(TAG) - - @WorkerThread - suspend fun signIn(activityContext: Activity) { - logger.d { "Browser-based auth initiated" } - val uri = Uri.parse("${config.getApiHostURL()}${ApiConstants.URL_AUTHORIZE}").buildUpon() - .appendQueryParameter("client_id", config.getOAuthClientId()) - .appendQueryParameter( - "redirect_uri", - "${activityContext.packageName}://${ApiConstants.BrowserLogin.REDIRECT_HOST}" - ) - .appendQueryParameter("response_type", ApiConstants.BrowserLogin.RESPONSE_TYPE).build() - val intent = - CustomTabsIntent.Builder().setUrlBarHidingEnabled(true).setShowTitle(true).build() - intent.intent.flags = FLAG_ACTIVITY_NEW_TASK - intent.launchUrl(activityContext, uri) - } - - private companion object { - const val TAG = "BrowserAuthHelper" - } -} diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt index 48480e310..551180c80 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt @@ -95,6 +95,9 @@ class SignInViewModelTest { coEvery { calendarInteractor.clearCalendarCachedData() } returns Unit every { analytics.logScreenEvent(any(), any()) } returns Unit every { config.isRegistrationEnabled() } returns true + every { config.isLoginRegistrationEnabled() } returns true + every { config.isSSOLoginEnabled() } returns false + every { config.isSSODefaultLoginButton() } returns false } @After @@ -123,7 +126,9 @@ class SignInViewModelTest { courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences + calendarPreferences = calendarPreferences, + configuration = config, + currentLang = "EN" ) viewModel.login("", "") coVerify(exactly = 0) { interactor.login(any(), any()) } @@ -159,7 +164,9 @@ class SignInViewModelTest { courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences + calendarPreferences = calendarPreferences, + configuration = config, + currentLang = "EN" ) viewModel.login("acc@test.o", "") coVerify(exactly = 0) { interactor.login(any(), any()) } @@ -195,7 +202,9 @@ class SignInViewModelTest { courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences + calendarPreferences = calendarPreferences, + configuration = config, + currentLang = "EN" ) viewModel.login("acc@test.org", "") @@ -230,7 +239,9 @@ class SignInViewModelTest { courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences + calendarPreferences = calendarPreferences, + configuration = config, + currentLang = "EN" ) viewModel.login("acc@test.org", "ed") @@ -269,7 +280,9 @@ class SignInViewModelTest { courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences + calendarPreferences = calendarPreferences, + configuration = config, + currentLang = "EN" ) coEvery { interactor.login("acc@test.org", "edx") } returns Unit viewModel.login("acc@test.org", "edx") @@ -308,7 +321,9 @@ class SignInViewModelTest { courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences + calendarPreferences = calendarPreferences, + configuration = config, + currentLang = "EN" ) coEvery { interactor.login("acc@test.org", "edx") } throws UnknownHostException() viewModel.login("acc@test.org", "edx") @@ -349,7 +364,9 @@ class SignInViewModelTest { courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences + calendarPreferences = calendarPreferences, + configuration = config, + currentLang = "EN" ) coEvery { interactor.login("acc@test.org", "edx") } throws EdxError.InvalidGrantException() viewModel.login("acc@test.org", "edx") @@ -390,7 +407,9 @@ class SignInViewModelTest { courseId = "", infoType = "", calendarInteractor = calendarInteractor, - calendarPreferences = calendarPreferences + calendarPreferences = calendarPreferences, + configuration = config, + currentLang = "EN" ) coEvery { interactor.login("acc@test.org", "edx") } throws IllegalStateException() viewModel.login("acc@test.org", "edx") From 2207571edf8bbb5838fe5ff2fb2e2de362b5fd15 Mon Sep 17 00:00:00 2001 From: RawanMatar89 <41669180+RawanMatar89@users.noreply.github.com> Date: Thu, 2 Oct 2025 16:25:52 +0300 Subject: [PATCH 4/6] fix: unit test issues --- app/schemas/org.openedx.app.room.AppDatabase/4.json | 10 ++++++++++ gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/schemas/org.openedx.app.room.AppDatabase/4.json b/app/schemas/org.openedx.app.room.AppDatabase/4.json index 276f86d72..7378b799a 100644 --- a/app/schemas/org.openedx.app.room.AppDatabase/4.json +++ b/app/schemas/org.openedx.app.room.AppDatabase/4.json @@ -895,6 +895,16 @@ "affinity": "TEXT", "notNull": true }, + { + "fieldPath": "courseInfoOverview.start.fastTime", + "columnName": "fastTime", + "affinity": "INTEGER" + }, + { + "fieldPath": "courseInfoOverview.start.cdate", + "columnName": "cdate", + "affinity": "TEXT" + }, { "fieldPath": "courseInfoOverview.end.fastTime", "columnName": "fastTime", diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0f37100ea..cccb73fbc 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Mon Aug 11 14:17:42 EEST 2025 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 316ebc4738204d522be0120443e7c24b542bdb6c Mon Sep 17 00:00:00 2001 From: RawanMatar89 <41669180+RawanMatar89@users.noreply.github.com> Date: Sun, 7 Dec 2025 11:51:28 +0200 Subject: [PATCH 5/6] fix: sso login flow and other fixes --- .../org.openedx.app.room.AppDatabase/4.json | 26 ++-------- .../auth/data/repository/AuthRepository.kt | 4 +- .../presentation/signin/SignInFragment.kt | 12 +++-- .../presentation/signin/SignInViewModel.kt | 15 ++---- .../presentation/signin/compose/SignInView.kt | 4 +- .../java/org/openedx/core/config/Config.kt | 7 ++- .../global/webview/SSOWebContentFragment.kt | 3 +- .../openedx/core/ui/SSOWebContentScreen.kt | 48 ++++++++++++------- 8 files changed, 58 insertions(+), 61 deletions(-) diff --git a/app/schemas/org.openedx.app.room.AppDatabase/4.json b/app/schemas/org.openedx.app.room.AppDatabase/4.json index 7378b799a..2be5bb5af 100644 --- a/app/schemas/org.openedx.app.room.AppDatabase/4.json +++ b/app/schemas/org.openedx.app.room.AppDatabase/4.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 4, - "identityHash": "ad2d11c09b3d243a97daf995a50b761f", + "identityHash": "488bd2b78e977fef626afb28014c80f2", "entities": [ { "tableName": "course_discovery_table", @@ -748,7 +748,7 @@ }, { "tableName": "course_enrollment_details_table", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `courseUpdates` TEXT NOT NULL, `courseHandouts` TEXT NOT NULL, `discussionUrl` TEXT NOT NULL, `hasUnmetPrerequisites` INTEGER NOT NULL, `isTooEarly` INTEGER NOT NULL, `isStaff` INTEGER NOT NULL, `auditAccessExpires` TEXT, `hasAccess` INTEGER, `errorCode` TEXT, `developerMessage` TEXT, `userMessage` TEXT, `additionalContextUserMessage` TEXT, `userFragment` TEXT, `certificateURL` TEXT, `created` TEXT, `mode` TEXT, `isActive` INTEGER NOT NULL, `upgradeDeadline` TEXT, `name` TEXT NOT NULL, `number` TEXT NOT NULL, `org` TEXT NOT NULL, `startDisplay` TEXT NOT NULL, `startType` TEXT NOT NULL, `isSelfPaced` INTEGER NOT NULL, `courseAbout` TEXT NOT NULL, `fastTime` INTEGER, `cdate` TEXT, `fastTime` INTEGER, `cdate` TEXT, `bannerImage` TEXT, `courseImage` TEXT, `courseVideo` TEXT, `image` TEXT, `facebook` TEXT NOT NULL, `twitter` TEXT NOT NULL, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `courseUpdates` TEXT NOT NULL, `courseHandouts` TEXT NOT NULL, `discussionUrl` TEXT NOT NULL, `hasUnmetPrerequisites` INTEGER NOT NULL, `isTooEarly` INTEGER NOT NULL, `isStaff` INTEGER NOT NULL, `auditAccessExpires` TEXT, `hasAccess` INTEGER, `errorCode` TEXT, `developerMessage` TEXT, `userMessage` TEXT, `additionalContextUserMessage` TEXT, `userFragment` TEXT, `certificateURL` TEXT, `created` TEXT, `mode` TEXT, `isActive` INTEGER NOT NULL, `upgradeDeadline` TEXT, `name` TEXT NOT NULL, `number` TEXT NOT NULL, `org` TEXT NOT NULL, `startDisplay` TEXT NOT NULL, `startType` TEXT NOT NULL, `isSelfPaced` INTEGER NOT NULL, `courseAbout` TEXT NOT NULL, `bannerImage` TEXT, `courseImage` TEXT, `courseVideo` TEXT, `image` TEXT, `facebook` TEXT NOT NULL, `twitter` TEXT NOT NULL, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -895,26 +895,6 @@ "affinity": "TEXT", "notNull": true }, - { - "fieldPath": "courseInfoOverview.start.fastTime", - "columnName": "fastTime", - "affinity": "INTEGER" - }, - { - "fieldPath": "courseInfoOverview.start.cdate", - "columnName": "cdate", - "affinity": "TEXT" - }, - { - "fieldPath": "courseInfoOverview.end.fastTime", - "columnName": "fastTime", - "affinity": "INTEGER" - }, - { - "fieldPath": "courseInfoOverview.end.cdate", - "columnName": "cdate", - "affinity": "TEXT" - }, { "fieldPath": "courseInfoOverview.media.bannerImage", "columnName": "bannerImage", @@ -1158,7 +1138,7 @@ ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ad2d11c09b3d243a97daf995a50b761f')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '488bd2b78e977fef626afb28014c80f2')" ] } } \ No newline at end of file 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 c11284276..666fd5297 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 @@ -34,10 +34,8 @@ class AuthRepository( suspend fun ssoLogin( jwtToken: String ) { - if (preferencesManager.accessToken.isBlank() || - preferencesManager.refreshToken.isBlank()){ + if (preferencesManager.accessToken.isBlank()){ preferencesManager.accessToken = jwtToken - preferencesManager.refreshToken = jwtToken } val user = api.getProfile() preferencesManager.user = user 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 ff3aae707..7ae636e36 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 @@ -2,6 +2,7 @@ package org.openedx.auth.presentation.signin import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -44,9 +45,7 @@ class SignInFragment : Fragment() { val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState(null) if (appUpgradeEvent == null) { - setFragmentResultListener("requestKey") { requestKey, bundle -> - viewModel.ssoLogin(token = requestKey) - } + LoginScreen( windowSize = windowSize, state = state, @@ -94,6 +93,13 @@ class SignInFragment : Fragment() { } } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + setFragmentResultListener("requestKey") { _, bundle -> + val token = bundle.getString("bundleKey") + viewModel.ssoLogin(token = "$token") + } + } companion object { private const val ARG_COURSE_ID = "courseId" private const val ARG_INFO_TYPE = "info_type" 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 3a72de661..60716da93 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 @@ -67,7 +67,7 @@ class SignInViewModel( SignInUIState( isLoginRegistrationFormEnabled = config.isLoginRegistrationEnabled(), isSSOLoginEnabled = config.isSSOLoginEnabled(), - ssoButtonTitle = currentLang, + ssoButtonTitle = config.getSSOButtonTitle(currentLang, "Login"), isSSODefaultLoginButton = config.isSSODefaultLoginButton(), isFacebookAuthEnabled = config.getFacebookConfig().isEnabled(), isGoogleAuthEnabled = config.getGoogleConfig().isEnabled(), @@ -157,19 +157,10 @@ class SignInViewModel( _uiState.update { it.copy(showProgress = true) } viewModelScope.launch { try { - interactor.ssoLogin("JWT $token") + interactor.ssoLogin(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 = 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 d5dd976a0..8e81715b6 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 @@ -337,7 +337,7 @@ private fun AuthForm( } if (state.isSSOLoginEnabled) { Spacer(modifier = Modifier.height(18.dp)) - if (state.isLoginRegistrationFormEnabled) { + if (!state.isLoginRegistrationFormEnabled) { Text( modifier = Modifier @@ -396,7 +396,7 @@ private fun AuthForm( buttonWidth .testTag("btn_sso") .fillMaxWidth(), - text = stringResource(id = coreR.string.core_sso_sign_in), + text = state.ssoButtonTitle, borderColor = MaterialTheme.appColors.primary, textColor = MaterialTheme.appColors.textPrimary, onClick = { onEvent(AuthEvent.SsoSignIn(jwtToken = "")) }, 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 3359c3129..c47cb012c 100644 --- a/core/src/main/java/org/openedx/core/config/Config.kt +++ b/core/src/main/java/org/openedx/core/config/Config.kt @@ -32,6 +32,9 @@ class Config(context: Context) { return getString(SSO_URL, "") } + fun getSSOFinishedURL(): String { + return getString(SSO_FINISHED_URL, "") + } fun getUriScheme(): String { return getString(URI_SCHEME) } @@ -125,8 +128,10 @@ class Config(context: Context) { } fun getSSOButtonTitle(key: String, defaultValue: String): String { + print("getSSOButtonTitle") val element = getObject(SSO_BUTTON_TITLE) - return element?.asJsonObject?.get(key)?.asString ?: defaultValue + print("element: $element, key: ${key.uppercase()}") + return element?.asJsonObject?.get(key.uppercase())?.asString ?: defaultValue } fun getCourseUIConfig(): UIConfig { 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 index 6812a30e9..9b75d75f7 100644 --- 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 @@ -9,6 +9,7 @@ import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.setFragmentResult import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel import org.openedx.core.config.Config import org.openedx.core.ui.SSOWebContentScreen import org.openedx.foundation.presentation.rememberWindowSize @@ -32,6 +33,7 @@ class SSOWebContentFragment : Fragment() { url = config.getSSOURL(), uriScheme = requireArguments().getString(ARG_TITLE, ""), title = "", + ssoFinishedUrl = config.getSSOFinishedURL().toString(), onBackClick = { // use it to close the webView requireActivity().supportFragmentManager.popBackStack() @@ -44,7 +46,6 @@ class SSOWebContentFragment : Fragment() { setFragmentResult("requestKey", bundleOf("bundleKey" to token)) requireActivity().supportFragmentManager.popBackStack() } - }) } } diff --git a/core/src/main/java/org/openedx/core/ui/SSOWebContentScreen.kt b/core/src/main/java/org/openedx/core/ui/SSOWebContentScreen.kt index fbc6d3976..febbdcae6 100644 --- a/core/src/main/java/org/openedx/core/ui/SSOWebContentScreen.kt +++ b/core/src/main/java/org/openedx/core/ui/SSOWebContentScreen.kt @@ -40,6 +40,7 @@ fun SSOWebContentScreen( url: String, uriScheme: String, title: String, + ssoFinishedUrl: String, onBackClick: () -> Unit, onWebPageLoaded: () -> Unit, onWebPageUpdated: (String) -> Unit = {}, @@ -47,6 +48,7 @@ fun SSOWebContentScreen( val webView = SSOWebView( url = url, uriScheme = uriScheme, + ssoFinishedUrl = ssoFinishedUrl, onWebPageLoaded = onWebPageLoaded, onWebPageUpdated = onWebPageUpdated ) @@ -111,6 +113,7 @@ fun SSOWebContentScreen( fun SSOWebView( url: String, uriScheme: String, + ssoFinishedUrl: String, onWebPageLoaded: () -> Unit, onWebPageUpdated: (String) -> Unit = {}, ): WebView { @@ -119,11 +122,21 @@ fun SSOWebView( 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 onPageFinished(view: WebView?, pageUrl: String?) { + super.onPageFinished(view, pageUrl) + + if (pageUrl == null) return + + if (pageUrl.contains(ssoFinishedUrl)) { + + val header = getCookie(pageUrl, "edx-jwt-cookie-header-payload") ?: "" + val signature = getCookie(pageUrl, "edx-jwt-cookie-signature") ?: "" + + val token = "$header.$signature" + + if (token.isNotEmpty()) { + onWebPageUpdated(token) + } } } @@ -170,18 +183,21 @@ fun SSOWebView( 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 + if (siteName != null && cookieName != null) { + 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 } + return cookieValue } \ No newline at end of file From e2badfdbff87c3c424bb8113f32adccfa5eab9a6 Mon Sep 17 00:00:00 2001 From: RawanMatar89 <41669180+RawanMatar89@users.noreply.github.com> Date: Mon, 29 Dec 2025 21:51:51 +0200 Subject: [PATCH 6/6] fix: sso login unit tests --- app/src/main/java/org/openedx/app/AppActivity.kt | 11 ++--------- .../auth/presentation/signin/SignInViewModelTest.kt | 1 + 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt index 7aad29075..8b2f8b68a 100644 --- a/app/src/main/java/org/openedx/app/AppActivity.kt +++ b/app/src/main/java/org/openedx/app/AppActivity.kt @@ -70,13 +70,6 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { private val authCode: String? get() { val data = intent?.data - if ( - data is Uri && - data.scheme == BuildConfig.APPLICATION_ID && - data.host == ApiConstants.BrowserLogin.REDIRECT_HOST - ) { - return data.getQueryParameter(ApiConstants.BrowserLogin.CODE_QUERY_PARAM) - } return null } @@ -172,7 +165,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { val fragment = if (viewModel.isLogistrationEnabled && authCode == null) { LogistrationFragment() } else { - SignInFragment.newInstance(null, null, authCode = authCode) + SignInFragment.newInstance(null, null) } addFragment(fragment) } @@ -220,7 +213,7 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder { this.intent = intent if (authCode != null) { - addFragment(SignInFragment.newInstance(null, null, authCode = authCode)) + addFragment(SignInFragment.newInstance(null, null)) } val extras = intent.extras diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt index a8be7a73a..ff1f5d450 100644 --- a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt +++ b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt @@ -96,6 +96,7 @@ class SignInViewModelTest { every { config.isLoginRegistrationEnabled() } returns true every { config.isSSOLoginEnabled() } returns false every { config.isSSODefaultLoginButton() } returns false + every { config.getSSOButtonTitle(any(), any()) } returns "SSO Login" } @After