Skip to content

Commit 09a9f45

Browse files
authored
Merge pull request #1154 from DimensionDev/feature/bluesky_oauth
add oauth login support for bluesky
2 parents f9f9c83 + 6e25e26 commit 09a9f45

File tree

17 files changed

+834
-197
lines changed

17 files changed

+834
-197
lines changed

app/src/main/java/dev/dimension/flare/ui/screen/serviceselect/ServiceSelectScreen.kt

Lines changed: 121 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,12 @@ import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridItemSpan
1717
import androidx.compose.foundation.text.input.TextFieldLineLimits
1818
import androidx.compose.foundation.text.input.rememberTextFieldState
1919
import androidx.compose.material3.Button
20-
import androidx.compose.material3.CircularProgressIndicator
20+
import androidx.compose.material3.ButtonGroup
21+
import androidx.compose.material3.CircularWavyProgressIndicator
2122
import androidx.compose.material3.ExperimentalMaterial3Api
23+
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
2224
import androidx.compose.material3.IconButton
25+
import androidx.compose.material3.LinearWavyProgressIndicator
2326
import androidx.compose.material3.MaterialTheme
2427
import androidx.compose.material3.OutlinedSecureTextField
2528
import androidx.compose.material3.OutlinedTextField
@@ -29,7 +32,9 @@ import androidx.compose.runtime.Composable
2932
import androidx.compose.runtime.LaunchedEffect
3033
import androidx.compose.runtime.derivedStateOf
3134
import androidx.compose.runtime.getValue
35+
import androidx.compose.runtime.mutableStateOf
3236
import androidx.compose.runtime.remember
37+
import androidx.compose.runtime.setValue
3338
import androidx.compose.runtime.snapshotFlow
3439
import androidx.compose.ui.Alignment
3540
import androidx.compose.ui.Modifier
@@ -73,7 +78,7 @@ import kotlinx.coroutines.FlowPreview
7378
import kotlinx.coroutines.flow.distinctUntilChanged
7479
import moe.tlaster.precompose.molecule.producePresenter
7580

