diff --git a/app-android/build.gradle.kts b/app-android/build.gradle.kts
index c60b6e8e..cc85e515 100644
--- a/app-android/build.gradle.kts
+++ b/app-android/build.gradle.kts
@@ -27,4 +27,5 @@ dependencies {
implementation(libs.io.insert.koin.android)
implementation(libs.com.arkivanov.decompose)
implementation(libs.com.arkivanov.decompose.extensions.compose.jetbrains)
+ implementation(libs.androidx.browser)
}
diff --git a/app-android/src/main/AndroidManifest.xml b/app-android/src/main/AndroidManifest.xml
index 054eec07..41f64d5a 100644
--- a/app-android/src/main/AndroidManifest.xml
+++ b/app-android/src/main/AndroidManifest.xml
@@ -24,7 +24,7 @@
-
+
diff --git a/app-android/src/main/kotlin/social/androiddev/dodo/MainActivity.kt b/app-android/src/main/kotlin/social/androiddev/dodo/MainActivity.kt
index 18048de3..90b5ed1d 100644
--- a/app-android/src/main/kotlin/social/androiddev/dodo/MainActivity.kt
+++ b/app-android/src/main/kotlin/social/androiddev/dodo/MainActivity.kt
@@ -10,20 +10,29 @@
package social.androiddev.dodo
import android.os.Bundle
+import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import com.arkivanov.decompose.defaultComponentContext
import kotlinx.coroutines.Dispatchers
+import org.koin.core.context.loadKoinModules
+import org.koin.core.context.unloadKoinModules
+import org.koin.dsl.module
import social.androiddev.common.theme.DodoTheme
import social.androiddev.root.composables.RootContent
import social.androiddev.root.navigation.DefaultRootComponent
class MainActivity : AppCompatActivity() {
+ private val activityModule = module {
+ factory { this@MainActivity }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ loadKoinModules(activityModule)
// Create the root component before starting Compose
val root = DefaultRootComponent(
@@ -40,4 +49,9 @@ class MainActivity : AppCompatActivity() {
}
}
}
+
+ override fun onDestroy() {
+ super.onDestroy()
+ unloadKoinModules(activityModule)
+ }
}
diff --git a/app-android/src/main/kotlin/social/androiddev/dodo/di/AndroidAppModule.kt b/app-android/src/main/kotlin/social/androiddev/dodo/di/AndroidAppModule.kt
index 558d7e91..0dfa7d32 100644
--- a/app-android/src/main/kotlin/social/androiddev/dodo/di/AndroidAppModule.kt
+++ b/app-android/src/main/kotlin/social/androiddev/dodo/di/AndroidAppModule.kt
@@ -10,9 +10,15 @@
package social.androiddev.dodo.di
import org.koin.dsl.module
+import social.androiddev.common.web.WebAuth
+import social.androiddev.dodo.web.CustomTabWebAuth
/**
* The Dodo Android app Koin module holding koin definitions
* specific to the android app
*/
-val androidModule = module { }
+val androidModule = module {
+ factory {
+ CustomTabWebAuth(activity = get())
+ }
+}
diff --git a/app-android/src/main/kotlin/social/androiddev/dodo/web/CustomTabWebAuth.kt b/app-android/src/main/kotlin/social/androiddev/dodo/web/CustomTabWebAuth.kt
new file mode 100644
index 00000000..8f217ddf
--- /dev/null
+++ b/app-android/src/main/kotlin/social/androiddev/dodo/web/CustomTabWebAuth.kt
@@ -0,0 +1,51 @@
+package social.androiddev.dodo.web
+
+import android.content.Intent
+import android.net.Uri
+import androidx.activity.ComponentActivity
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.browser.customtabs.CustomTabColorSchemeParams
+import androidx.browser.customtabs.CustomTabsIntent
+import androidx.core.util.Consumer
+import androidx.lifecycle.lifecycleScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.launch
+import social.androiddev.common.web.WebAuth
+import social.androiddev.common.web.WebOpenExtras
+
+class CustomTabWebAuth(private val activity: ComponentActivity) : WebAuth {
+
+ private val _state = MutableSharedFlow(replay = 1)
+ override val state: Flow = _state
+
+ override suspend fun start(): String {
+ val onNewIntentListener = Consumer { intent ->
+ activity.lifecycleScope.launch {
+ val code = intent.data?.getQueryParameter("code")
+ if (code != null) {
+ _state.emit(WebAuth.State.Success(code))
+ }
+ val error = intent.data?.getQueryParameter("error")
+ val errorDescription = intent.data?.getQueryParameter("error_description")
+ if (error != null) {
+ _state.emit(WebAuth.State.Error(errorDescription))
+ }
+ }
+ }
+ activity.addOnNewIntentListener(onNewIntentListener)
+ return "dodooauth2redirect://callback"
+ }
+
+ override fun open(uri: String, extras: WebOpenExtras) {
+ CustomTabsIntent.Builder()
+ .setDefaultColorSchemeParams(
+ CustomTabColorSchemeParams.Builder()
+ .setToolbarColor(extras.primaryColor)
+ .setSecondaryToolbarColor(extras.secondaryColor)
+ .build()
+ )
+ .build()
+ .launchUrl(activity, Uri.parse(uri))
+ }
+}
\ No newline at end of file
diff --git a/app-desktop/build.gradle.kts b/app-desktop/build.gradle.kts
index 8ff7ffd9..b18a3db5 100644
--- a/app-desktop/build.gradle.kts
+++ b/app-desktop/build.gradle.kts
@@ -23,6 +23,8 @@ kotlin {
implementation(libs.io.insert.koin.core)
implementation(libs.kotlinx.coroutines.javafx)
implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.io.ktor.server.core)
+ implementation(libs.io.ktor.server.netty)
}
}
}
diff --git a/app-desktop/src/jvmMain/kotlin/social/androiddev/desktop/Main.kt b/app-desktop/src/jvmMain/kotlin/social/androiddev/desktop/Main.kt
index a077ff90..28218b65 100644
--- a/app-desktop/src/jvmMain/kotlin/social/androiddev/desktop/Main.kt
+++ b/app-desktop/src/jvmMain/kotlin/social/androiddev/desktop/Main.kt
@@ -22,6 +22,7 @@ import kotlinx.coroutines.Dispatchers
import org.koin.core.context.startKoin
import social.androiddev.common.di.appModule
import social.androiddev.common.theme.DodoTheme
+import social.androiddev.desktop.di.desktopModule
import social.androiddev.root.composables.RootContent
import social.androiddev.root.navigation.DefaultRootComponent
@@ -29,7 +30,7 @@ import social.androiddev.root.navigation.DefaultRootComponent
fun main() {
startKoin {
- modules(appModule())
+ modules(appModule() + desktopModule)
}
val lifecycle = LifecycleRegistry()
diff --git a/app-desktop/src/jvmMain/kotlin/social/androiddev/desktop/di/DesktopAppModule.kt b/app-desktop/src/jvmMain/kotlin/social/androiddev/desktop/di/DesktopAppModule.kt
new file mode 100644
index 00000000..2a5c12fc
--- /dev/null
+++ b/app-desktop/src/jvmMain/kotlin/social/androiddev/desktop/di/DesktopAppModule.kt
@@ -0,0 +1,9 @@
+package social.androiddev.desktop.di
+
+import org.koin.dsl.module
+import social.androiddev.common.web.WebAuth
+import social.androiddev.desktop.web.ExternalBrowserWebAuth
+
+val desktopModule = module {
+ factory { ExternalBrowserWebAuth() }
+}
\ No newline at end of file
diff --git a/app-desktop/src/jvmMain/kotlin/social/androiddev/desktop/web/ExternalBrowserWebAuth.kt b/app-desktop/src/jvmMain/kotlin/social/androiddev/desktop/web/ExternalBrowserWebAuth.kt
new file mode 100644
index 00000000..481683c8
--- /dev/null
+++ b/app-desktop/src/jvmMain/kotlin/social/androiddev/desktop/web/ExternalBrowserWebAuth.kt
@@ -0,0 +1,81 @@
+package social.androiddev.desktop.web
+
+import androidx.compose.ui.res.painterResource
+import io.ktor.server.application.call
+import io.ktor.server.response.respond
+import io.ktor.server.routing.get
+import io.ktor.server.routing.routing
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import social.androiddev.common.web.WebAuth
+import social.androiddev.common.web.WebOpenExtras
+
+private val Browsers = arrayOf(
+ "xdg-open",
+ "google-chrome",
+ "firefox",
+ "opera",
+ "konqueror",
+ "mozilla"
+)
+
+class ExternalBrowserWebAuth : WebAuth {
+
+ private val _state = MutableSharedFlow(replay = 1)
+ override val state: Flow = _state
+
+ override suspend fun start(): String {
+ val server = io.ktor.server.engine.embeddedServer(io.ktor.server.netty.Netty, port = 0) {
+ routing {
+ get("/callback") {
+ val code = call.request.queryParameters["code"]
+ if (code != null) {
+ _state.emit(WebAuth.State.Success(code))
+ this@embeddedServer.dispose()
+ call.respond("Success! You may close the tab")
+ }
+ val error = call.request.queryParameters["error"]
+ val errorDescription = call.request.queryParameters["error_description"]
+ if (error != null) {
+ _state.emit(WebAuth.State.Error(errorDescription))
+ call.respond("$error: $errorDescription")
+ }
+ }
+ }
+ }
+ server.start()
+ val port = server.resolvedConnectors().first().port
+ return "http://localhost:${port}/callback"
+ }
+
+ override fun open(uri: String, extras: WebOpenExtras) {
+ val osName = System.getProperty("os.name")
+ try {
+ if (osName.startsWith("Mac OS")) {
+ Runtime.getRuntime().exec(
+ "open $uri"
+ )
+ } else if (osName.startsWith("Windows")) {
+ Runtime.getRuntime().exec(
+ "rundll32 url.dll,FileProtocolHandler $uri"
+ )
+ } else { //assume Unix or Linux
+ var browser: String? = null
+ for (b in Browsers) {
+ if (browser == null && Runtime.getRuntime()
+ .exec(arrayOf("which", b)).inputStream.read() != -1
+ ) {
+ Runtime.getRuntime().exec(arrayOf(b.also { browser = it }, uri))
+ }
+ }
+ if (browser == null) {
+ throw Exception("No web browser found")
+ }
+ }
+ } catch (e: Exception) {
+ // should not happen
+ // dump stack for debug purpose
+ e.printStackTrace()
+ }
+ }
+}
\ No newline at end of file
diff --git a/build-logic/convention-plugins/src/main/kotlin/social.androiddev.android.common.gradle.kts b/build-logic/convention-plugins/src/main/kotlin/social.androiddev.android.common.gradle.kts
index 306df756..d633d9bc 100644
--- a/build-logic/convention-plugins/src/main/kotlin/social.androiddev.android.common.gradle.kts
+++ b/build-logic/convention-plugins/src/main/kotlin/social.androiddev.android.common.gradle.kts
@@ -10,7 +10,7 @@ android {
compileSdk = 33
defaultConfig {
- minSdk = 33
+ minSdk = 23
}
// targetSdk is in a different interface for library and application projects
diff --git a/data/repository/src/commonMain/kotlin/social/androiddev/common/repository/AuthenticationRepositoryImpl.kt b/data/repository/src/commonMain/kotlin/social/androiddev/common/repository/AuthenticationRepositoryImpl.kt
index 0e0ca4fb..12fd660e 100644
--- a/data/repository/src/commonMain/kotlin/social/androiddev/common/repository/AuthenticationRepositoryImpl.kt
+++ b/data/repository/src/commonMain/kotlin/social/androiddev/common/repository/AuthenticationRepositoryImpl.kt
@@ -108,5 +108,5 @@ internal class AuthenticationRepositoryImpl(
}
}
- override val selectedServer: String? = settings.currentDomain
+ override val selectedServer: String? get() = settings.currentDomain
}
diff --git a/di/build.gradle.kts b/di/build.gradle.kts
index bad71d46..acc3ac73 100644
--- a/di/build.gradle.kts
+++ b/di/build.gradle.kts
@@ -15,6 +15,7 @@ kotlin {
implementation(projects.data.repository)
implementation(projects.domain.authentication)
implementation(libs.io.insert.koin.core)
+ implementation(libs.kotlinx.coroutines.core)
}
}
}
diff --git a/di/src/androidMain/kotlin/social/androiddev/common/web/WebOpenExtras.kt b/di/src/androidMain/kotlin/social/androiddev/common/web/WebOpenExtras.kt
new file mode 100644
index 00000000..27909452
--- /dev/null
+++ b/di/src/androidMain/kotlin/social/androiddev/common/web/WebOpenExtras.kt
@@ -0,0 +1,3 @@
+package social.androiddev.common.web
+
+actual class WebOpenExtras(val primaryColor: Int, val secondaryColor: Int)
\ No newline at end of file
diff --git a/di/src/commonMain/kotlin/social/androiddev/common/web/WebAuth.kt b/di/src/commonMain/kotlin/social/androiddev/common/web/WebAuth.kt
new file mode 100644
index 00000000..3f5a2991
--- /dev/null
+++ b/di/src/commonMain/kotlin/social/androiddev/common/web/WebAuth.kt
@@ -0,0 +1,18 @@
+package social.androiddev.common.web
+
+import kotlinx.coroutines.flow.Flow
+
+interface WebAuth {
+
+ val state: Flow
+
+ suspend fun start(): String
+
+ fun open(uri: String, extras: WebOpenExtras)
+
+ sealed interface State {
+ data class Success(val code: String) : State
+
+ data class Error(val error: String? = null) : State
+ }
+}
\ No newline at end of file
diff --git a/di/src/commonMain/kotlin/social/androiddev/common/web/WebOpenExtras.kt b/di/src/commonMain/kotlin/social/androiddev/common/web/WebOpenExtras.kt
new file mode 100644
index 00000000..5a308db3
--- /dev/null
+++ b/di/src/commonMain/kotlin/social/androiddev/common/web/WebOpenExtras.kt
@@ -0,0 +1,3 @@
+package social.androiddev.common.web
+
+expect class WebOpenExtras
\ No newline at end of file
diff --git a/di/src/desktopMain/kotlin/social/androiddev/common/web/WebOpenExtras.kt b/di/src/desktopMain/kotlin/social/androiddev/common/web/WebOpenExtras.kt
new file mode 100644
index 00000000..eb1eac5c
--- /dev/null
+++ b/di/src/desktopMain/kotlin/social/androiddev/common/web/WebOpenExtras.kt
@@ -0,0 +1,3 @@
+package social.androiddev.common.web
+
+actual class WebOpenExtras
\ No newline at end of file
diff --git a/domain/authentication/build.gradle.kts b/domain/authentication/build.gradle.kts
index 79e02dbe..7d9c4d1d 100644
--- a/domain/authentication/build.gradle.kts
+++ b/domain/authentication/build.gradle.kts
@@ -22,5 +22,12 @@ kotlin {
implementation(libs.org.jetbrains.kotlin.test.annotations.common)
}
}
+
+ val desktopMain by getting {
+ dependencies {
+ implementation(libs.io.ktor.server.core)
+ implementation(libs.io.ktor.server.netty)
+ }
+ }
}
}
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 5f38b54e..5d325bf6 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -47,11 +47,14 @@ io-ktor-client-mock-jvm = { module = "io.ktor:ktor-client-mock-jvm", version.ref
io-ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "io-ktor" }
io-ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "io-ktor" }
io-ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "io-ktor" }
+io-ktor-server-core = { module = "io.ktor:ktor-server-core", version.ref = "io-ktor" }
+io-ktor-server-netty = { module = "io.ktor:ktor-server-netty", version.ref = "io-ktor" }
io-insert-koin-core = { module = "io.insert-koin:koin-core", version.ref = "io-insert-koin" }
io-insert-koin-test = { module = "io.insert-koin:koin-test", version.ref = "io-insert-koin" }
io-insert-koin-android = { module = "io.insert-koin:koin-android", version.ref = "io-insert-koin" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "org-jetbrains-kotlinx-coroutines" }
+kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "org-jetbrains-kotlinx-coroutines" }
kotlinx-coroutines-javafx = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-javafx", version.ref = "org-jetbrains-kotlinx-coroutines" }
org-jetbrains-kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "org-jetbrains-kotlinx-coroutines" }
org-jetbrains-kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "org-jetbrains-kotlinx-serialization" }
diff --git a/ui/signed-out/build.gradle.kts b/ui/signed-out/build.gradle.kts
index efe1c11c..12d32e12 100644
--- a/ui/signed-out/build.gradle.kts
+++ b/ui/signed-out/build.gradle.kts
@@ -12,6 +12,7 @@ kotlin {
val commonMain by getting {
dependencies {
implementation(projects.ui.common)
+ implementation(projects.di)
implementation(projects.domain.authentication)
implementation(libs.io.insert.koin.core)
}
diff --git a/ui/signed-out/src/androidMain/kotlin/social/androiddev/signedout/selectserver/redirectScheme.kt b/ui/signed-out/src/androidMain/kotlin/social/androiddev/signedout/selectserver/redirectScheme.kt
deleted file mode 100644
index 91941043..00000000
--- a/ui/signed-out/src/androidMain/kotlin/social/androiddev/signedout/selectserver/redirectScheme.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-/*
- * This file is part of Dodo.
- *
- * Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with Dodo. If not, see .
- */
-package social.androiddev.signedout.selectserver
-
-/**
- * Note: this _must_ match the value in the manifest for deep linking back to the app to work
- * correctly.
- */
-actual val redirectScheme: String get() = "dodooauth2redirect"
diff --git a/ui/signed-out/src/androidMain/kotlin/social/androiddev/signedout/selectserver/webOpenExtras.kt b/ui/signed-out/src/androidMain/kotlin/social/androiddev/signedout/selectserver/webOpenExtras.kt
new file mode 100644
index 00000000..3acc4484
--- /dev/null
+++ b/ui/signed-out/src/androidMain/kotlin/social/androiddev/signedout/selectserver/webOpenExtras.kt
@@ -0,0 +1,14 @@
+package social.androiddev.signedout.selectserver
+
+import androidx.compose.material.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.graphics.toArgb
+import social.androiddev.common.web.WebOpenExtras
+
+@Composable
+actual fun webOpenExtras(): WebOpenExtras {
+ return WebOpenExtras(
+ primaryColor = MaterialTheme.colors.primary.toArgb(),
+ secondaryColor = MaterialTheme.colors.secondary.toArgb(),
+ )
+}
\ No newline at end of file
diff --git a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/root/DefaultSignedOutRootComponent.kt b/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/root/DefaultSignedOutRootComponent.kt
index 486408d0..8b975f3b 100644
--- a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/root/DefaultSignedOutRootComponent.kt
+++ b/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/root/DefaultSignedOutRootComponent.kt
@@ -15,6 +15,7 @@ import com.arkivanov.decompose.router.stack.StackNavigation
import com.arkivanov.decompose.router.stack.childStack
import com.arkivanov.decompose.router.stack.pop
import com.arkivanov.decompose.router.stack.push
+import com.arkivanov.decompose.router.stack.replaceAll
import com.arkivanov.decompose.value.Value
import com.arkivanov.essenty.parcelable.Parcelable
import com.arkivanov.essenty.parcelable.Parcelize
@@ -78,9 +79,7 @@ class DefaultSignedOutRootComponent(
) = DefaultSelectServerComponent(
componentContext = componentContext,
mainContext = mainContext,
- launchOAuth = {
- navigation.push(Config.SignIn)
- }
+ onAuthenticated = navigateToTimeLine,
)
private fun createSignInComponent(
diff --git a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/DefaultSelectServerComponent.kt b/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/DefaultSelectServerComponent.kt
index 2105b7cf..a14d8da5 100644
--- a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/DefaultSelectServerComponent.kt
+++ b/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/DefaultSelectServerComponent.kt
@@ -17,21 +17,33 @@ import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import social.androiddev.common.decompose.coroutineScope
+import social.androiddev.common.web.WebAuth
+import social.androiddev.common.web.WebOpenExtras
+import social.androiddev.domain.authentication.model.ApplicationOAuthToken
import social.androiddev.domain.authentication.usecase.AuthenticateClient
+import social.androiddev.domain.authentication.usecase.CreateAccessToken
+import social.androiddev.domain.authentication.usecase.GetSelectedApplicationOAuthToken
+import social.androiddev.signedout.util.encode
import kotlin.coroutines.CoroutineContext
class DefaultSelectServerComponent(
private val componentContext: ComponentContext,
mainContext: CoroutineContext,
- private val launchOAuth: () -> Unit,
+ onAuthenticated: () -> Unit,
) : KoinComponent, SelectServerComponent, ComponentContext by componentContext {
private val authenticateClient: AuthenticateClient by inject()
+ private val getSelectedApplicationOAuthToken: GetSelectedApplicationOAuthToken by inject()
+ private val createAccessToken: CreateAccessToken by inject()
+ private val webAuth: WebAuth by inject()
private val viewModel = instanceKeeper.getOrCreate {
SelectServerViewModel(
mainContext = mainContext,
authenticateClient = authenticateClient,
+ getSelectedApplicationOAuthToken = getSelectedApplicationOAuthToken,
+ createAccessToken = createAccessToken,
+ webAuth = webAuth,
)
}
@@ -40,12 +52,24 @@ class DefaultSelectServerComponent(
override val state: StateFlow = viewModel.state
- override fun onServerSelected(server: String) {
+ init {
scope.launch {
- val success = viewModel.validateServer(server)
- if (success) {
- launchOAuth()
+ viewModel.authenticated.collect { authenticated ->
+ if (authenticated) {
+ onAuthenticated()
+ }
}
}
}
+
+ override fun onServerSelected(server: String, extras: WebOpenExtras) {
+ scope.launch {
+ viewModel.validateAndOpenServerAuth(server, extras)
+ }
+ }
+
+ override fun onAuthCanceled() {
+ viewModel.cancelServerAuth()
+ }
}
+
diff --git a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/RedirectScheme.kt b/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/RedirectScheme.kt
deleted file mode 100644
index b64ac44d..00000000
--- a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/RedirectScheme.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * This file is part of Dodo.
- *
- * Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with Dodo. If not, see .
- */
-package social.androiddev.signedout.selectserver
-
-expect val redirectScheme: String
diff --git a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerComponent.kt b/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerComponent.kt
index 5789585f..e471c0a1 100644
--- a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerComponent.kt
+++ b/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerComponent.kt
@@ -10,6 +10,7 @@
package social.androiddev.signedout.selectserver
import kotlinx.coroutines.flow.StateFlow
+import social.androiddev.common.web.WebOpenExtras
/**
* The base component describing all business logic needed for the select server screen
@@ -18,9 +19,12 @@ interface SelectServerComponent {
val state: StateFlow
- fun onServerSelected(server: String)
+ fun onServerSelected(server: String, extras: WebOpenExtras)
+ fun onAuthCanceled()
data class State(
- val selectButtonEnabled: Boolean = true,
+ val server: String = "",
+ val loading: Boolean = false,
+ val error: String? = null,
)
}
diff --git a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerContent.kt b/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerContent.kt
index 67849f19..4e425877 100644
--- a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerContent.kt
+++ b/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerContent.kt
@@ -9,11 +9,23 @@
*/
package social.androiddev.signedout.selectserver
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -26,6 +38,7 @@ import androidx.compose.ui.unit.dp
import social.androiddev.common.composables.buttons.DodoButton
import social.androiddev.common.composables.text.DodoTextField
import social.androiddev.common.theme.DodoTheme
+import social.androiddev.common.web.WebOpenExtras
/**
* Select Server view that delegates business/navigation logic to [SelectServerComponent]
@@ -37,19 +50,26 @@ fun SelectServerContent(
component: SelectServerComponent,
) {
val state by component.state.collectAsState()
+ val extras = webOpenExtras()
SelectServerContent(
modifier = modifier,
- btnEnabled = state.selectButtonEnabled,
- onServerSelected = component::onServerSelected,
+ loading = state.loading,
+ error = state.error,
+ onServerSelected = {
+ component.onServerSelected(it, extras)
+ },
+ onCancel = component::onAuthCanceled
)
}
@Composable
fun SelectServerContent(
modifier: Modifier,
- btnEnabled: Boolean,
+ loading: Boolean,
+ error: String?,
onServerSelected: (String) -> Unit,
+ onCancel: () -> Unit,
) {
var server by rememberSaveable { mutableStateOf("androiddev.social") }
@@ -59,6 +79,10 @@ fun SelectServerContent(
horizontalAlignment = Alignment.CenterHorizontally,
) {
+ AnimatedVisibility(error != null) {
+ ErrorBox(error.orEmpty())
+ }
+
DodoTextField(
value = server,
onValueChange = { v -> server = v },
@@ -66,22 +90,60 @@ fun SelectServerContent(
Spacer(Modifier.height(24.dp))
- DodoButton(
- text = "Select",
- enabled = btnEnabled,
- onClick = { onServerSelected(server) },
+ if (loading) {
+ Row {
+ CircularProgressIndicator()
+ DodoButton(text = "Cancel", onClick = onCancel)
+ }
+ } else {
+ DodoButton(
+ text = "Select",
+ onClick = { onServerSelected(server) },
+ )
+ }
+
+ }
+}
+
+@Composable
+fun ErrorBox(message: String) {
+ Box(
+ modifier = Modifier.fillMaxWidth()
+ .wrapContentHeight()
+ .padding(16.dp)
+ .background(color = MaterialTheme.colors.error.copy(alpha = 0.5F))
+ .border(
+ border = BorderStroke(
+ width = 2.dp,
+ color =
+ MaterialTheme.colors.error
+ ),
+ shape = MaterialTheme.shapes.small
+ )
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ message,
+ color = MaterialTheme.colors.onError,
+ style = MaterialTheme.typography.subtitle2
)
}
}
+@Composable
+expect fun webOpenExtras(): WebOpenExtras
+
// @Preview
@Composable
private fun PreviewLandingContent() {
DodoTheme(true) {
SelectServerContent(
modifier = Modifier.fillMaxSize(),
- btnEnabled = true,
+ loading = false,
+ error = null,
onServerSelected = {},
+ onCancel = {},
)
}
-}
+}
\ No newline at end of file
diff --git a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerViewModel.kt b/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerViewModel.kt
index 90bf9804..931ea4a5 100644
--- a/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerViewModel.kt
+++ b/ui/signed-out/src/commonMain/kotlin/social/androiddev/signedout/selectserver/SelectServerViewModel.kt
@@ -12,17 +12,28 @@ package social.androiddev.signedout.selectserver
import com.arkivanov.essenty.instancekeeper.InstanceKeeper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.launch
+import social.androiddev.common.web.WebAuth
+import social.androiddev.common.web.WebOpenExtras
+import social.androiddev.domain.authentication.model.ApplicationOAuthToken
import social.androiddev.domain.authentication.usecase.AuthenticateClient
+import social.androiddev.domain.authentication.usecase.CreateAccessToken
+import social.androiddev.domain.authentication.usecase.GetSelectedApplicationOAuthToken
+import social.androiddev.signedout.util.encode
import kotlin.coroutines.CoroutineContext
internal class SelectServerViewModel(
mainContext: CoroutineContext,
- private val authenticateClient: AuthenticateClient
+ private val authenticateClient: AuthenticateClient,
+ private val getSelectedApplicationOAuthToken: GetSelectedApplicationOAuthToken,
+ private val createAccessToken: CreateAccessToken,
+ private val webAuth: WebAuth,
) : InstanceKeeper.Instance {
// The scope survives Android configuration changes
@@ -31,21 +42,76 @@ internal class SelectServerViewModel(
private val _state = MutableStateFlow(SelectServerComponent.State())
val state: StateFlow = _state.asStateFlow()
- suspend fun validateServer(server: String): Boolean {
+ private val _authenticated = MutableStateFlow(false)
+ val authenticated: StateFlow = _authenticated
+ private val redirectUri = scope.async { webAuth.start() }
+
+ init {
+ scope.launch {
+ webAuth.state.collect { state ->
+ when (state) {
+ is WebAuth.State.Success -> {
+ val success = createAccessToken(
+ authCode = state.code,
+ server = _state.value.server
+ )
+ if (success) {
+ _authenticated.value = true
+ } else {
+ _state.update {
+ it.copy(
+ loading = false,
+ error = "An error occurred."
+ )
+ }
+ }
+ }
+
+ is WebAuth.State.Error -> {
+ _state.update {
+ it.copy(
+ loading = false,
+ error = state.error ?: "Something is Wrong!"
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+
+ suspend fun validateAndOpenServerAuth(server: String, extras: WebOpenExtras) {
// TODO Sanitize and format the user entered server
- _state.update { it.copy(selectButtonEnabled = false) }
+ _state.update {
+ it.copy(
+ server = server,
+ loading = true,
+ error = null
+ )
+ }
val success = authenticateClient(
domain = server,
clientName = "Dodo",
- redirectURIs = "$redirectScheme://$server/",
+ redirectURIs = redirectUri.await(),
scopes = OAUTH_SCOPES,
website = "https://$server",
)
- _state.update { it.copy(selectButtonEnabled = true) }
- return success
+
+ if (success) {
+ val token = getSelectedApplicationOAuthToken()
+ webAuth.open(createOAuthAuthorizeUrl(token), extras)
+ } else {
+ _state.update { it.copy(loading = false) }
+ }
+ }
+
+ fun cancelServerAuth() {
+ _state.update {
+ it.copy(loading = false)
+ }
}
override fun onDestroy() {
@@ -56,3 +122,13 @@ internal class SelectServerViewModel(
private const val OAUTH_SCOPES = "read write follow push"
}
}
+
+private fun createOAuthAuthorizeUrl(token: ApplicationOAuthToken): String {
+ return buildString {
+ append("https://${token.server}")
+ append("/oauth/authorize?client_id=${token.clientId}")
+ append("&scope=${"read write follow push".encode()}")
+ append("&redirect_uri=${token.redirectUri.encode()}")
+ append("&response_type=code")
+ }
+}
diff --git a/ui/signed-out/src/desktopMain/kotlin/social/androiddev/signedout/selectserver/redirectScheme.kt b/ui/signed-out/src/desktopMain/kotlin/social/androiddev/signedout/selectserver/redirectScheme.kt
deleted file mode 100644
index 2219590d..00000000
--- a/ui/signed-out/src/desktopMain/kotlin/social/androiddev/signedout/selectserver/redirectScheme.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * This file is part of Dodo.
- *
- * Dodo is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
- *
- * Dodo is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License along with Dodo. If not, see .
- */
-package social.androiddev.signedout.selectserver
-
-actual val redirectScheme: String get() = "https"
diff --git a/ui/signed-out/src/desktopMain/kotlin/social/androiddev/signedout/selectserver/webOpenExtras.kt b/ui/signed-out/src/desktopMain/kotlin/social/androiddev/signedout/selectserver/webOpenExtras.kt
new file mode 100644
index 00000000..aa85f290
--- /dev/null
+++ b/ui/signed-out/src/desktopMain/kotlin/social/androiddev/signedout/selectserver/webOpenExtras.kt
@@ -0,0 +1,9 @@
+package social.androiddev.signedout.selectserver
+
+import androidx.compose.runtime.Composable
+import social.androiddev.common.web.WebOpenExtras
+
+@Composable
+actual fun webOpenExtras(): WebOpenExtras {
+ return WebOpenExtras()
+}
\ No newline at end of file