1
Fork 0
mirror of https://github.com/Steffo99/twom.git synced 2024-11-24 00:54:24 +00:00

Refactor out manageLogin

This commit is contained in:
Steffo 2024-01-31 05:06:42 +01:00
parent 63ef1a4edd
commit 529d821db1
Signed by: steffo
GPG key ID: 5ADA3868646C3FC0
7 changed files with 239 additions and 234 deletions

View file

@ -5,7 +5,7 @@ import android.content.Intent
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContract
import eu.steffo.twom.composables.login.LoginScaffold
import eu.steffo.twom.composables.login.components.LoginScaffold
class LoginActivity : ComponentActivity() {

View file

@ -1,8 +1,13 @@
package eu.steffo.twom.composables.errorhandling
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.res.stringResource
import kotlinx.coroutines.CancellationException
private const val TAG = "LocalizableError"
data class LocalizableError(
@StringRes val stringResourceId: Int,
@ -25,3 +30,20 @@ fun LocalizableError?.Display(contents: @Composable (rendered: String) -> Unit)
val rendered = this.render() ?: return
contents(rendered)
}
suspend fun MutableState<LocalizableError?>.capture(
@StringRes error: Int,
coroutine: suspend () -> Unit,
): Unit? {
try {
coroutine()
} catch (e: CancellationException) {
Log.v(TAG, "Cancelled coroutine execution", e)
return null
} catch (e: Throwable) {
Log.e(TAG, "Captured error during coroutine execution", e)
this.value = LocalizableError(error, e)
return null
}
return Unit
}

View file

@ -1,231 +0,0 @@
package eu.steffo.twom.composables.login
import android.util.Log
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.R
import eu.steffo.twom.composables.errorhandling.Display
import eu.steffo.twom.composables.errorhandling.ErrorText
import eu.steffo.twom.composables.errorhandling.LocalizableError
import eu.steffo.twom.composables.fields.PasswordField
import eu.steffo.twom.composables.theme.basePadding
import eu.steffo.twom.utils.TwoMGlobals
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.data.LoginFlowResult
import org.matrix.android.sdk.api.auth.login.LoginWizard
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
import org.matrix.android.sdk.api.failure.MatrixIdFailure
import org.matrix.android.sdk.api.session.Session
enum class LoginStep(val step: Int) {
NONE(0),
SERVICE(1),
WELLKNOWN(2),
FLOWS(3),
WIZARD(4),
LOGIN(5),
DONE(6),
}
@Composable
@Preview(showBackground = true)
fun LoginForm(
modifier: Modifier = Modifier,
onLogin: (session: Session) -> Unit = {},
) {
val scope = rememberCoroutineScope()
var username by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
var loginStep by rememberSaveable { mutableStateOf(LoginStep.NONE) }
var error by remember { mutableStateOf<LocalizableError?>(null) }
suspend fun doLogin() {
error = null
Log.d("Login", "Getting authentication service...")
loginStep = LoginStep.SERVICE
val auth = TwoMGlobals.matrix.authenticationService()
Log.d("Login", "Resetting authentication service...")
auth.reset()
Log.d("Login", "Retrieving .well-known data for: $username")
loginStep = LoginStep.WELLKNOWN
lateinit var wellKnown: WellknownResult
try {
wellKnown = auth.getWellKnownData(username, null)
} catch (e: MatrixIdFailure.InvalidMatrixId) {
Log.d(
"Login",
"User seems to have input an invalid Matrix ID: $username",
e
)
error = LocalizableError(R.string.login_error_username_invalid)
return
} catch (e: Throwable) {
Log.e(
"Login",
"Something went wrong while retrieving .well-known data for: $username",
e
)
error = LocalizableError(R.string.login_error_wellknown_generic, e)
return
}
if (wellKnown !is WellknownResult.Prompt) {
Log.w(
"Login",
"Data is not .well-known for: $username"
)
error = LocalizableError(R.string.login_error_wellknown_missing)
return
}
Log.d("Login", "Retrieving login flows for: ${wellKnown.homeServerUrl}")
loginStep = LoginStep.FLOWS
@Suppress("ASSIGNED_BUT_NEVER_ACCESSED_VARIABLE")
lateinit var flows: LoginFlowResult
try {
@Suppress("UNUSED_VALUE")
flows = auth.getLoginFlow(
HomeServerConnectionConfig
.Builder()
.withHomeServerUri(wellKnown.homeServerUrl)
.build()
)
} catch (e: Throwable) {
Log.e(
"Login",
"Something went wrong while retrieving login flows for: ${wellKnown.homeServerUrl}",
e
)
error = LocalizableError(R.string.login_error_flows_generic, e)
return
}
Log.d("Login", "Creating login wizard...")
loginStep = LoginStep.WIZARD
lateinit var wizard: LoginWizard
try {
wizard = auth.getLoginWizard() // Why is this stateful? Aargh.
} catch (e: Throwable) {
Log.e(
"Login",
"Something went wrong while setting up the login wizard.",
e
)
error = LocalizableError(R.string.login_error_wizard_generic, e)
return
}
Log.d("Login", "Logging in as: $username")
loginStep = LoginStep.LOGIN
lateinit var session: Session
try {
session = wizard.login(
login = username,
password = password,
initialDeviceName = "TwoM (Android)",
)
} catch (e: Throwable) {
Log.e(
"Login",
"Something went wrong while logging in as: $username",
e
)
error = LocalizableError(R.string.login_error_login_generic, e)
return
}
Log.d(
"Login",
"Logged in successfully with session id: ${session.sessionId}"
)
loginStep = LoginStep.DONE
onLogin(session)
}
Column(modifier) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
progress = loginStep.step.toFloat() / LoginStep.DONE.step.toFloat(),
color = if (error != null) MaterialTheme.colorScheme.error else ProgressIndicatorDefaults.linearColor
)
Row(Modifier.basePadding()) {
Text(LocalContext.current.getString(R.string.login_text))
}
Row(Modifier.basePadding()) {
TextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
value = username,
onValueChange = { username = it },
label = {
Text(LocalContext.current.getString(R.string.login_username_label))
},
placeholder = {
Text(LocalContext.current.getString(R.string.login_username_placeholder))
},
supportingText = {
Text(LocalContext.current.getString(R.string.login_username_supporting))
},
)
}
Row(Modifier.basePadding()) {
PasswordField(
modifier = Modifier.fillMaxWidth(),
value = password,
onValueChange = { password = it },
label = {
Text(LocalContext.current.getString(R.string.login_password_label))
},
placeholder = {
Text(LocalContext.current.getString(R.string.login_password_placeholder))
},
supportingText = {
Text(LocalContext.current.getString(R.string.login_password_supporting))
},
)
}
Row(Modifier.basePadding()) {
Button(
modifier = Modifier.fillMaxWidth(),
enabled = (username != "" && (loginStep == LoginStep.NONE || error != null)),
onClick = {
scope.launch { doLogin() }
},
) {
Text(LocalContext.current.getString(R.string.login_complete_text))
}
}
error.Display {
Row(Modifier.basePadding()) {
ErrorText(
text = it
)
}
}
}
}

