feat: M10 — native SSO fixes + staff operations #21
@@ -320,8 +320,16 @@ private fun RunicNavHost(
|
||||
ContactScreen()
|
||||
}
|
||||
composable(Routes.LOGIN) {
|
||||
// Leave the login screen as soon as the session is established — whether by
|
||||
// password or the SSO bridge. Keying off the shared session (not just the
|
||||
// login VM's local flag) makes this robust to the deep-link/recomposition
|
||||
// timing of the Custom-Tab return, which the LoginScreen callback alone can miss.
|
||||
if (session is Session.SignedIn) {
|
||||
LaunchedEffect(Unit) { navController.popBackStack(Routes.LOGIN, inclusive = true) }
|
||||
} else {
|
||||
LoginScreen(onSignedIn = { navController.popBackStack() })
|
||||
}
|
||||
}
|
||||
composable(Routes.ACCOUNT) {
|
||||
// Only meaningful while signed in; a sign-out (here or from the drawer)
|
||||
// sends the user home rather than leaving a stale identity on screen.
|
||||
|
||||
@@ -5,8 +5,10 @@ package com.runicgateway.app.ui.auth
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
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.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
@@ -15,14 +17,20 @@ import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.rememberModalBottomSheetState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -163,22 +171,25 @@ fun LoginScreen(
|
||||
}
|
||||
}
|
||||
|
||||
// ── Native SSO (§4.2, M9): a button per enabled provider that opens the
|
||||
// Custom-Tab bridge and returns the user signed in. No website-login
|
||||
// fallback — that page isn't mobile-formatted and can't deep-link the
|
||||
// session back; a failed discovery offers a retry instead.
|
||||
// ── Native SSO (§4.2, M9): a single "Sign in with SSO" button that opens the
|
||||
// Custom-Tab bridge. With one provider it launches straight through; with
|
||||
// several it presents a native picker (below). No website-login fallback —
|
||||
// that page can't deep-link the session back; a failed discovery offers a retry.
|
||||
var showSsoPicker by remember { mutableStateOf(false) }
|
||||
when {
|
||||
state.ssoProviders.isNotEmpty() -> {
|
||||
state.ssoProviders.forEach { provider ->
|
||||
OutlinedButton(
|
||||
onClick = { viewModel.onSsoProviderClick(provider) },
|
||||
onClick = {
|
||||
val providers = state.ssoProviders
|
||||
if (providers.size == 1) viewModel.onSsoProviderClick(providers.first())
|
||||
else showSsoPicker = true
|
||||
},
|
||||
enabled = !state.submitting,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp),
|
||||
) {
|
||||
Text(stringResource(R.string.login_sso_provider, provider.name))
|
||||
}
|
||||
Text(stringResource(R.string.login_sso_button))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -214,6 +225,53 @@ fun LoginScreen(
|
||||
Text(stringResource(R.string.login_forgot))
|
||||
}
|
||||
}
|
||||
|
||||
if (showSsoPicker) {
|
||||
SsoProviderPicker(
|
||||
providers = state.ssoProviders,
|
||||
onDismiss = { showSsoPicker = false },
|
||||
onPick = { provider ->
|
||||
showSsoPicker = false
|
||||
viewModel.onSsoProviderClick(provider)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The native provider picker (§4.2): a bottom sheet listing the shard's enabled SSO
|
||||
* providers so a single "Sign in with SSO" button can serve several IdPs without a
|
||||
* website chooser page. Each row opens the Custom-Tab bridge for that provider.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun SsoProviderPicker(
|
||||
providers: List<com.runicgateway.app.data.api.dto.SsoProviderDto>,
|
||||
onDismiss: () -> Unit,
|
||||
onPick: (com.runicgateway.app.data.api.dto.SsoProviderDto) -> Unit,
|
||||
) {
|
||||
ModalBottomSheet(onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState()) {
|
||||
Text(
|
||||
text = stringResource(R.string.login_sso_pick_title),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||
)
|
||||
providers.forEach { provider ->
|
||||
TextButton(
|
||||
onClick = { onPick(provider) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 2.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.login_sso_provider, provider.name),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(24.dp)) // clears the gesture inset at the sheet's bottom
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,7 +142,10 @@
|
||||
<string name="login_button">Sign in</string>
|
||||
<string name="login_register">Create an account</string>
|
||||
<string name="login_forgot">Forgot your password?</string>
|
||||
<!-- %1$s is the provider name, e.g. "Google" or "Discord" (native SSO, M9). -->
|
||||
<!-- Single SSO entry point; the picker lists the shard's providers (native SSO, M9/M10). -->
|
||||
<string name="login_sso_button">Sign in with SSO</string>
|
||||
<string name="login_sso_pick_title">Choose a sign-in provider</string>
|
||||
<!-- %1$s is the provider name, e.g. "Google" or "Discord". -->
|
||||
<string name="login_sso_provider">Sign in with %1$s</string>
|
||||
<string name="login_sso_loading">Loading sign-in options…</string>
|
||||
<string name="login_sso_retry">Couldn\'t load sign-in options. Tap to retry.</string>
|
||||
|
||||
Reference in New Issue
Block a user