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

Conversation

xitij2000
Copy link

@xitij2000 xitij2000 commented Jul 30, 2024

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.

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
@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Jul 30, 2024
@openedx-webhooks
Copy link

openedx-webhooks commented Jul 30, 2024

Thanks for the pull request, @xitij2000!

What's next?

Please work through the following steps to get your changes ready for engineering review:

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.

🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads

🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

🔘 Let us know that your PR is ready for review:

Who will review my changes?

This repository is currently maintained by @openedx/openedx-mobile-maintainers. Tag them in a comment and let them know that your changes are ready for review.

Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

Copy link

@navinkarkera navinkarkera left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@xitij2000 Nice work! 👍

👍

  • I tested this: (tested login in both real hardware as well as emulator)
  • I read through the code
  • I checked for accessibility issues
  • Includes documentation

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
open-source-contribution PR author is not from Axim or 2U
Projects
Status: Ready for Review
Development

Successfully merging this pull request may close these issues.

3 participants