Skip to content

Commit

Permalink
feat: Support for login and registration via a browser custom tab
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
xitij2000 committed Dec 2, 2024
1 parent b892560 commit 189b3f4
Show file tree
Hide file tree
Showing 27 changed files with 387 additions and 70 deletions.
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,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>
Expand Down
26 changes: 23 additions & 3 deletions app/src/main/java/org/openedx/app/AppActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -24,6 +25,7 @@ import org.openedx.app.databinding.ActivityAppBinding
import org.openedx.app.deeplink.DeepLink
import org.openedx.auth.presentation.logistration.LogistrationFragment
import org.openedx.auth.presentation.signin.SignInFragment
import org.openedx.core.ApiConstants
import org.openedx.core.data.storage.CorePreferences
import org.openedx.core.presentation.global.InsetHolder
import org.openedx.core.presentation.global.WindowSizeHolder
Expand Down Expand Up @@ -64,6 +66,18 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
private var _insetCutout = 0

private var _windowSize = WindowSize(WindowType.Compact, WindowType.Compact)
private val authCode: String?
get() {
val data = intent?.data
if (
data is Uri &&
data.scheme == BuildConfig.APPLICATION_ID &&
data.host == ApiConstants.BrowserLogin.REDIRECT_HOST
) {
return data.getQueryParameter(ApiConstants.BrowserLogin.CODE_QUERY_PARAM)
}
return null
}

private val branchCallback =
BranchUniversalReferralInitListener { branchUniversalObject, _, error ->
Expand Down Expand Up @@ -154,10 +168,12 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
if (savedInstanceState == null) {
when {
corePreferencesManager.user == null -> {
val fragment = if (viewModel.isLogistrationEnabled) {
LogistrationFragment()
val authCode = authCode;

Check warning

Code scanning / detekt

Detects semicolons Warning

Unnecessary semicolon

if (viewModel.isLogistrationEnabled && authCode == null) {
addFragment(LogistrationFragment())
} else {
SignInFragment()
addFragment(SignInFragment.newInstance(null, null, authCode = authCode))
}
addFragment(fragment)
}
Expand Down Expand Up @@ -204,6 +220,10 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder {
super.onNewIntent(intent)
this.intent = intent

if (authCode != null) {
addFragment(SignInFragment.newInstance(null, null, authCode = authCode))
}

val extras = intent?.extras
if (extras?.containsKey(DeepLink.Keys.NOTIFICATION_TYPE.value) == true) {
handlePushNotification(extras)
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/org/openedx/app/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ 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
Expand Down Expand Up @@ -209,6 +210,7 @@ val appModule = module {
factory { FacebookAuthHelper() }
factory { GoogleAuthHelper(get()) }
factory { MicrosoftAuthHelper() }
factory { BrowserAuthHelper(get()) }
factory { OAuthHelper(get(), get(), get()) }

factory { FileUtil(get(), get<ResourceManager>().getString(R.string.app_name)) }
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -100,10 +100,11 @@ val screenModule = module {
get(),
get(),
get(),
get(),
)
}

viewModel { (courseId: String?, infoType: String?) ->
viewModel { (courseId: String?, infoType: String?, authCode: String) ->
SignInViewModel(
get(),
get(),
Expand All @@ -118,8 +119,10 @@ val screenModule = module {
get(),
get(),
get(),
get(),
courseId,
infoType,
authCode,
)
}

Expand Down
1 change: 1 addition & 0 deletions auth/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ android {
dependencies {
implementation project(path: ':core')

implementation 'androidx.browser:browser:1.7.0'
implementation "androidx.credentials:credentials:1.3.0"
implementation "androidx.credentials:credentials-play-services-auth:1.3.0"
implementation "com.facebook.android:facebook-login:16.2.0"
Expand Down
11 changes: 11 additions & 0 deletions auth/src/main/java/org/openedx/auth/data/api/AuthApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ 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(
Expand Down
1 change: 1 addition & 0 deletions auth/src/main/java/org/openedx/auth/data/model/AuthType.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ 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<RegistrationField> {
return api.getRegistrationFields().fields?.map { it.mapToDomain() } ?: emptyList()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ 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
Expand All @@ -50,6 +51,7 @@ 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() {

Expand All @@ -67,10 +69,22 @@ class LogistrationFragment : Fragment() {
OpenEdXTheme {
LogistrationScreen(
onSignInClick = {
viewModel.navigateToSignIn(parentFragmentManager)
if (viewModel.isBrowserLoginEnabled) {
viewModel.signInBrowser(requireActivity())
} else {
viewModel.navigateToSignIn(parentFragmentManager)
}
},
onRegisterClick = {
viewModel.navigateToSignUp(parentFragmentManager)
if (viewModel.isBrowserRegistrationEnabled) {
UrlUtils.openInBrowser(
activity = context,
apiHostUrl = viewModel.apiHostUrl,
url = ApiConstants.URL_REGISTER_BROWSER,
)
} else {
viewModel.navigateToSignUp(parentFragmentManager)
}
},
onSearchClick = { querySearch ->
viewModel.navigateToDiscovery(parentFragmentManager, querySearch)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
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

Expand All @@ -14,10 +19,16 @@ 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()
Expand All @@ -28,6 +39,16 @@ 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ class SignInFragment : Fragment() {
private val viewModel: SignInViewModel by viewModel {
parametersOf(
requireArguments().getString(ARG_COURSE_ID, ""),
requireArguments().getString(ARG_INFO_TYPE, "")
requireArguments().getString(ARG_INFO_TYPE, ""),
requireArguments().getString(ARG_AUTH_CODE, ""),
)
}

Expand All @@ -43,6 +44,9 @@ 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,
Expand All @@ -59,6 +63,10 @@ class SignInFragment : Fragment() {
viewModel.navigateToForgotPassword(parentFragmentManager)
}

AuthEvent.SignInBrowser -> {
viewModel.signInBrowser(requireActivity())
}

AuthEvent.RegisterClick -> {
viewModel.navigateToSignUp(parentFragmentManager)
}
Expand Down Expand Up @@ -92,11 +100,13 @@ class SignInFragment : Fragment() {
companion object {
private const val ARG_COURSE_ID = "courseId"
private const val ARG_INFO_TYPE = "info_type"
fun newInstance(courseId: String?, infoType: String?): SignInFragment {
private const val ARG_AUTH_CODE = "auth_code"
fun newInstance(courseId: String?, infoType: String?, authCode: String? = null): SignInFragment {
val fragment = SignInFragment()
fragment.arguments = bundleOf(
ARG_COURSE_ID to courseId,
ARG_INFO_TYPE to infoType
ARG_INFO_TYPE to infoType,
ARG_AUTH_CODE to authCode,
)
return fragment
}
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ internal data class SignInUIState(
val isGoogleAuthEnabled: Boolean = false,
val isMicrosoftAuthEnabled: Boolean = false,
val isSocialAuthEnabled: Boolean = false,
val isBrowserLoginEnabled: Boolean = false,
val isBrowserRegistrationEnabled: Boolean = false,
val isLogistrationEnabled: Boolean = false,
val isRegistrationEnabled: Boolean = true,
val showProgress: Boolean = false,
val loginSuccess: Boolean = false,
val agreement: RegistrationField? = null,
val loginFailure: Boolean = false,
)
Loading

0 comments on commit 189b3f4

Please sign in to comment.