View file

@ -0,0 +1,113 @@
package eu.steffo.twom.composables.login.components
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Button
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProgressIndicatorDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import eu.steffo.twom.R
import eu.steffo.twom.composables.errorhandling.Display
import eu.steffo.twom.composables.errorhandling.ErrorText
import eu.steffo.twom.composables.fields.PasswordField
import eu.steffo.twom.composables.login.effects.LoginStep
import eu.steffo.twom.composables.login.effects.manageLogin
import eu.steffo.twom.composables.theme.basePadding
import kotlinx.coroutines.launch
import org.matrix.android.sdk.api.session.Session
@Composable
@Preview(showBackground = true)
fun LoginForm(
modifier: Modifier = Modifier,
onLogin: (session: Session) -> Unit = {},
) {
val scope = rememberCoroutineScope()
var username by rememberSaveable { mutableStateOf("") }
var password by rememberSaveable { mutableStateOf("") }
val manager = manageLogin()
Column(modifier) {
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
progress = manager.step.ord.toFloat() / LoginStep.DONE.ord.toFloat(),
color = if (manager.error != null) {
MaterialTheme.colorScheme.error
} else {
ProgressIndicatorDefaults.linearColor
}
)
Row(Modifier.basePadding()) {
Text(LocalContext.current.getString(R.string.login_text))
}
Row(Modifier.basePadding()) {
TextField(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
value = username,
onValueChange = { username = it },
label = {
Text(LocalContext.current.getString(R.string.login_username_label))
},
placeholder = {
Text(LocalContext.current.getString(R.string.login_username_placeholder))
},
supportingText = {
Text(LocalContext.current.getString(R.string.login_username_supporting))
},
)
}
Row(Modifier.basePadding()) {
PasswordField(
modifier = Modifier.fillMaxWidth(),
value = password,
onValueChange = { password = it },
label = {
Text(LocalContext.current.getString(R.string.login_password_label))
},
placeholder = {
Text(LocalContext.current.getString(R.string.login_password_placeholder))
},
supportingText = {
Text(LocalContext.current.getString(R.string.login_password_supporting))
},
)
}
Row(Modifier.basePadding()) {
Button(
modifier = Modifier.fillMaxWidth(),
enabled = (username != "" && (manager.step == LoginStep.NONE || manager.error != null)),
onClick = {
scope.launch DoLogin@{
val session = manager.login(username, password) ?: return@DoLogin
onLogin(session)
}
},
) {
Text(LocalContext.current.getString(R.string.login_complete_text))
}
}
manager.error.Display {
Row(Modifier.basePadding()) {
ErrorText(
text = it
)
}
}
}
}

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.composables.login
package eu.steffo.twom.composables.login.components
import android.app.Activity
import android.content.Intent

