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