Skip to content

OAuth - request the token via REST API #19

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

Merged
merged 6 commits into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies {
implementation(project(":homeUi"))
implementation(project(":loginUi"))
implementation(project(":analytics"))
implementation(project(":foundations"))

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
Expand All @@ -19,6 +20,7 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.material3)
implementation(libs.androidx.navigation)
implementation(libs.kotlinx.coroutines)
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.koin.android)
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Gravatar"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/gravatar/app/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ val appModule = module {
homeUiModule,
loginUiModule,
analyticsModule,
dispatcherModule,
)
}
22 changes: 22 additions & 0 deletions app/src/main/java/com/gravatar/app/di/DispatcherModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.gravatar.app.di

import com.gravatar.app.foundations.DispatcherProvider
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import org.koin.dsl.module

val dispatcherModule = module {
single<DispatcherProvider> {
AppDispatcherProvider(
main = Dispatchers.Main,
io = Dispatchers.IO,
default = Dispatchers.Default
)
}
}

data class AppDispatcherProvider(
override val main: CoroutineDispatcher,
override val io: CoroutineDispatcher,
override val default: CoroutineDispatcher
) : DispatcherProvider
8 changes: 8 additions & 0 deletions app/src/main/res/xml/network_security_config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<network-security-config>
<debug-overrides>
<trust-anchors>
<!-- Trust user added CAs while debuggable only -->
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ plugins {
alias(libs.plugins.detekt) apply false
alias(libs.plugins.android.library) apply false
alias(libs.plugins.kotlin.serialization) apply false
alias(libs.plugins.kotlin.jvm) apply false
}
1 change: 1 addition & 0 deletions foundations/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
17 changes: 17 additions & 0 deletions foundations/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
plugins {
id("java-library")
alias(libs.plugins.kotlin.jvm)
}
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17

