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 #190

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,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
9 changes: 9 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,15 @@ 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
): 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,15 @@ class AuthRepository(
.processAuthResponse()
}

suspend fun browserAuthCodeLogin(code: String) {
api.getAccessTokenFromCode(
grantType = ApiConstants.GRANT_TYPE_CODE,
clientId = config.getOAuthClientId(),
code = code,
redirectUri = "${config.getApplicationID()}://oauth2Callback"
).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
@@ -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,6 +127,7 @@ 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
Expand Down
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