-
Notifications
You must be signed in to change notification settings - Fork 47
feat: Add support for browser-based logins #45
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
Changes from all commits
dd3eb56
3bc8dfb
9f8c4f1
a78857b
91df854
c54b897
44ffa16
3915e96
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,29 @@ | ||
| import org.yaml.snakeyaml.Yaml | ||
|
|
||
| buildscript { | ||
|
|
||
| dependencies { | ||
| classpath 'org.yaml:snakeyaml:1.33' | ||
| } | ||
| } | ||
|
|
||
| plugins { | ||
| id 'com.android.application' | ||
| id 'org.jetbrains.kotlin.android' | ||
| id 'kotlin-parcelize' | ||
| id 'kotlin-kapt' | ||
| id 'com.google.gms.google-services' | ||
| id "com.google.firebase.crashlytics" | ||
| } | ||
|
|
||
| def config_file = System.getenv("OPENEDX_ANDROID_CFG_FILE") ?: "config.yaml" | ||
| def config = new Yaml().load(new File(config_file).newInputStream()) | ||
|
|
||
| android { | ||
| compileSdk 34 | ||
|
|
||
| defaultConfig { | ||
| applicationId "org.openedx.app" | ||
| applicationId config?.application_id ?: "org.openedx.app" | ||
| minSdk 24 | ||
| targetSdk 34 | ||
| versionCode 1 | ||
|
|
@@ -30,9 +43,31 @@ android { | |
| } | ||
| develop { | ||
| dimension 'env' | ||
| applicationIdSuffix '.dev' | ||
| } | ||
| stage { | ||
| dimension 'env' | ||
| applicationIdSuffix '.stage' | ||
|
Comment on lines
+46
to
+50
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added suffixes for each environment since AFAIK this is also done for iOS. |
||
| } | ||
| } | ||
| sourceSets { | ||
| prod { | ||
| def envMap = config.environments.find { it.key == "PROD" } | ||
| if (envMap.value.RES_DIR) { | ||
| res.srcDirs = [envMap.value.RES_DIR] | ||
| } | ||
| } | ||
| develop { | ||
| def envMap = config.environments.find { it.key == "DEV" } | ||
| if (envMap.value.RES_DIR) { | ||
| res.srcDirs = [envMap.value.RES_DIR] | ||
| } | ||
| } | ||
| stage { | ||
| def envMap = config.environments.find { it.key == "STAGE" } | ||
| if (envMap.value.RES_DIR) { | ||
| res.srcDirs = [envMap.value.RES_DIR] | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -94,4 +129,12 @@ dependencies { | |
| testImplementation "io.mockk:mockk-android:$mockk_version" | ||
| testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version" | ||
| testImplementation "androidx.arch.core:core-testing:$android_arch_version" | ||
|
|
||
| // Import the Firebase BoM | ||
| implementation(platform("com.google.firebase:firebase-bom:32.2.3")) | ||
|
|
||
| // When using the BoM, you don't specify versions in Firebase library dependencies | ||
|
|
||
| // Add the dependency for the Firebase SDK for Google Analytics | ||
| implementation("com.google.firebase:firebase-analytics") | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -35,6 +35,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> | ||
|
Comment on lines
+38
to
+43
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This will have the app handle the scheme matching the applicationId. i.e. if the application id is "org.openedx.app" then this will handle: com.openedx.app:// |
||
| </activity> | ||
|
|
||
| <provider | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ package org.openedx.app | |
| import android.content.pm.ActivityInfo | ||
| 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 | ||
|
|
@@ -49,6 +50,15 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder, AppDataH | |
|
|
||
| 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 | ||
| } | ||
|
|
||
|
Comment on lines
+53
to
+61
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the application was opened using the auth intent filter, |
||
| override fun onSaveInstanceState(outState: Bundle) { | ||
| outState.putInt(TOP_INSET, topInset) | ||
| outState.putInt(BOTTOM_INSET, bottomInset) | ||
|
|
@@ -103,8 +113,12 @@ class AppActivity : AppCompatActivity(), InsetHolder, WindowSizeHolder, AppDataH | |
| .add(R.id.container, MainFragment()) | ||
| .commit() | ||
| } else { | ||
| val fragment = SignInFragment() | ||
| val bundle = Bundle() | ||
| bundle.putString("auth_code", authCode) | ||
| fragment.arguments = bundle | ||
| supportFragmentManager.beginTransaction() | ||
| .add(R.id.container, SignInFragment()) | ||
| .add(R.id.container, fragment) | ||
|
Comment on lines
+116
to
+121
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Pass the code to the login fragment if available. |
||
| .commit() | ||
| } | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package org.openedx.auth.data.repository | ||
|
|
||
| import org.openedx.auth.data.api.AuthApi | ||
| import org.openedx.auth.data.model.AuthResponse | ||
| import org.openedx.auth.data.model.ValidationFields | ||
| import org.openedx.core.ApiConstants | ||
| import org.openedx.core.data.storage.CorePreferences | ||
|
|
@@ -12,6 +13,16 @@ class AuthRepository( | |
| private val preferencesManager: CorePreferences, | ||
| ) { | ||
|
|
||
| private suspend fun processAuthResponse(authResponse: AuthResponse) { | ||
| if (authResponse.error != null) { | ||
| throw EdxError.UnknownException(authResponse.error!!) | ||
| } | ||
| preferencesManager.accessToken = authResponse.accessToken ?: "" | ||
| preferencesManager.refreshToken = authResponse.refreshToken ?: "" | ||
| val user = api.getProfile().mapToDomain() | ||
| preferencesManager.user = user | ||
| } | ||
|
Comment on lines
+16
to
+24
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Extracted out the common code since both login functions only have a slightly different API call but the same response. |
||
|
|
||
| suspend fun login( | ||
| username: String, | ||
| password: String, | ||
|
|
@@ -22,13 +33,16 @@ class AuthRepository( | |
| username, | ||
| password | ||
| ) | ||
| if (authResponse.error != null) { | ||
| throw EdxError.UnknownException(authResponse.error!!) | ||
| } | ||
| preferencesManager.accessToken = authResponse.accessToken ?: "" | ||
| preferencesManager.refreshToken = authResponse.refreshToken ?: "" | ||
| val user = api.getProfile().mapToDomain() | ||
| preferencesManager.user = user | ||
| processAuthResponse(authResponse) | ||
| } | ||
|
|
||
| suspend fun login(code: String) { | ||
| val authResponse = api.getAccessToken( | ||
| ApiConstants.GRANT_TYPE_CODE, | ||
| org.openedx.core.BuildConfig.CLIENT_ID, | ||
| code, | ||
| ) | ||
| processAuthResponse(authResponse) | ||
| } | ||
|
|
||
| suspend fun getRegistrationFields(): List<RegistrationField> { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,13 @@ | ||
| package org.openedx.auth.presentation.signin | ||
|
|
||
| import android.content.Intent.FLAG_ACTIVITY_NEW_TASK | ||
| import android.content.res.Configuration.UI_MODE_NIGHT_NO | ||
| import android.content.res.Configuration.UI_MODE_NIGHT_YES | ||
| import android.net.Uri | ||
| import android.os.Bundle | ||
| import android.view.LayoutInflater | ||
| import android.view.ViewGroup | ||
| import androidx.browser.customtabs.CustomTabsIntent | ||
| import androidx.compose.foundation.Image | ||
| import androidx.compose.foundation.background | ||
| import androidx.compose.foundation.layout.* | ||
|
|
@@ -42,6 +45,7 @@ import org.openedx.core.ui.theme.appShapes | |
| import org.openedx.core.ui.theme.appTypography | ||
| import org.koin.android.ext.android.inject | ||
| import org.koin.androidx.viewmodel.ext.android.viewModel | ||
| import org.openedx.core.BuildConfig | ||
|
|
||
| class SignInFragment : Fragment() { | ||
|
|
||
|
|
@@ -53,6 +57,10 @@ class SignInFragment : Fragment() { | |
| container: ViewGroup?, | ||
| savedInstanceState: Bundle?, | ||
| ) = ComposeView(requireContext()).apply { | ||
| val authCode = arguments?.getString("auth_code") | ||
| if (authCode is String) { | ||
| viewModel.login(authCode) | ||
| } | ||
|
Comment on lines
+60
to
+63
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this view was passed the auth_code then we can just log in straight away. |
||
| setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) | ||
| setContent { | ||
| OpenEdXTheme { | ||
|
|
@@ -66,6 +74,7 @@ class SignInFragment : Fragment() { | |
| windowSize = windowSize, | ||
| showProgress = showProgress, | ||
| uiMessage = uiMessage, | ||
| browserLogin = BuildConfig.BROWSER_LOGIN, | ||
| onLoginClick = { login, password -> | ||
| viewModel.login(login, password) | ||
| }, | ||
|
|
@@ -76,6 +85,20 @@ class SignInFragment : Fragment() { | |
| onForgotPasswordClick = { | ||
| viewModel.forgotPasswordClickedEvent() | ||
| router.navigateToRestorePassword(parentFragmentManager) | ||
| }, | ||
| onLoginClickOauth = { | ||
| val uri = Uri.parse("${BuildConfig.BASE_URL}oauth2/authorize") | ||
| .buildUpon() | ||
| .appendQueryParameter("client_id", BuildConfig.CLIENT_ID) | ||
| .appendQueryParameter("redirect_uri", "${context.packageName}://oauth2Callback") | ||
| .appendQueryParameter("response_type", "code") | ||
| .build() | ||
| val intent = CustomTabsIntent.Builder() | ||
| .setUrlBarHidingEnabled(true) | ||
| .setShowTitle(true) | ||
| .build() | ||
| intent.intent.setFlags(FLAG_ACTIVITY_NEW_TASK); | ||
| intent.launchUrl(context, uri) | ||
| } | ||
| ) | ||
|
|
||
|
|
@@ -94,7 +117,9 @@ private fun LoginScreen( | |
| windowSize: WindowSize, | ||
| showProgress: Boolean, | ||
| uiMessage: UIMessage?, | ||
| browserLogin: Boolean, | ||
| onLoginClick: (login: String, password: String) -> Unit, | ||
| onLoginClickOauth: () -> Unit, | ||
| onRegisterClick: () -> Unit, | ||
| onForgotPasswordClick: () -> Unit | ||
| ) { | ||
|
|
@@ -187,13 +212,27 @@ private fun LoginScreen( | |
| style = MaterialTheme.appTypography.titleSmall | ||
| ) | ||
| Spacer(modifier = Modifier.height(24.dp)) | ||
| AuthForm( | ||
| buttonWidth, | ||
| showProgress, | ||
| onLoginClick, | ||
| onRegisterClick, | ||
| onForgotPasswordClick | ||
| ) | ||
| if (browserLogin) { | ||
| Column( | ||
| modifier = Modifier.fillMaxSize(), | ||
| verticalArrangement = Arrangement.SpaceAround, | ||
| horizontalAlignment = Alignment.CenterHorizontally, | ||
| ) { | ||
| OpenEdXButton( | ||
| width = buttonWidth, | ||
| text = stringResource(id = R.string.auth_sign_in), | ||
| onClick = onLoginClickOauth, | ||
| ) | ||
| } | ||
| } else { | ||
| AuthForm( | ||
| buttonWidth, | ||
| showProgress, | ||
| onLoginClick, | ||
| onRegisterClick, | ||
| onForgotPasswordClick, | ||
| ) | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -334,9 +373,11 @@ private fun SignInScreenPreview() { | |
| windowSize = WindowSize(WindowType.Compact, WindowType.Compact), | ||
| showProgress = false, | ||
| uiMessage = null, | ||
| browserLogin = true, | ||
| onLoginClick = { _, _ -> | ||
|
|
||
| }, | ||
| onLoginClickOauth = {}, | ||
| onRegisterClick = {}, | ||
| onForgotPasswordClick = {} | ||
| ) | ||
|
|
@@ -353,9 +394,11 @@ private fun SignInScreenTabletPreview() { | |
| windowSize = WindowSize(WindowType.Expanded, WindowType.Expanded), | ||
| showProgress = false, | ||
| uiMessage = null, | ||
| browserLogin = true, | ||
| onLoginClick = { _, _ -> | ||
|
|
||
| }, | ||
| onLoginClickOauth = {}, | ||
| onRegisterClick = {}, | ||
| onForgotPasswordClick = {} | ||
| ) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Allow customising the applicationId via the config file, but fall back to org.openedx.app as before.