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