View file

@ -1,4 +1,4 @@
package eu.steffo.twom.composables.login
package eu.steffo.twom.composables.login.components
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text

View file

@ -0,0 +1,101 @@
package eu.steffo.twom.composables.login.effects
import android.util.Log
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import eu.steffo.twom.R
import eu.steffo.twom.composables.errorhandling.LocalizableError
import eu.steffo.twom.composables.errorhandling.capture
import eu.steffo.twom.utils.TwoMGlobals
import org.matrix.android.sdk.api.auth.data.HomeServerConnectionConfig
import org.matrix.android.sdk.api.auth.wellknown.WellknownResult
import org.matrix.android.sdk.api.session.Session
private const val TAG = "manageLogin"
enum class LoginStep(val ord: Int) {
NONE(0),
RESET(1),
WELLKNOWN(2),
FLOWS(3),
WIZARD(4),
LOGIN(5),
DONE(6),
}
data class LoginManager(
val step: LoginStep,
val error: LocalizableError?,
val login: suspend (username: String, password: String) -> Session?,
)
@Composable
fun manageLogin(): LoginManager {
var step by remember { mutableStateOf(LoginStep.NONE) }
val error = remember { mutableStateOf<LocalizableError?>(null) }
suspend fun login(username: String, password: String): Session? {
Log.i(TAG, "Starting login process for: $username")
step = LoginStep.NONE
Log.d(TAG, "Resetting authentication service...")
step = LoginStep.RESET
val auth = TwoMGlobals.matrix.authenticationService()
error.capture(R.string.login_error_wellknown_generic) {
auth.reset()
} ?: return null
Log.d(TAG, "Retrieving .well-known data for: $username")
step = LoginStep.WELLKNOWN
lateinit var wellKnown: WellknownResult
error.capture(R.string.login_error_wellknown_generic) {
wellKnown = auth.getWellKnownData(
matrixId = username,
homeServerConnectionConfig = null,
)
} ?: return null
if (wellKnown !is WellknownResult.Prompt) {
error.value = LocalizableError(R.string.login_error_wellknown_missing)
return null
}
Log.d(TAG, "Retrieving login flows for: $username")
step = LoginStep.FLOWS
error.capture(R.string.login_error_flows_generic) {
auth.getLoginFlow(
HomeServerConnectionConfig
.Builder()
.withHomeServerUri((wellKnown as WellknownResult.Prompt).homeServerUrl)
.build()
)
} ?: return null
Log.d(TAG, "Getting login wizard...")
step = LoginStep.WIZARD
val wizard = auth.getLoginWizard()
Log.d(TAG, "Logging in as: $username")
step = LoginStep.LOGIN
lateinit var session: Session
error.capture(R.string.login_error_login_generic) {
session = wizard.login(
login = username,
password = password,
initialDeviceName = "TwoM (Android)"
)
} ?: return null
Log.i(TAG, "Logged in as: $session")
step = LoginStep.DONE
return session
}
return LoginManager(
step = step,
error = error.value,
login = ::login,
)
}