76-
@OptIn(ExperimentalMaterial3Api::class)
81+
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
7782
@Composable
7883
internal fun ServiceSelectScreen(
7984
onXQT: () -> Unit,
@@ -178,7 +183,7 @@ internal fun ServiceSelectScreen(
178183
contentDescription = null,
179184
)
180185
}.onLoading {
181-
CircularProgressIndicator(
186+
CircularWavyProgressIndicator(
182187
modifier = Modifier.size(24.dp),
183188
)
184189
}
@@ -194,52 +199,117 @@ internal fun ServiceSelectScreen(
194199
when (state.detectedPlatformType.takeSuccess()) {
195200
null -> Unit
196201
PlatformType.Bluesky -> {
202+
val oauthString = stringResource(id = R.string.bluesky_login_oauth_button)
203+
val passwordString =
204+
stringResource(id = R.string.bluesky_login_use_password_button)
205+
ButtonGroup(
206+
overflowIndicator = {},
207+
) {
208+
toggleableItem(
209+
!state.blueskyInputState.usePasswordLogin,
210+
onCheckedChange = {
211+
state.blueskyLoginState.clear()
212+
state.blueskyOauthLoginState.clear()
213+
state.blueskyInputState.setUsePasswordLogin(!it)
214+
},
215+
label = oauthString,
216+
)
217+
toggleableItem(
218+
state.blueskyInputState.usePasswordLogin,
219+
onCheckedChange = {
220+
state.blueskyLoginState.clear()
221+
state.blueskyOauthLoginState.clear()
222+
state.blueskyInputState.setUsePasswordLogin(it)
223+
},
224+
label = passwordString,
225+
)
226+
}
197227
OutlinedTextField(
198228
state = state.blueskyInputState.username,
199229
label = {
200230
Text(text = stringResource(id = R.string.bluesky_login_username_hint))
201231
},
202-
enabled = !state.blueskyLoginState.loading,
232+
enabled =
233+
!state.blueskyOauthLoginState.loading &&
234+
!state.blueskyLoginState.loading,
203235
modifier =
204236
Modifier
205237
.width(300.dp),
206238
lineLimits = TextFieldLineLimits.SingleLine,
207239
)
208-
OutlinedSecureTextField(
209-
state = state.blueskyInputState.password,
210-
label = {
211-
Text(text = stringResource(id = R.string.bluesky_login_password_hint))
212-
},
213-
enabled = !state.blueskyLoginState.loading,
214-
modifier =
215-
Modifier
216-
.width(300.dp),
217-
// lineLimits = TextFieldLineLimits.SingleLine,
218-
onKeyboardAction = {
219-
state.blueskyLoginState.login(
220-
"https://${state.instanceInputState.text}",
221-
state.blueskyInputState.username.text
222-
.toString(),
223-
state.blueskyInputState.password.text
224-
.toString(),
225-
)
226-
},
227-
)
240+
AnimatedVisibility(state.blueskyInputState.usePasswordLogin) {
241+
OutlinedSecureTextField(
242+
state = state.blueskyInputState.password,
243+
label = {
244+
Text(text = stringResource(id = R.string.bluesky_login_password_hint))
245+
},
246+
enabled = !state.blueskyLoginState.loading,
247+
modifier =
248+
Modifier
249+
.width(300.dp),
250+
)
251+
}
252+
AnimatedVisibility(state.blueskyLoginState.require2FA && state.blueskyInputState.usePasswordLogin) {
253+
OutlinedTextField(
254+
state = state.blueskyInputState.authFactorToken,
255+
label = {
256+
Text(text = stringResource(id = R.string.bluesky_login_auth_factor_token_hint))
257+
},
258+
enabled = !state.blueskyLoginState.loading,
259+
modifier =
260+
Modifier
261+
.width(300.dp),
262+
lineLimits = TextFieldLineLimits.SingleLine,
263+
)
264+
}
265+
if (!state.blueskyInputState.usePasswordLogin) {
266+
OnNewIntent {
267+
state.blueskyOauthLoginState.resume(it.dataString.orEmpty())
268+
}
269+
}
270+
228271
Button(
229272
onClick = {
230-
state.blueskyLoginState.login(
231-
"https://${state.instanceInputState.text}",
232-
state.blueskyInputState.username.text
233-
.toString(),
234-
state.blueskyInputState.password.text
235-
.toString(),
236-
)
273+
if (state.blueskyInputState.usePasswordLogin) {
274+
state.blueskyLoginState.login(
275+
baseUrl = state.instanceInputState.text.toString(),
276+
username =
277+
state.blueskyInputState.username.text
278+
.toString(),
279+
password =
280+
state.blueskyInputState.password.text
281+
.toString(),
282+
authFactorToken =
283+
state.blueskyInputState.authFactorToken.text
284+
.toString(),
285+
)
286+
} else {
287+
state.blueskyOauthLoginState.login(
288+
userName =
289+
state.blueskyInputState.username.text
290+
.toString(),
291+
launchUrl = uriHandler::openUri,
292+
)
293+
}
237294
},
238295
modifier = Modifier.width(300.dp),
239-
enabled = state.blueskyInputState.canLogin && !state.blueskyLoginState.loading,
296+
enabled =
297+
state.blueskyInputState.canLogin &&
298+
(
299+
!state.blueskyOauthLoginState.loading &&
300+
!state.blueskyLoginState.loading
301+
),
240302
) {
241303
Text(text = stringResource(id = R.string.login_button))
242304
}
305+
if (state.blueskyOauthLoginState.error != null) {
306+
Text(
307+
text = state.blueskyOauthLoginState.error.toString(),
308+
)
309+
}
310+
if (state.blueskyOauthLoginState.loading) {
311+
LinearWavyProgressIndicator()
312+
}
243313
}
244314

245315
PlatformType.Misskey -> {
@@ -251,7 +321,7 @@ internal fun ServiceSelectScreen(
251321
Text(
252322
text = stringResource(id = R.string.mastodon_login_verify_message),
253323
)
254-
CircularProgressIndicator()
324+
LinearWavyProgressIndicator()
255325
}?.onError {
256326
Text(text = it.message ?: "Unknown error")
257327
} ?: run {
@@ -284,7 +354,7 @@ internal fun ServiceSelectScreen(
284354
Text(
285355
text = stringResource(id = R.string.mastodon_login_verify_message),
286356
)
287-
CircularProgressIndicator()
357+
LinearWavyProgressIndicator()
288358
}?.onError {
289359
Text(text = it.message ?: "Unknown error")
290360
} ?: run {
@@ -479,10 +549,10 @@ private fun serviceSelectPresenter(onBack: (() -> Unit)?) =
479549
state.setFilter(it.toString())
480550
}
481551
}
482-
val blueskyLoginState = blueskyLoginPresenter()
552+
val blueskyInputState = blueskyInputPresenter()
483553
object : ServiceSelectState by state {
484554
val instanceInputState = instanceInputState
485-
val blueskyInputState = blueskyLoginState
555+
val blueskyInputState = blueskyInputState
486556

487557
fun selectInstance(instance: UiInstance) {
488558
instanceInputState.edit {
@@ -501,18 +571,31 @@ private fun serviceSelectPresenter(onBack: (() -> Unit)?) =
501571
}
502572

503573
@Composable
504-
private fun blueskyLoginPresenter() =
574+
private fun blueskyInputPresenter() =
505575
run {
576+
var usePasswordLogin by remember { mutableStateOf(false) }
506577
val username = rememberTextFieldState()
507578
val password = rememberTextFieldState()
579+
val authFactorToken = rememberTextFieldState()
508580
val canLogin by remember(username, password) {
509581
derivedStateOf {
510-
username.text.isNotEmpty() && password.text.isNotEmpty()
582+
username.text.isNotEmpty() &&
583+
if (usePasswordLogin) {
584+
password.text.isNotEmpty()
585+
} else {
586+
true
587+
}
511588
}
512589
}
513590
object {
514591
val username = username
515592
val password = password
593+
val authFactorToken = authFactorToken
594+
val usePasswordLogin = usePasswordLogin
516595
val canLogin = canLogin
596+
597+
fun setUsePasswordLogin(value: Boolean) {
598+
usePasswordLogin = value
599+
}
517600
}
518601
}

app/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@
8888

8989
<string name="bluesky_login_username_hint">Username</string>
9090
<string name="bluesky_login_password_hint">Password</string>
91+
<string name="bluesky_login_auth_factor_token_hint">Authentication Factor Token</string>
92+
<string name="bluesky_login_oauth_button">Login with OAuth</string>
93+
<string name="bluesky_login_use_password_button">Use Password</string>
9194

9295
<string name="report_title">Report</string>
9396
<string name="report_description">What\'s the issue with this post?</string>

gradle/libs.versions.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ okio = "3.16.0"
3737
skie = "0.10.5"
3838
ksoup = "0.2.0"
3939
versionUpdate = "0.52.0"
40-
bluesky = "0.3.3"
40+
bluesky = "0.3.3-SNAPSHOT"
4141
kotlinx-coroutines = "1.10.2"
4242
koin = "4.1.0"
4343
composeIcons = "1.3.0"
@@ -56,7 +56,8 @@ zoomable = "0.16.0"
5656

5757
[libraries]
5858
androidx-collection = { module = "androidx.collection:collection", version.ref = "collection" }
59-
bluesky = { module = "sh.christian.ozone:bluesky", version.ref = "bluesky" }
59+
bluesky = { module = "moe.tlaster.ozone:bluesky", version.ref = "bluesky" }
60+
bluesky-oauth = { module = "moe.tlaster.ozone:oauth", version.ref = "bluesky" }
6061
clikt = { module = "com.github.ajalt.clikt:clikt", version.ref = "clikt" }
6162
core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core-ktx" }
6263
haze-materials = { module = "dev.chrisbanes.haze:haze-materials", version.ref = "haze" }

iosApp/iosApp/UI/Page/OAuth/ServiceSelectScreen.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@ struct ServiceSelectScreen: View {
176176
state.blueskyLoginState.login(
177177
baseUrl: blueskyInputViewModel.baseUrl,
178178
username: blueskyInputViewModel.username,
179-
password: blueskyInputViewModel.password
179+
password: blueskyInputViewModel.password,
180+
authFactorToken: nil
180181
)
181182
}, label: {
182183
Text("confirm")

shared/build.gradle.kts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ kotlin {
3131
iosX64(),
3232
iosArm64(),
3333
iosSimulatorArm64(),
34-
// macosArm64(),
35-
// macosX64(),
34+
macosArm64(),
35+
macosX64(),
3636
).forEach { appleTarget ->
3737
appleTarget.binaries.framework {
3838
baseName = "shared"
@@ -73,6 +73,7 @@ kotlin {
7373
implementation(libs.twitter.parser)
7474
implementation(libs.molecule.runtime)
7575
api(libs.bluesky)
76+
api(libs.bluesky.oauth)
7677
implementation(libs.room.runtime)
7778
implementation(libs.room.paging)
7879
implementation(libs.sqlite.bundled)

shared/src/appleMain/kotlin/dev/dimension/flare/common/AppDeepLink.apple.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ public object AppDeepLinkHelper {
1414
when (data.segments.getOrNull(1)) {
1515
"Mastodon" -> AppleRoute.Callback.Mastodon
1616
"Misskey" -> AppleRoute.Callback.Misskey
17+
"Bluesky" -> AppleRoute.Callback.Bluesky
1718
else -> null
1819
}
1920
else -> null
@@ -177,6 +178,11 @@ public sealed class AppleRoute {
177178
override val routeType: RouteType
178179
get() = RouteType.Screen
179180
}
181+
182+
public data object Bluesky : Callback() {
183+
override val routeType: RouteType
184+
get() = RouteType.Screen
185+
}
180186
}
181187

182188
public data class Search(

shared/src/commonMain/kotlin/dev/dimension/flare/common/AppDeepLink.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public object AppDeepLink {
1212
public object Callback {
1313
public const val MASTODON: String = "$APPSCHEMA://Callback/SignIn/Mastodon"
1414
public const val MISSKEY: String = "$APPSCHEMA://Callback/SignIn/Misskey"
15+
16+
public const val BLUESKY: String = "$APPSCHEMA://Callback/SignIn/Bluesky"
1517
}
1618

1719
public object Search {

0 commit comments

Comments
 (0)