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 Jan 18, 2024
1 parent 5f06478 commit 389f90f
Show file tree
Hide file tree
Showing 18 changed files with 231 additions and 33 deletions.
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,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>
</activity>

<provider
Expand Down
18 changes: 16 additions & 2 deletions app/src/main/java/org/openedx/app/AppActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.openedx.app

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 Down Expand Up @@ -50,6 +51,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)
Expand Down Expand Up @@ -112,10 +121,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)
}
}

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 @@ -20,6 +20,7 @@ import org.openedx.app.room.DATABASE_NAME
import org.openedx.app.system.notifier.AppNotifier
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 @@ -153,4 +154,5 @@ val appModule = module {
factory { FacebookAuthHelper() }
factory { GoogleAuthHelper(get()) }
factory { MicrosoftAuthHelper() }
factory { BrowserAuthHelper(get()) }
}
1 change: 1 addition & 0 deletions app/src/main/java/org/openedx/app/di/ScreenModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ val screenModule = module {
get(),
get(),
get(),
get(),
courseId,
)
}
Expand Down
1 change: 1 addition & 0 deletions auth/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
8 changes: 8 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 @@ -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(
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
@@ -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
Expand Down Expand Up @@ -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()
}
Expand All @@ -60,12 +69,16 @@ class AuthRepository(
}

private suspend fun AuthResponse.processAuthResponse() {
Log.d("MMMMMMMM", error.toString())
if (error != null) {
throw EdxError.UnknownException(error!!)
}
preferencesManager.accessToken = accessToken ?: ""
preferencesManager.refreshToken = refreshToken ?: ""
preferencesManager.accessTokenExpiresAt = getTokenExpiryTime()
Log.d("MMMMMMMM", preferencesManager.accessToken)
Log.d("MMMMMMMM", preferencesManager.refreshToken)
Log.d("MMMMMMMM", preferencesManager.accessTokenExpiresAt.toString())
val user = api.getProfile()
preferencesManager.user = user
}
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
@@ -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
Expand Down Expand Up @@ -41,18 +43,22 @@ import androidx.fragment.app.Fragment
import org.koin.android.ext.android.inject
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
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.R as coreR

class LogistrationFragment : Fragment() {

private val router: AuthRouter by inject()
private val config: Config by inject()

override fun onCreateView(
inflater: LayoutInflater,
Expand All @@ -68,7 +74,15 @@ class LogistrationFragment : Fragment() {
router.navigateToSignIn(parentFragmentManager, courseId)
},
onRegisterClick = {
router.navigateToSignUp(parentFragmentManager, courseId)
if (config.isBrowserRegistrationEnabled()) {
UrlUtils.openInBrowser(
activity = context,
apiHostUrl = config.getApiHostURL(),
url = "/register",
)
} else {
router.navigateToSignUp(parentFragmentManager, courseId)
}
},
onSearchClick = { querySearch ->
router.navigateToDiscoverCourses(parentFragmentManager, querySearch)
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -44,6 +45,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,
Expand All @@ -65,6 +71,10 @@ class SignInFragment : Fragment() {
router.navigateToRestorePassword(parentFragmentManager)
}

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

AuthEvent.RegisterClick -> {
viewModel.signUpClickedEvent()
router.navigateToSignUp(parentFragmentManager, null)
Expand Down Expand Up @@ -117,7 +127,9 @@ internal sealed interface AuthEvent {
object SignInGoogle : AuthEvent
object SignInFacebook : AuthEvent
object SignInMicrosoft : AuthEvent
object SignInBrowser : AuthEvent
object RegisterClick : AuthEvent
object ForgotPasswordClick : AuthEvent
object BackClick : AuthEvent

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ 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 loginFailure: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import org.openedx.auth.R
import org.openedx.auth.data.model.AuthType
import org.openedx.auth.domain.interactor.AuthInteractor
import org.openedx.auth.presentation.AuthAnalytics
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 @@ -42,6 +43,7 @@ class SignInViewModel(
private val facebookAuthHelper: FacebookAuthHelper,
private val googleAuthHelper: GoogleAuthHelper,
private val microsoftAuthHelper: MicrosoftAuthHelper,
private val browserAuthHelper: BrowserAuthHelper,
val config: Config,
val courseId: String?,
) : BaseViewModel() {
Expand All @@ -53,6 +55,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(),
)
Expand Down Expand Up @@ -140,6 +144,17 @@ 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 signInMicrosoft(activityContext: Activity) {
_uiState.update { it.copy(showProgress = true) }
viewModelScope.launch {
Expand All @@ -155,6 +170,26 @@ class SignInViewModel(
}
}

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 signUpClickedEvent() {
analytics.signUpClickedEvent()
}
Expand Down
Loading

0 comments on commit 389f90f

Please sign in to comment.