Skip to content

Commit c7fb675

Browse files
committed
feat(onboarding): simplified bouncer auth (S2)
Soju path shows only Username + Password (SASL PLAIN); no mechanism picker or EXTERNAL/none. Direct IRC-network path keeps full auth options.
1 parent 7509c2d commit c7fb675

4 files changed

Lines changed: 95 additions & 2 deletions

File tree

app/src/main/kotlin/io/github/trevarj/motd/ui/onboarding/OnboardingReducer.kt

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,16 @@ fun onboardingReducer(state: OnboardingState, action: OnboardingAction): Onboard
149149
is OnboardingAction.GoTo -> state.copy(step = action.step)
150150

151151
is OnboardingAction.ChooseConnection ->
152-
state.copy(choice = action.choice)
152+
// soju logs in with SASL PLAIN (bouncer user + password); force the mode so the
153+
// AUTH step only needs to collect the two fields and the entity persists as PLAIN.
154+
state.copy(
155+
choice = action.choice,
156+
auth = if (action.choice == ConnectionChoice.SOJU) {
157+
state.auth.copy(mode = AuthMode.PLAIN)
158+
} else {
159+
state.auth
160+
},
161+
)
153162

154163
is OnboardingAction.ApplyLiberaPreset ->
155164
state.copy(

app/src/main/kotlin/io/github/trevarj/motd/ui/onboarding/OnboardingScreen.kt

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import androidx.compose.ui.graphics.Color
4646
import androidx.compose.ui.res.painterResource
4747
import androidx.compose.ui.res.stringResource
4848
import androidx.compose.ui.text.font.FontWeight
49+
import androidx.compose.ui.text.input.PasswordVisualTransformation
4950
import androidx.compose.ui.text.style.TextAlign
5051
import androidx.compose.ui.tooling.preview.Preview
5152
import androidx.compose.ui.unit.dp
@@ -109,7 +110,13 @@ fun OnboardingContent(
109110
OnboardingStep.WELCOME -> WelcomePage()
110111
OnboardingStep.CHOICE -> ChoicePage(state, onChoose, onLibera)
111112
OnboardingStep.SERVER -> ServerPage(state, onServerChange, onAuthChange, authOnly = false)
112-
OnboardingStep.AUTH -> ServerPage(state, onServerChange, onAuthChange, authOnly = true)
113+
OnboardingStep.AUTH ->
114+
// soju always uses SASL PLAIN: show only user/password, no mechanism picker.
115+
if (state.isSoju) {
116+
SojuAuthPage(state, onAuthChange)
117+
} else {
118+
ServerPage(state, onServerChange, onAuthChange, authOnly = true)
119+
}
113120
OnboardingStep.CONNECT -> ConnectPage(state, onRetry, onToggleBouncer, onAddBouncer)
114121
OnboardingStep.FINISH -> FinishPage()
115122
}
@@ -277,6 +284,42 @@ private fun ServerPage(
277284
}
278285
}
279286

287+
/**
288+
* Simplified AUTH page for the soju bouncer path: only username + password, always SASL PLAIN.
289+
* No mechanism picker (NONE/EXTERNAL are meaningless for soju login).
290+
*/
291+
@Composable
292+
private fun SojuAuthPage(
293+
state: OnboardingState,
294+
onAuthChange: (AuthForm) -> Unit,
295+
) {
296+
Column(
297+
modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(24.dp),
298+
verticalArrangement = Arrangement.spacedBy(12.dp),
299+
) {
300+
Text(
301+
stringResource(R.string.onboarding_auth_title),
302+
style = MaterialTheme.typography.titleLarge,
303+
fontWeight = FontWeight.Bold,
304+
)
305+
OutlinedTextField(
306+
value = state.auth.saslUser,
307+
onValueChange = { onAuthChange(state.auth.copy(saslUser = it)) },
308+
label = { Text(stringResource(R.string.onboarding_auth_soju_username)) },
309+
singleLine = true,
310+
modifier = Modifier.fillMaxWidth(),
311+
)
312+
OutlinedTextField(
313+
value = state.auth.saslPassword,
314+
onValueChange = { onAuthChange(state.auth.copy(saslPassword = it)) },
315+
label = { Text(stringResource(R.string.onboarding_auth_soju_password)) },
316+
singleLine = true,
317+
visualTransformation = PasswordVisualTransformation(),
318+
modifier = Modifier.fillMaxWidth(),
319+
)
320+
}
321+
}
322+
280323
@Composable
281324
private fun ConnectPage(
282325
state: OnboardingState,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
<string name="onboarding_auth_sasl_password">Password</string>
5151
<string name="onboarding_auth_choose_cert">Choose certificate</string>
5252
<string name="onboarding_auth_cert_selected">Certificate: %1$s</string>
53+
<!-- soju bouncer auth: always SASL PLAIN, no picker (WP-S2) -->
54+
<string name="onboarding_auth_soju_username">Username</string>
55+
<string name="onboarding_auth_soju_password">Password</string>
5356
<string name="onboarding_connect_title">Connecting</string>
5457
<string name="onboarding_connect_retry">Retry</string>
5558
<string name="onboarding_connect_networks_title">Bouncer networks</string>

app/src/test/kotlin/io/github/trevarj/motd/ui/onboarding/OnboardingReducerTest.kt

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,44 @@ class OnboardingReducerTest {
5050
assertEquals(NetworkRole.BOUNCER_ROOT, s.role)
5151
}
5252

53+
@Test
54+
fun `soju choice forces SASL PLAIN auth mode`() {
55+
val s = onboardingReducer(
56+
OnboardingState(step = OnboardingStep.CHOICE),
57+
OnboardingAction.ChooseConnection(ConnectionChoice.SOJU),
58+
)
59+
assertEquals(AuthMode.PLAIN, s.auth.mode)
60+
}
61+
62+
@Test
63+
fun `soju AUTH advance requires both username and password`() {
64+
// After choosing soju, mode is PLAIN so AUTH validity gates on both fields.
65+
val base = reduce(
66+
OnboardingState(step = OnboardingStep.CHOICE),
67+
OnboardingAction.ChooseConnection(ConnectionChoice.SOJU),
68+
).copy(step = OnboardingStep.AUTH)
69+
70+
assertFalse(base.canAdvance)
71+
assertFalse(base.copy(auth = base.auth.copy(saslUser = "u")).canAdvance)
72+
val complete = base.copy(auth = base.auth.copy(saslUser = "u", saslPassword = "p"))
73+
assertTrue(complete.canAdvance)
74+
assertEquals(OnboardingStep.CONNECT, onboardingReducer(complete, OnboardingAction.Next).step)
75+
}
76+
77+
@Test
78+
fun `network choice leaves auth mode untouched`() {
79+
// Direct path keeps the full picker: NONE stays valid, EXTERNAL still selectable.
80+
val s = onboardingReducer(
81+
OnboardingState(step = OnboardingStep.CHOICE),
82+
OnboardingAction.ChooseConnection(ConnectionChoice.NETWORK),
83+
)
84+
assertEquals(AuthMode.NONE, s.auth.mode)
85+
val none = s.copy(step = OnboardingStep.AUTH)
86+
assertTrue(none.canAdvance)
87+
val external = none.copy(auth = none.auth.copy(mode = AuthMode.EXTERNAL, certAlias = "a"))
88+
assertTrue(external.canAdvance)
89+
}
90+
5391
@Test
5492
fun `libera preset fills host port tls and selects network path`() {
5593
val s = onboardingReducer(

0 commit comments

Comments
 (0)