Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for login and registration via a browser custom tab #371

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Commits on Jul 30, 2024

  1. feat: Support for login and registration via a browser custom tab

    This change adds support for logging in and registering a new account using the
    browser. This can be useful for cases where the only way to log into the
    instatance is via a custom third-party auth provider.
    
    diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
    index 8020f6b..e2c208d 100644
    --- a/app/src/main/AndroidManifest.xml
    +++ b/app/src/main/AndroidManifest.xml
    @@ -45,6 +45,12 @@
    
                     <category android:name="android.intent.category.LAUNCHER" />
                 </intent-filter>
    +            <intent-filter>
    +                <action android:name="android.intent.action.VIEW" />
    +                <category android:name="android.intent.category.DEFAULT" />
    +                <category android:name="android.intent.category.BROWSABLE" />
    +                <data android:scheme="${applicationId}"  />
    +            </intent-filter>
    
                 <!-- Branch URI Scheme -->
                 <intent-filter>
    diff --git a/app/src/main/java/org/openedx/app/AppActivity.kt b/app/src/main/java/org/openedx/app/AppActivity.kt
    index 5ab0d0b..6e5089e 100644
    --- a/app/src/main/java/org/openedx/app/AppActivity.kt
    +++ b/app/src/main/java/org/openedx/app/AppActivity.kt
    @@ -3,6 +3,7 @@ package org.openedx.app
     import android.content.Intent
     import android.content.res.Configuration
     import android.graphics.Color
    +import android.net.Uri
     import android.os.Bundle
     import android.view.View
     import android.view.WindowManager
    @@ -56,6 +57,14 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
         private var _insetCutout = 0
    
         private var _windowSize = WindowSize(WindowType.Compact, WindowType.Compact)
    +    private val authCode: String?
    +        get() {
    +            val data = intent?.data
    +            if (data is Uri && data.scheme == BuildConfig.APPLICATION_ID && data.host == "oauth2Callback") {
    +                return data.getQueryParameter("code")
    +            }
    +            return null
    +        }
    
         override fun onSaveInstanceState(outState: Bundle) {
             outState.putInt(TOP_INSET, topInset)
    @@ -119,10 +128,15 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
             if (savedInstanceState == null) {
                 when {
                     corePreferencesManager.user == null -> {
    -                    if (viewModel.isLogistrationEnabled) {
    +                    val authCode = authCode;
    +                    if (viewModel.isLogistrationEnabled && authCode == null) {
                             addFragment(LogistrationFragment())
                         } else {
    -                        addFragment(SignInFragment())
    +                        val bundle = Bundle()
    +                        bundle.putString("auth_code", authCode)
    +                        val fragment = SignInFragment()
    +                        fragment.arguments = bundle
    +                        addFragment(fragment)
                         }
                     }
    
    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 16a30c0..0d3feb4 100644
    --- a/app/src/main/java/org/openedx/app/di/AppModule.kt
    +++ b/app/src/main/java/org/openedx/app/di/AppModule.kt
    @@ -21,6 +21,7 @@ import org.openedx.app.system.notifier.AppNotifier
     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
    @@ -180,5 +181,6 @@ val appModule = module {
         factory { FacebookAuthHelper() }
         factory { GoogleAuthHelper(get()) }
         factory { MicrosoftAuthHelper() }
    +    factory { BrowserAuthHelper(get()) }
         factory { OAuthHelper(get(), get(), get()) }
     }
    diff --git a/auth/build.gradle b/auth/build.gradle
    index 7cf4d0a..b66db95 100644
    --- a/auth/build.gradle
    +++ b/auth/build.gradle
    @@ -55,6 +55,7 @@ android {
     dependencies {
         implementation project(path: ':core')
    
    +    implementation 'androidx.browser:browser:1.7.0'
         implementation "androidx.credentials:credentials:1.2.0"
         implementation "androidx.credentials:credentials-play-services-auth:1.2.0"
         implementation "com.facebook.android:facebook-login:16.2.0"
    diff --git a/auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt b/auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt
    index 903cbd6..6d40554 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
    @@ -32,6 +32,14 @@ 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,
    +    ): 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 5addd62..c56ba0c 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,4 +13,5 @@ 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 6cf54a7..a7d364a 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
    @@ -1,5 +1,6 @@
     package org.openedx.auth.data.repository
    
    +import android.util.Log
     import org.openedx.auth.data.api.AuthApi
     import org.openedx.auth.data.model.AuthType
     import org.openedx.auth.data.model.ValidationFields
    @@ -43,6 +44,14 @@ class AuthRepository(
                 .processAuthResponse()
         }
    
    +    suspend fun browserAuthCodeLogin(code: String) {
    +        api.getAccessTokenFromCode(
    +            grantType = ApiConstants.GRANT_TYPE_CODE,
    +            clientId = config.getOAuthClientId(),
    +            code = code,
    +        ).mapToDomain().processAuthResponse()
    +    }
    +
         suspend fun getRegistrationFields(): List<RegistrationField> {
             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 00fe509..d81c51e 100644
    --- a/auth/src/main/java/org/openedx/auth/domain/interactor/AuthInteractor.kt
    +++ b/auth/src/main/java/org/openedx/auth/domain/interactor/AuthInteractor.kt
    @@ -18,6 +18,10 @@ class AuthInteractor(private val repository: AuthRepository) {
             repository.socialLogin(token, authType)
         }
    
    +    suspend fun loginAuthCode(authCode: String) {
    +        repository.browserAuthCodeLogin(authCode)
    +    }
    +
         suspend fun getRegistrationFields(): List<RegistrationField> {
             return repository.getRegistrationFields()
         }
    diff --git a/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt b/auth/src/main/java/org/openedx/auth/presentation/logistration/LogistrationFragment.kt
    index 738364c..0b615f3 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
    @@ -1,6 +1,8 @@
     package org.openedx.auth.presentation.logistration
    
    +import android.content.Intent
     import android.content.res.Configuration
    +import android.net.Uri
     import android.os.Bundle
     import android.view.LayoutInflater
     import android.view.ViewGroup
    @@ -41,6 +43,9 @@ 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.auth.presentation.AuthRouter
    +import org.openedx.core.config.Config
    +import org.openedx.core.presentation.dialog.alert.ActionDialogFragment
     import org.openedx.core.ui.AuthButtonsPanel
     import org.openedx.core.ui.SearchBar
     import org.openedx.core.ui.displayCutoutForLandscape
    @@ -48,6 +53,7 @@ import org.openedx.core.ui.noRippleClickable
     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.utils.UrlUtils
     import org.openedx.core.ui.theme.compose.LogistrationLogoView
    
     class LogistrationFragment : Fragment() {
    @@ -55,6 +61,8 @@ class LogistrationFragment : Fragment() {
         private val viewModel: LogistrationViewModel by viewModel {
             parametersOf(arguments?.getString(ARG_COURSE_ID, "") ?: "")
         }
    +    private val router: AuthRouter by inject()
    +    private val config: Config by inject()
    
         override fun onCreateView(
             inflater: LayoutInflater,
    @@ -70,6 +78,15 @@ class LogistrationFragment : Fragment() {
                         },
                         onRegisterClick = {
                             viewModel.navigateToSignUp(parentFragmentManager)
    +                        if (config.isBrowserRegistrationEnabled()) {
    +                            UrlUtils.openInBrowser(
    +                                activity = context,
    +                                apiHostUrl = config.getApiHostURL(),
    +                                url = "/register",
    +                            )
    +                        } else {
    +                            router.navigateToSignUp(parentFragmentManager, courseId)
    +                        }
                         },
                         onSearchClick = { querySearch ->
                             viewModel.navigateToDiscovery(parentFragmentManager, querySearch)
    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 fabd8a4..e89c000 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
    @@ -1,6 +1,7 @@
     package org.openedx.auth.presentation.signin
    
     import android.os.Bundle
    +import android.util.Log
     import android.view.LayoutInflater
     import android.view.ViewGroup
     import androidx.compose.runtime.LaunchedEffect
    @@ -43,6 +44,11 @@ class SignInFragment : Fragment() {
                     val appUpgradeEvent by viewModel.appUpgradeEvent.observeAsState(null)
    
                     if (appUpgradeEvent == null) {
    +                    val authCode = arguments?.getString("auth_code")
    +                    if (authCode is String && !state.loginFailure && !state.loginSuccess) {
    +                        arguments?.remove("auth_code")
    +                        viewModel.signInAuthCode(authCode)
    +                    }
                         LoginScreen(
                             windowSize = windowSize,
                             state = state,
    @@ -59,6 +65,10 @@ class SignInFragment : Fragment() {
                                         viewModel.navigateToForgotPassword(parentFragmentManager)
                                     }
    
    +                                AuthEvent.SignInBrowser -> {
    +                                    viewModel.signInBrowser(requireActivity())
    +                                }
    +
                                     AuthEvent.RegisterClick -> {
                                         viewModel.navigateToSignUp(parentFragmentManager)
                                     }
    @@ -107,6 +117,7 @@ internal sealed interface AuthEvent {
         data class SignIn(val login: String, val password: String) : AuthEvent
         data class SocialSignIn(val authType: AuthType) : AuthEvent
         data class OpenLink(val links: Map<String, String>, 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 9ce5cfc..8954c1f 100644
    --- a/auth/src/main/java/org/openedx/auth/presentation/signin/SignInUIState.kt
    +++ b/auth/src/main/java/org/openedx/auth/presentation/signin/SignInUIState.kt
    @@ -17,8 +17,11 @@ internal data class SignInUIState(
         val isGoogleAuthEnabled: Boolean = false,
         val isMicrosoftAuthEnabled: Boolean = false,
         val isSocialAuthEnabled: Boolean = false,
    +    val isBrowserLoginEnabled: Boolean = false,
    +    val isBrowserRegistrationEnabled: Boolean = false,
         val isLogistrationEnabled: Boolean = false,
         val 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 7ebc5a5..4e6db6a 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
    @@ -17,6 +17,7 @@ import org.openedx.auth.domain.interactor.AuthInteractor
     import org.openedx.auth.domain.model.SocialAuthResponse
     import org.openedx.auth.presentation.AgreementProvider
     import org.openedx.auth.presentation.AuthAnalytics
    +import org.openedx.auth.presentation.sso.BrowserAuthHelper
     import org.openedx.auth.presentation.AuthAnalyticsEvent
     import org.openedx.auth.presentation.AuthAnalyticsKey
     import org.openedx.auth.presentation.AuthRouter
    @@ -49,6 +50,11 @@ class SignInViewModel(
         private val whatsNewGlobalManager: WhatsNewGlobalManager,
         agreementProvider: AgreementProvider,
         config: Config,
    +    private val facebookAuthHelper: FacebookAuthHelper,
    +    private val googleAuthHelper: GoogleAuthHelper,
    +    private val microsoftAuthHelper: MicrosoftAuthHelper,
    +    private val browserAuthHelper: BrowserAuthHelper,
    +    val config: Config,
         val courseId: String?,
         val infoType: String?,
     ) : BaseViewModel() {
    @@ -60,6 +66,8 @@ class SignInViewModel(
                 isFacebookAuthEnabled = config.getFacebookConfig().isEnabled(),
                 isGoogleAuthEnabled = config.getGoogleConfig().isEnabled(),
                 isMicrosoftAuthEnabled = config.getMicrosoftConfig().isEnabled(),
    +            isBrowserLoginEnabled = config.isBrowserLoginEnabled(),
    +            isBrowserRegistrationEnabled = config.isBrowserRegistrationEnabled(),
                 isSocialAuthEnabled = config.isSocialAuthEnabled(),
                 isLogistrationEnabled = config.isPreLoginExperienceEnabled(),
                 agreement = agreementProvider.getAgreement(isSignIn = true)?.createHonorCodeField(),
    @@ -144,11 +152,42 @@ 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 {
    +                    logger.d { "Browser login success" }
    +                    _uiState.update { it.copy(loginSuccess = true) }
    +                    setUserId()
    +                    analytics.userLoginEvent(AuthType.BROWSER.methodName)
    +                    _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 77e2909..642ab2f 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
    @@ -218,55 +218,60 @@ private fun AuthForm(
         var password by rememberSaveable { mutableStateOf("") }
    
         Column(horizontalAlignment = Alignment.CenterHorizontally) {
    -        LoginTextField(
    -            modifier = Modifier
    -                .fillMaxWidth(),
    -            title = stringResource(id = R.string.auth_email_username),
    -            description = stringResource(id = R.string.auth_enter_email_username),
    -            onValueChanged = {
    -                login = it
    -            })
    +        if (!state.isBrowserLoginEnabled) {
    +            LoginTextField(
    +                modifier = Modifier
    +                    .fillMaxWidth(),
    +                title = stringResource(id = R.string.auth_email_username),
    +            description = stringResource(id = R.string.auth_enter_email_username),onValueChanged = {
    +                    login = it
    +                })
    
    -        Spacer(modifier = Modifier.height(18.dp))
    -        PasswordTextField(
    -            modifier = Modifier
    -                .fillMaxWidth(),
    -            onValueChanged = {
    -                password = it
    -            },
    -            onPressDone = {
    -                onEvent(AuthEvent.SignIn(login = login, password = password))
    -            }
    -        )
    +            Spacer(modifier = Modifier.height(18.dp))
    +            PasswordTextField(
    +                modifier = Modifier
    +                    .fillMaxWidth(),
    +                onValueChanged = {
    +                    password = it
    +                },
    +                onPressDone = {
    +                    onEvent(AuthEvent.SignIn(login = login, password = password))
    +                }
    +            )
    +        } else {
    +            Spacer(modifier = Modifier.height(40.dp))
    +        }
    
             Row(
                 Modifier
                     .fillMaxWidth()
                     .padding(top = 20.dp, bottom = 36.dp)
             ) {
    -            if (state.isLogistrationEnabled.not()) {
    -                Text(
    -                    modifier = Modifier
    +            if (!state.isBrowserLoginEnabled) {
    +                if (state.isLogistrationEnabled.not()) {
    +                    Text(
    +                        modifier = Modifier
                             .testTag("txt_register")
                             .noRippleClickable {
                                 onEvent(AuthEvent.RegisterClick)
                             },
    -                    text = stringResource(id = coreR.string.core_register),
    -                    color = MaterialTheme.appColors.primary,
    -                    style = MaterialTheme.appTypography.labelLarge
    -                )
    -            }
    -            Spacer(modifier = Modifier.weight(1f))
    -            Text(
    -                modifier = Modifier
    +                        text = stringResource(id = coreR.string.core_register),
    +                        color = MaterialTheme.appColors.primary,
    +                        style = MaterialTheme.appTypography.labelLarge
    +                    )
    +                }
    +                Spacer(modifier = Modifier.weight(1f))
    +                Text(
    +                    modifier = Modifier
                         .testTag("txt_forgot_password")
                         .noRippleClickable {
                             onEvent(AuthEvent.ForgotPasswordClick)
                         },
    -                text = stringResource(id = R.string.auth_forgot_password),
    -                color = MaterialTheme.appColors.primary,
    -                style = MaterialTheme.appTypography.labelLarge
    -            )
    +                    text = stringResource(id = R.string.auth_forgot_password),
    +                    color = MaterialTheme.appColors.primary,
    +                    style = MaterialTheme.appTypography.labelLarge
    +                )
    +            }
             }
    
             if (state.showProgress) {
    @@ -276,7 +281,11 @@ private fun AuthForm(
                     modifier = buttonWidth.testTag("btn_sign_in"),
                     text = stringResource(id = coreR.string.core_sign_in),
                     onClick = {
    -                    onEvent(AuthEvent.SignIn(login = login, password = password))
    +                    if(state.isBrowserLoginEnabled) {
    +                        onEvent(AuthEvent.SignInBrowser)
    +                    } else {
    +                        onEvent(AuthEvent.SignIn(login = login, password = password))
    +                    }
                     }
                 )
             }
    @@ -365,6 +374,24 @@ private fun SignInScreenPreview() {
         }
     }
    
    +@Preview(uiMode = UI_MODE_NIGHT_NO)
    +@Preview(uiMode = UI_MODE_NIGHT_YES)
    +@Preview(name = "NEXUS_5_Light", device = Devices.NEXUS_5, uiMode = UI_MODE_NIGHT_NO)
    +@Preview(name = "NEXUS_5_Dark", device = Devices.NEXUS_5, uiMode = UI_MODE_NIGHT_YES)
    +@composable
    +private fun SignInUsingBrowserScreenPreview() {
    +    OpenEdXTheme {
    +        LoginScreen(
    +            windowSize = WindowSize(WindowType.Compact, WindowType.Compact),
    +            state = SignInUIState().copy(
    +                isBrowserLoginEnabled = true,
    +            ),
    +            uiMessage = null,
    +            onEvent = {},
    +        )
    +    }
    +}
    +
     @Preview(name = "NEXUS_9_Light", device = Devices.NEXUS_9, uiMode = UI_MODE_NIGHT_NO)
     @Preview(name = "NEXUS_9_Night", device = Devices.NEXUS_9, uiMode = UI_MODE_NIGHT_YES)
     @composable
    diff --git a/auth/src/main/java/org/openedx/auth/presentation/sso/BrowserAuthHelper.kt b/auth/src/main/java/org/openedx/auth/presentation/sso/BrowserAuthHelper.kt
    new file mode 100644
    index 0000000..5822bab
    --- /dev/null
    +++ b/auth/src/main/java/org/openedx/auth/presentation/sso/BrowserAuthHelper.kt
    @@ -0,0 +1,32 @@
    +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.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()}/oauth2/authorize").buildUpon()
    +            .appendQueryParameter("client_id", config.getOAuthClientId())
    +            .appendQueryParameter("redirect_uri", "${activityContext.packageName}://oauth2Callback")
    +            .appendQueryParameter("response_type", "code").build()
    +        val intent =
    +            CustomTabsIntent.Builder().setUrlBarHidingEnabled(true).setShowTitle(true).build()
    +        intent.intent.setFlags(FLAG_ACTIVITY_NEW_TASK)
    +        logger.d { "Launching custom tab with ${uri.toString()}"}
    +        intent.launchUrl(activityContext, uri)
    +    }
    +
    +    private companion object {
    +        const val TAG = "BrowserAuthHelper"
    +    }
    +}
    diff --git a/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt b/auth/src/test/java/org/openedx/auth/presentation/signin/SignInViewModelTest.kt
    index b36aabb..0d22d93 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
    @@ -25,6 +25,7 @@ import org.openedx.auth.R
     import org.openedx.auth.domain.interactor.AuthInteractor
     import org.openedx.auth.presentation.AgreementProvider
     import org.openedx.auth.presentation.AuthAnalytics
    +import org.openedx.auth.presentation.sso.BrowserAuthHelper
     import org.openedx.auth.presentation.AuthRouter
     import org.openedx.auth.presentation.sso.OAuthHelper
     import org.openedx.core.UIMessage
    @@ -61,6 +62,7 @@ class SignInViewModelTest {
         private val oAuthHelper = mockk<OAuthHelper>()
         private val router = mockk<AuthRouter>()
         private val whatsNewGlobalManager = mockk<WhatsNewGlobalManager>()
    +    private val browserAuthHelper = mockk<BrowserAuthHelper>()
    
         private val invalidCredential = "Invalid credentials"
         private val noInternet = "Slow or no internet connection"
    @@ -85,6 +87,8 @@ class SignInViewModelTest {
             every { config.getFacebookConfig() } returns FacebookConfig()
             every { config.getGoogleConfig() } returns GoogleConfig()
             every { config.getMicrosoftConfig() } returns MicrosoftConfig()
    +        every { config.isBrowserLoginEnabled() } returns false
    +        every { config.isBrowserRegistrationEnabled() } returns false
         }
    
         @after
    @@ -110,6 +114,7 @@ class SignInViewModelTest {
                 config = config,
                 router = router,
                 whatsNewGlobalManager = whatsNewGlobalManager,
    +            browserAuthHelper = browserAuthHelper,
                 courseId = "",
                 infoType = "",
             )
    @@ -143,6 +148,7 @@ class SignInViewModelTest {
                 config = config,
                 router = router,
                 whatsNewGlobalManager = whatsNewGlobalManager,
    +            browserAuthHelper = browserAuthHelper,
                 courseId = "",
                 infoType = "",
             )
    @@ -177,6 +183,7 @@ class SignInViewModelTest {
                 config = config,
                 router = router,
                 whatsNewGlobalManager = whatsNewGlobalManager,
    +            browserAuthHelper = browserAuthHelper,
                 courseId = "",
                 infoType = "",
             )
    @@ -210,6 +217,7 @@ class SignInViewModelTest {
                 config = config,
                 router = router,
                 whatsNewGlobalManager = whatsNewGlobalManager,
    +            browserAuthHelper = browserAuthHelper,
                 courseId = "",
                 infoType = "",
             )
    @@ -245,6 +253,7 @@ class SignInViewModelTest {
                 config = config,
                 router = router,
                 whatsNewGlobalManager = whatsNewGlobalManager,
    +            browserAuthHelper = browserAuthHelper,
                 courseId = "",
                 infoType = "",
             )
    @@ -281,6 +290,7 @@ class SignInViewModelTest {
                 config = config,
                 router = router,
                 whatsNewGlobalManager = whatsNewGlobalManager,
    +            browserAuthHelper = browserAuthHelper,
                 courseId = "",
                 infoType = "",
             )
    @@ -319,6 +329,7 @@ class SignInViewModelTest {
                 config = config,
                 router = router,
                 whatsNewGlobalManager = whatsNewGlobalManager,
    +            browserAuthHelper = browserAuthHelper,
                 courseId = "",
                 infoType = "",
             )
    @@ -357,6 +368,7 @@ class SignInViewModelTest {
                 config = config,
                 router = router,
                 whatsNewGlobalManager = whatsNewGlobalManager,
    +            browserAuthHelper = browserAuthHelper,
                 courseId = "",
                 infoType = "",
             )
    diff --git a/core/src/main/java/org/openedx/core/ApiConstants.kt b/core/src/main/java/org/openedx/core/ApiConstants.kt
    index 786d63c..86d678c 100644
    --- a/core/src/main/java/org/openedx/core/ApiConstants.kt
    +++ b/core/src/main/java/org/openedx/core/ApiConstants.kt
    @@ -12,6 +12,7 @@ object ApiConstants {
         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"
    @@ -27,6 +28,7 @@ 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"
    
    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 4b40fbc..186c6d3 100644
    --- a/core/src/main/java/org/openedx/core/config/Config.kt
    +++ b/core/src/main/java/org/openedx/core/config/Config.kt
    @@ -111,6 +111,14 @@ class Config(context: Context) {
             return getBoolean(COURSE_UNIT_PROGRESS_ENABLED, false)
         }
    
    +    fun isBrowserLoginEnabled(): Boolean {
    +        return getBoolean(BROWSER_LOGIN, false)
    +    }
    +
    +    fun isBrowserRegistrationEnabled(): Boolean {
    +        return getBoolean(BROWSER_REGISTRATION, false)
    +    }
    +
         private fun getString(key: String, defaultValue: String): String {
             val element = getObject(key)
             return if (element != null) {
    @@ -162,6 +170,8 @@ class Config(context: Context) {
             private const val GOOGLE = "GOOGLE"
             private const val MICROSOFT = "MICROSOFT"
             private const val PRE_LOGIN_EXPERIENCE_ENABLED = "PRE_LOGIN_EXPERIENCE_ENABLED"
    +        private const val BROWSER_LOGIN = "BROWSER_LOGIN"
    +        private const val BROWSER_REGISTRATION = "BROWSER_REGISTRATION"
             private const val DISCOVERY = "DISCOVERY"
             private const val PROGRAM = "PROGRAM"
             private const val BRANCH = "BRANCH"
    diff --git a/default_config/dev/config.yaml b/default_config/dev/config.yaml
    index e1582bf..9347d27 100644
    --- a/default_config/dev/config.yaml
    +++ b/default_config/dev/config.yaml
    @@ -75,6 +75,10 @@ TOKEN_TYPE: "JWT"
     WHATS_NEW_ENABLED: false
     #feature flag enable Social Login buttons
     SOCIAL_AUTH_ENABLED: false
    +#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
     COURSE_NESTED_LIST_ENABLED: false
     COURSE_UNIT_PROGRESS_ENABLED: false
    diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml
    index f7afc7b..fa71747 100644
    --- a/default_config/prod/config.yaml
    +++ b/default_config/prod/config.yaml
    @@ -75,6 +75,10 @@ TOKEN_TYPE: "JWT"
     WHATS_NEW_ENABLED: false
     #feature flag enable Social Login buttons
     SOCIAL_AUTH_ENABLED: false
    +#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
     COURSE_NESTED_LIST_ENABLED: false
     COURSE_UNIT_PROGRESS_ENABLED: false
    diff --git a/default_config/stage/config.yaml b/default_config/stage/config.yaml
    index f7afc7b..fa71747 100644
    --- a/default_config/stage/config.yaml
    +++ b/default_config/stage/config.yaml
    @@ -75,6 +75,10 @@ TOKEN_TYPE: "JWT"
     WHATS_NEW_ENABLED: false
     #feature flag enable Social Login buttons
     SOCIAL_AUTH_ENABLED: false
    +#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
     COURSE_NESTED_LIST_ENABLED: false
     COURSE_UNIT_PROGRESS_ENABLED: false
    xitij2000 committed Jul 30, 2024
    Configuration menu
    Copy the full SHA
    b0b4165 View commit details
    Browse the repository at this point in the history