diff --git a/app/schemas/org.openedx.app.room.AppDatabase/4.json b/app/schemas/org.openedx.app.room.AppDatabase/4.json
index 0bf47775d..53c0f4277 100644
--- a/app/schemas/org.openedx.app.room.AppDatabase/4.json
+++ b/app/schemas/org.openedx.app.room.AppDatabase/4.json
@@ -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,9 +744,7 @@
"columnNames": [
"course_id"
]
- },
- "indices": [],
- "foreignKeys": []
+ }
},
{
"tableName": "course_enrollment_details_table",
@@ -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",
@@ -963,26 +898,22 @@
{
"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 +933,7 @@
"columnNames": [
"id"
]
- },
- "indices": [],
- "foreignKeys": []
+ }
},
{
"tableName": "video_progress_table",
@@ -1040,9 +969,7 @@
"columnNames": [
"block_id"
]
- },
- "indices": [],
- "foreignKeys": []
+ }
},
{
"tableName": "course_progress_table",
@@ -1123,98 +1050,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,12 +1133,9 @@
"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, '7ea446decde04c9c16700cb3981703c2')"
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 b904bf6a1..7aad29075 100644
--- a/app/src/main/java/org/openedx/app/AppActivity.kt
+++ b/app/src/main/java/org/openedx/app/AppActivity.kt
@@ -160,12 +160,8 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
WindowCompat.setDecorFitsSystemWindows(this, false)
val insetsController = WindowInsetsControllerCompat(this, binding.root)
insetsController.isAppearanceLightStatusBars = !isUsingNightModeResources()
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
- insetsController.systemBarsBehavior =
- WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
- } else {
- window.statusBarColor = Color.TRANSPARENT
- }
+ insetsController.systemBarsBehavior =
+ WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
diff --git a/app/src/main/java/org/openedx/app/AppRouter.kt b/app/src/main/java/org/openedx/app/AppRouter.kt
index 4678344ee..3c3e6f93f 100644
--- a/app/src/main/java/org/openedx/app/AppRouter.kt
+++ b/app/src/main/java/org/openedx/app/AppRouter.kt
@@ -13,6 +13,7 @@ import org.openedx.core.CalendarRouter
import org.openedx.core.FragmentViewType
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/AppModule.kt b/app/src/main/java/org/openedx/app/di/AppModule.kt
index cdb240387..d60dc77e2 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
@@ -215,7 +214,6 @@ val appModule = module {
factory { FacebookAuthHelper() }
factory { GoogleAuthHelper(get()) }
factory { MicrosoftAuthHelper() }
- factory { BrowserAuthHelper(get()) }
factory { OAuthHelper(get(), get(), get()) }
factory { VideoPreviewHelper(get(), get()) }
diff --git a/app/src/main/java/org/openedx/app/di/ScreenModule.kt b/app/src/main/java/org/openedx/app/di/ScreenModule.kt
index 1d3604050..013cff2f5 100644
--- a/app/src/main/java/org/openedx/app/di/ScreenModule.kt
+++ b/app/src/main/java/org/openedx/app/di/ScreenModule.kt
@@ -79,6 +79,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 {
@@ -108,11 +109,11 @@ val screenModule = module {
get(),
get(),
get(),
- get(),
)
}
- viewModel { (courseId: String?, infoType: String?, authCode: String) ->
+ val lang = Resources.getSystem().configuration.locales[0].language
+ viewModel { (courseId: String?, infoType: String?) ->
SignInViewModel(
get(),
get(),
@@ -130,7 +131,7 @@ val screenModule = module {
get(),
courseId,
infoType,
- authCode,
+ lang
)
}
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..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
@@ -31,6 +31,16 @@ class AuthRepository(
.processAuthResponse()
}
+ suspend fun ssoLogin(
+ jwtToken: String
+ ) {
+ if (preferencesManager.accessToken.isBlank()){
+ preferencesManager.accessToken = jwtToken
+ }
+ val user = api.getProfile()
+ preferencesManager.user = user
+ }
+
suspend fun socialLogin(token: String?, authType: AuthType) {
require(!token.isNullOrBlank()) { "Token is null" }
api.exchangeAccessToken(
@@ -43,16 +53,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..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,12 +14,14 @@ class AuthInteractor(private val repository: AuthRepository) {
repository.login(username, password)
}
- suspend fun loginSocial(token: String?, authType: AuthType) {
- repository.socialLogin(token, authType)
+ suspend fun ssoLogin(
+ jwtToken: String
+ ) {
+ repository.ssoLogin(jwtToken)
}
- suspend fun loginAuthCode(authCode: String) {
- repository.browserAuthCodeLogin(authCode)
+ suspend fun loginSocial(token: String?, authType: AuthType) {
+ repository.socialLogin(token, authType)
}
suspend fun getRegistrationFields(): List {
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/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..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
@@ -11,6 +12,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
@@ -25,8 +27,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 +45,7 @@ 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,
@@ -54,6 +53,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
@@ -63,10 +63,6 @@ class SignInFragment : Fragment() {
viewModel.navigateToForgotPassword(parentFragmentManager)
}
- AuthEvent.SignInBrowser -> {
- viewModel.signInBrowser(requireActivity())
- }
-
AuthEvent.RegisterClick -> {
viewModel.navigateToSignUp(parentFragmentManager)
}
@@ -97,16 +93,21 @@ 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"
- 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
}
@@ -115,9 +116,9 @@ class SignInFragment : Fragment() {
internal sealed interface AuthEvent {
data class SignIn(val login: String, val password: String) : AuthEvent
+ data class SsoSignIn(val jwtToken: String) : AuthEvent
data class SocialSignIn(val authType: AuthType) : AuthEvent
data class OpenLink(val links: Map, val link: String) : AuthEvent
- object SignInBrowser : AuthEvent
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..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,16 +13,17 @@ 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,
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..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
@@ -1,6 +1,6 @@
package org.openedx.auth.presentation.signin
-import android.app.Activity
+import android.content.res.Resources
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LiveData
@@ -21,7 +21,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
@@ -50,27 +49,29 @@ 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,
private val calendarInteractor: CalendarInteractor,
agreementProvider: AgreementProvider,
- private val browserAuthHelper: BrowserAuthHelper,
- val config: Config,
+ config: Config,
val courseId: String?,
val infoType: String?,
- val authCode: String,
+ currentLang: String,
) : BaseViewModel() {
private val logger = Logger("SignInViewModel")
private val _uiState = MutableStateFlow(
SignInUIState(
+ isLoginRegistrationFormEnabled = config.isLoginRegistrationEnabled(),
+ isSSOLoginEnabled = config.isSSOLoginEnabled(),
+ ssoButtonTitle = config.getSSOButtonTitle(currentLang, "Login"),
+ isSSODefaultLoginButton = config.isSSODefaultLoginButton(),
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(),
@@ -141,6 +142,41 @@ 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(token)
+ _uiState.update { it.copy(loginSuccess = true) }
+ setUserId()
+
+ } 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 ->
@@ -164,41 +200,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..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
@@ -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,9 +237,8 @@ private fun AuthForm(
val keyboardController = LocalSoftwareKeyboardController.current
var isEmailError by rememberSaveable { mutableStateOf(false) }
var isPasswordError by rememberSaveable { mutableStateOf(false) }
-
- Column(horizontalAlignment = Alignment.CenterHorizontally) {
- if (!state.isBrowserLoginEnabled) {
+ if (state.isLoginRegistrationFormEnabled) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
LoginTextField(
modifier = Modifier
.fillMaxWidth(),
@@ -259,16 +271,12 @@ private fun AuthForm(
},
isError = isPasswordError,
)
- } else {
- Spacer(modifier = Modifier.height(40.dp))
- }
- Row(
- Modifier
- .fillMaxWidth()
- .padding(top = 20.dp, bottom = 36.dp)
- ) {
- if (!state.isBrowserLoginEnabled) {
+ Row(
+ Modifier
+ .fillMaxWidth()
+ .padding(top = 20.dp, bottom = 36.dp)
+ ) {
if (state.isLogistrationEnabled.not() && state.isRegistrationEnabled) {
Text(
modifier = Modifier
@@ -293,20 +301,16 @@ private fun AuthForm(
style = MaterialTheme.appTypography.labelLarge
)
}
- }
- if (state.showProgress) {
- CircularProgressIndicator(color = MaterialTheme.appColors.primary)
- } else {
- OpenEdXButton(
- modifier = buttonWidth.testTag("btn_sign_in"),
- text = stringResource(id = coreR.string.core_sign_in),
- textColor = MaterialTheme.appColors.primaryButtonText,
- backgroundColor = MaterialTheme.appColors.secondaryButtonBackground,
- onClick = {
- if (state.isBrowserLoginEnabled) {
- onEvent(AuthEvent.SignInBrowser)
- } else {
+ 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))
@@ -315,19 +319,88 @@ private fun AuthForm(
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_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.isSocialAuthEnabled) {
- SocialAuthView(
- modifier = buttonWidth,
- isGoogleAuthEnabled = state.isGoogleAuthEnabled,
- isFacebookAuthEnabled = state.isFacebookAuthEnabled,
- isMicrosoftAuthEnabled = state.isMicrosoftAuthEnabled,
- isSignIn = true,
- ) {
- keyboardController?.hide()
- onEvent(AuthEvent.SocialSignIn(it))
+ if (state.showProgress) {
+ CircularProgressIndicator(color = MaterialTheme.appColors.primary)
+ } else {
+ if (state.isSSODefaultLoginButton) {
+ OpenEdXButton(
+ modifier =
+ buttonWidth
+ .testTag("btn_sso")
+ .fillMaxWidth(),
+ text = state.ssoButtonTitle,
+ onClick = {
+ onEvent(AuthEvent.SsoSignIn(jwtToken = ""))
+ },
+ )
+ } else {
+ OpenEdXOutlinedButton(
+ modifier =
+ buttonWidth
+ .testTag("btn_sso")
+ .fillMaxWidth(),
+ text = state.ssoButtonTitle,
+ borderColor = MaterialTheme.appColors.primary,
+ textColor = MaterialTheme.appColors.textPrimary,
+ onClick = { onEvent(AuthEvent.SsoSignIn(jwtToken = "")) },
+ )
}
}
}
@@ -347,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))
@@ -364,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,
@@ -431,24 +510,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
@@ -456,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/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 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/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 4a0db245c..a8be7a73a 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.CoreMocks
import org.openedx.core.Validator
@@ -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"
@@ -95,8 +93,9 @@ 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
+ every { config.isLoginRegistrationEnabled() } returns true
+ every { config.isSSOLoginEnabled() } returns false
+ every { config.isSSODefaultLoginButton() } returns false
}
@After
@@ -122,12 +121,12 @@ class SignInViewModelTest {
config = config,
router = router,
whatsNewGlobalManager = whatsNewGlobalManager,
- browserAuthHelper = browserAuthHelper,
courseId = "",
infoType = "",
calendarInteractor = calendarInteractor,
calendarPreferences = calendarPreferences,
- authCode = "",
+ configuration = config,
+ currentLang = "EN"
)
viewModel.login("", "")
coVerify(exactly = 0) { interactor.login(any(), any()) }
@@ -160,12 +159,12 @@ class SignInViewModelTest {
config = config,
router = router,
whatsNewGlobalManager = whatsNewGlobalManager,
- browserAuthHelper = browserAuthHelper,
courseId = "",
infoType = "",
calendarInteractor = calendarInteractor,
calendarPreferences = calendarPreferences,
- authCode = "",
+ configuration = config,
+ currentLang = "EN"
)
viewModel.login("acc@test.o", "")
coVerify(exactly = 0) { interactor.login(any(), any()) }
@@ -198,12 +197,12 @@ class SignInViewModelTest {
config = config,
router = router,
whatsNewGlobalManager = whatsNewGlobalManager,
- browserAuthHelper = browserAuthHelper,
courseId = "",
infoType = "",
calendarInteractor = calendarInteractor,
calendarPreferences = calendarPreferences,
- authCode = "",
+ configuration = config,
+ currentLang = "EN"
)
viewModel.login("acc@test.org", "")
@@ -235,12 +234,12 @@ class SignInViewModelTest {
config = config,
router = router,
whatsNewGlobalManager = whatsNewGlobalManager,
- browserAuthHelper = browserAuthHelper,
courseId = "",
infoType = "",
calendarInteractor = calendarInteractor,
calendarPreferences = calendarPreferences,
- authCode = "",
+ configuration = config,
+ currentLang = "EN"
)
viewModel.login("acc@test.org", "ed")
@@ -276,12 +275,12 @@ class SignInViewModelTest {
config = config,
router = router,
whatsNewGlobalManager = whatsNewGlobalManager,
- browserAuthHelper = browserAuthHelper,
courseId = "",
infoType = "",
calendarInteractor = calendarInteractor,
calendarPreferences = calendarPreferences,
- authCode = "",
+ configuration = config,
+ currentLang = "EN"
)
coEvery { interactor.login("acc@test.org", "edx") } returns Unit
viewModel.login("acc@test.org", "edx")
@@ -317,12 +316,12 @@ class SignInViewModelTest {
config = config,
router = router,
whatsNewGlobalManager = whatsNewGlobalManager,
- browserAuthHelper = browserAuthHelper,
courseId = "",
infoType = "",
calendarInteractor = calendarInteractor,
calendarPreferences = calendarPreferences,
- authCode = "",
+ configuration = config,
+ currentLang = "EN"
)
coEvery { interactor.login("acc@test.org", "edx") } throws UnknownHostException()
viewModel.login("acc@test.org", "edx")
@@ -360,12 +359,12 @@ class SignInViewModelTest {
config = config,
router = router,
whatsNewGlobalManager = whatsNewGlobalManager,
- browserAuthHelper = browserAuthHelper,
courseId = "",
infoType = "",
calendarInteractor = calendarInteractor,
calendarPreferences = calendarPreferences,
- authCode = "",
+ configuration = config,
+ currentLang = "EN"
)
coEvery { interactor.login("acc@test.org", "edx") } throws EdxError.InvalidGrantException()
viewModel.login("acc@test.org", "edx")
@@ -403,12 +402,12 @@ class SignInViewModelTest {
config = config,
router = router,
whatsNewGlobalManager = whatsNewGlobalManager,
- browserAuthHelper = browserAuthHelper,
courseId = "",
infoType = "",
calendarInteractor = calendarInteractor,
calendarPreferences = calendarPreferences,
- authCode = "",
+ configuration = config,
+ currentLang = "EN"
)
coEvery { interactor.login("acc@test.org", "edx") } throws IllegalStateException()
viewModel.login("acc@test.org", "edx")
diff --git a/build.gradle b/build.gradle
index 674a1057f..9afabd777 100644
--- a/build.gradle
+++ b/build.gradle
@@ -87,6 +87,8 @@ ext {
java_version = JavaVersion.VERSION_17
jvm_target_version = JvmTarget.JVM_17
+ webkit_version = "1.11.0"
+
configHelper = new ConfigHelper(projectDir, getCurrentFlavor())
}
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..c47cb012c 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,13 @@ class Config(context: Context) {
return getString(API_HOST_URL)
}
+ fun getSSOURL(): String {
+ return getString(SSO_URL, "")
+ }
+
+ fun getSSOFinishedURL(): String {
+ return getString(SSO_FINISHED_URL, "")
+ }
fun getUriScheme(): String {
return getString(URI_SCHEME)
}
@@ -108,20 +115,31 @@ class Config(context: Context) {
return getBoolean(PRE_LOGIN_EXPERIENCE_ENABLED, true)
}
- fun getCourseUIConfig(): UIConfig {
- return getObjectOrNewInstance(UI_COMPONENTS, UIConfig::class.java)
+ fun isLoginRegistrationEnabled(): Boolean {
+ return getBoolean(LOGIN_REGISTRATION_ENABLED, true)
}
- fun isRegistrationEnabled(): Boolean {
- return getBoolean(REGISTRATION_ENABLED, true)
+ fun isSSOLoginEnabled(): Boolean {
+ return getBoolean(SAML_SSO_LOGIN_ENABLED, false)
}
- fun isBrowserLoginEnabled(): Boolean {
- return getBoolean(BROWSER_LOGIN, false)
+ fun isSSODefaultLoginButton(): Boolean {
+ return getBoolean(SAML_SSO_DEFAULT_LOGIN_BUTTON, false)
}
- fun isBrowserRegistrationEnabled(): Boolean {
- return getBoolean(BROWSER_REGISTRATION, false)
+ fun getSSOButtonTitle(key: String, defaultValue: String): String {
+ print("getSSOButtonTitle")
+ val element = getObject(SSO_BUTTON_TITLE)
+ print("element: $element, key: ${key.uppercase()}")
+ return element?.asJsonObject?.get(key.uppercase())?.asString ?: defaultValue
+ }
+
+ fun getCourseUIConfig(): UIConfig {
+ return getObjectOrNewInstance(UI_COMPONENTS, UIConfig::class.java)
+ }
+
+ fun isRegistrationEnabled(): Boolean {
+ return getBoolean(REGISTRATION_ENABLED, true)
}
private fun getExperimentalFeaturesConfig(): ExperimentalFeaturesConfig {
@@ -167,6 +185,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"
@@ -182,8 +206,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/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..9b75d75f7
--- /dev/null
+++ b/core/src/main/java/org/openedx/core/presentation/global/webview/SSOWebContentFragment.kt
@@ -0,0 +1,72 @@
+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.koin.androidx.viewmodel.ext.android.viewModel
+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 = "",
+ ssoFinishedUrl = config.getSSOFinishedURL().toString(),
+ 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 eed214567..0b4941949 100644
--- a/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt
+++ b/core/src/main/java/org/openedx/core/ui/ComposeCommon.kt
@@ -982,6 +982,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
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..febbdcae6
--- /dev/null
+++ b/core/src/main/java/org/openedx/core/ui/SSOWebContentScreen.kt
@@ -0,0 +1,203 @@
+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,
+ ssoFinishedUrl: String,
+ onBackClick: () -> Unit,
+ onWebPageLoaded: () -> Unit,
+ onWebPageUpdated: (String) -> Unit = {},
+){
+ val webView = SSOWebView(
+ url = url,
+ uriScheme = uriScheme,
+ ssoFinishedUrl = ssoFinishedUrl,
+ 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,
+ ssoFinishedUrl: String,
+ onWebPageLoaded: () -> Unit,
+ onWebPageUpdated: (String) -> Unit = {},
+): WebView {
+ val context = LocalContext.current
+
+ return remember {
+ WebView(context).apply {
+ webViewClient = object : WebViewClient() {
+ 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)
+ }
+ }
+ }
+
+ 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? = ""
+ 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
diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml
index f4fabd553..11c12dc42 100644
--- a/core/src/main/res/values/strings.xml
+++ b/core/src/main/res/values/strings.xml
@@ -217,4 +217,8 @@
Explore other parts of this course or download this when you reconnect.
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 952e041de..51e2a50cc 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:
@@ -83,10 +92,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..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:
@@ -81,12 +90,9 @@ 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
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 a7f265a45..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:
@@ -81,12 +90,9 @@ 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
COURSE_UNIT_PROGRESS_ENABLED: false
COURSE_DOWNLOAD_QUEUE_SCREEN: 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:
-
-*
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