dependencies {
implementation(libs.kotlinx.coroutines)
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.gravatar.app.foundations

import kotlinx.coroutines.CoroutineDispatcher

interface DispatcherProvider {
val main: CoroutineDispatcher
val io: CoroutineDispatcher
val default: CoroutineDispatcher
}
10 changes: 10 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ roborazzi = "1.45.1"
robolectric = "4.14.1"
tracks = "6.0.3"
browser = "1.8.0"
ktor = "3.1.3"
kotlinCoroutines = "1.10.2"

[libraries]
kotlinx-coroutines = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinCoroutines" }
kotlinx-coroutines-test = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-test", version.ref = "kotlinCoroutines" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
Expand All @@ -36,12 +40,17 @@ junit = { group = "junit", name = "junit", version.ref = "junit" }
koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" }
koin-core = { module = "io.insert-koin:koin-core" }
koin-android = { module = "io.insert-koin:koin-android" }
koin-compose = { module = "io.insert-koin:koin-androidx-compose" }
koin-test-junit4 = { module = "io.insert-koin:koin-test-junit4" }
mockk-android = { group = "io.mockk", name = "mockk-android", version.ref = "mockk" }
roborazzi = { group = "io.github.takahirom.roborazzi", name = "roborazzi", version.ref = "roborazzi" }
roborazzi-compose = { group = "io.github.takahirom.roborazzi", name = "roborazzi-compose", version.ref = "roborazzi" }
roborazzi-junit-rule = { group = "io.github.takahirom.roborazzi", name = "roborazzi-junit-rule", version.ref = "roborazzi" }
robolectric = { group = "org.robolectric", name = "robolectric", version.ref = "robolectric" }
ktor-core = { group = "io.ktor", name = "ktor-client-core", version.ref = "ktor" }
ktor-okhttp = { group = "io.ktor", name = "ktor-client-okhttp", version.ref = "ktor" }
ktor-content-negotiation = { group = "io.ktor", name = "ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-json = { group = "io.ktor", name = "ktor-serialization-kotlinx-json", version.ref = "ktor" }

# Dependencies of the included build-logic
android-gradlePlugin = { group = "com.android.tools.build", name = "gradle", version.ref = "androidGradlePlugin" }
Expand All @@ -57,6 +66,7 @@ detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }

# Plugins defined by this project
gravatar-android-application = { id = "gravatar.android.application" }
Expand Down
9 changes: 9 additions & 0 deletions loginUi/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ android {
"OAUTH_REDIRECT_URI",
"\"${properties["oauth.redirectUri"]?.toString() ?: ""}\"",
)
buildConfigField(
"String",
"OAUTH_CLIENT_SECRET",
"\"${properties["oauth.clientSecret"]?.toString() ?: ""}\"",
)
Comment on lines +35 to +39
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Remember to add this to your local.properties

manifestPlaceholders["OAUTH_REDIRECT_URI_HOST"] =
properties["oauth.redirectUri"]?.toString()?.split("://")?.get(1) ?: ""
manifestPlaceholders["OAUTH_REDIRECT_URI_SCHEME"] =
Expand All @@ -41,6 +46,9 @@ android {
}

dependencies {
implementation(project(":foundations"))
implementation(project(":userComponent"))

implementation(libs.androidx.core.ktx)
implementation(libs.androidx.material3)
implementation(libs.androidx.ui)
Expand All @@ -50,6 +58,7 @@ dependencies {
implementation(project.dependencies.platform(libs.koin.bom))
implementation(libs.koin.core)
implementation(libs.koin.android)
implementation(libs.koin.compose)
testImplementation(libs.junit)
testImplementation(project(":testUtils"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ internal val buildConfigModule = module {
single {
OAuthConfig(
clientId = BuildConfig.OAUTH_CLIENT_ID,
redirectUri = BuildConfig.OAUTH_REDIRECT_URI
redirectUri = BuildConfig.OAUTH_REDIRECT_URI,
clientSecret = BuildConfig.OAUTH_CLIENT_SECRET,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.gravatar.app.loginUi.di

import com.gravatar.app.loginUi.presentation.login.LoginViewModel
import com.gravatar.app.usercomponent.di.userComponentModule
import org.koin.core.module.dsl.viewModelOf
import org.koin.dsl.module

val loginUiModule = module {
includes(buildConfigModule)
includes(buildConfigModule, userComponentModule)

viewModelOf(::LoginViewModel)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,38 +7,79 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import com.gravatar.app.loginUi.presentation.oauth.OAuthResult
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LocalLifecycleOwner
import androidx.lifecycle.repeatOnLifecycle
import com.gravatar.app.loginUi.presentation.oauth.OAuthResultContract
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.androidx.compose.koinViewModel

@Composable
fun LoginScreen(
onLoggedIn: () -> Unit,
) {
LoginScreen(
onLoggedIn = onLoggedIn,
viewModel = koinViewModel(),
)
}

@Composable
internal fun LoginScreen(
Copy link
Contributor

Choose a reason for hiding this comment

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

Preview in this class no longer works, as Koin won't be initialized. We could use KoinAppApplicationPreview:

@Preview
@Composable
private fun LoginScreenPreview() {
    KoinApplicationPreview(application = { modules(loginUiModule) }) {
        LoginScreen(
            onLoggedIn = { },
        )
    }
}

However, if you try that code, it will fail because it doesn't know how to resolve dispatcherModule.

dispatcherModule is being defined in the app module, so DispatcherProvider will be available everywhere in the app when needed. However, thinking in isolated modules, appmodule doesn't require :foundations. It's only required by loginUI.

I wonder if dispatcherModule should be defined in foundations and refactor a bit the dependencies.

Something like this:
adam/GRA-199...hamorillo/GRA-199-refactor-dispatcher

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be fixed now. I didn't update the test to use the proper composable that doesn't rely on ViewModel, but rather on a ui state class.

onLoggedIn: () -> Unit,
viewModel: LoginViewModel,
) {
val uiState by viewModel.uiState.collectAsState()
val context = LocalContext.current
val lifecycle = LocalLifecycleOwner.current.lifecycle

val oAuthLauncher = rememberLauncherForActivityResult(OAuthResultContract()) { result ->
when (result) {
OAuthResult.DISMISSED -> Unit
is OAuthResult.TOKEN -> {
Toast.makeText(context, "Logged in successfully!", Toast.LENGTH_SHORT).show()
onLoggedIn()
}
LaunchedEffect(Unit) {
withContext(Dispatchers.Main.immediate) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.actions.collect { action ->
when (action) {
LoginAction.UserLoggedIn -> {
onLoggedIn()
}

OAuthResult.ERROR -> {
Toast.makeText(context, "Login failed. Please try again.", Toast.LENGTH_SHORT)
.show()
LoginAction.ShowError -> {
Toast.makeText(context, "Login error", Toast.LENGTH_SHORT)
.show()
}
}
}
}
}
}

LoginScreen(
uiState = uiState,
onEvent = viewModel::onEvent
)
}

@Composable
internal fun LoginScreen(
uiState: LoginUiState,
onEvent: (LoginEvent) -> Unit,
) {
val oAuthLauncher = rememberLauncherForActivityResult(OAuthResultContract()) { result ->
onEvent(LoginEvent.OAuthResultReceived(result))
}

Scaffold { innerPadding ->
Surface(
modifier = Modifier.padding(innerPadding)
Expand All @@ -47,14 +88,22 @@ fun LoginScreen(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column {
Text("Login Screen")
Button(
onClick = {
oAuthLauncher.launch(Unit)
when {
uiState.isLoading -> {
CircularProgressIndicator()
}

else -> {
Column {
Text("Login Screen")
Button(
onClick = {
oAuthLauncher.launch(Unit)
}
) {
Text("Log In")
}
}
) {
Text("Log In")
}
}
}
Expand Down
Loading