feat: onboarding flow
Signed-off-by: Infi <wingit@geist.ga>
This commit is contained in:
parent
e316c0bf3a
commit
d42395b826
|
|
@ -26,6 +26,7 @@ import chat.revolt.screens.chat.dialogs.FeedbackDialog
|
|||
import chat.revolt.screens.login.LoginGreetingScreen
|
||||
import chat.revolt.screens.login.LoginScreen
|
||||
import chat.revolt.screens.login.MfaScreen
|
||||
import chat.revolt.screens.register.OnboardingScreen
|
||||
import chat.revolt.screens.register.RegisterDetailsScreen
|
||||
import chat.revolt.screens.register.RegisterGreetingScreen
|
||||
import chat.revolt.screens.register.RegisterVerifyScreen
|
||||
|
|
@ -119,6 +120,7 @@ fun AppEntrypoint() {
|
|||
|
||||
RegisterVerifyScreen(navController, email)
|
||||
}
|
||||
composable("register/onboarding") { OnboardingScreen(navController) }
|
||||
|
||||
composable("chat") { ChatRouterScreen(navController) }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package chat.revolt.api.routes.onboard
|
||||
|
||||
import android.util.Log
|
||||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.RevoltError
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import chat.revolt.api.RevoltJson
|
||||
import chat.revolt.api.schemas.RsResult
|
||||
import io.ktor.client.request.get
|
||||
import io.ktor.client.request.header
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.contentType
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
|
||||
@Serializable
|
||||
data class OnboardingResponse(
|
||||
val onboarding: Boolean
|
||||
)
|
||||
|
||||
|
||||
suspend fun needsOnboarding(
|
||||
sessionToken: String = RevoltAPI.sessionToken,
|
||||
): Boolean {
|
||||
val response = RevoltHttp.get("/onboard/hello") {
|
||||
header(RevoltAPI.TOKEN_HEADER_NAME, sessionToken)
|
||||
}
|
||||
|
||||
val responseContent = response.bodyAsText()
|
||||
|
||||
return RevoltJson.decodeFromString(OnboardingResponse.serializer(), responseContent).onboarding
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class OnboardingCompletionBody(
|
||||
val username: String,
|
||||
)
|
||||
|
||||
suspend fun completeOnboarding(
|
||||
body: OnboardingCompletionBody,
|
||||
sessionToken: String = RevoltAPI.sessionToken,
|
||||
): RsResult<Unit, RevoltError> {
|
||||
val response = RevoltHttp.post("/onboard/complete") {
|
||||
setBody(body)
|
||||
contentType(ContentType.Application.Json)
|
||||
header(RevoltAPI.TOKEN_HEADER_NAME, sessionToken)
|
||||
}
|
||||
|
||||
Log.d("RevoltAPI", "completeOnboarding: ${response.status}")
|
||||
|
||||
if (response.status == HttpStatusCode.Conflict) {
|
||||
return RsResult.err(RevoltError("UsernameTaken"))
|
||||
}
|
||||
|
||||
if (response.status == HttpStatusCode.BadRequest) {
|
||||
return RsResult.err(RevoltError("InvalidUsername"))
|
||||
}
|
||||
|
||||
val responseContent = response.bodyAsText()
|
||||
|
||||
try {
|
||||
val error = RevoltJson.decodeFromString(RevoltError.serializer(), responseContent)
|
||||
return RsResult.err(error)
|
||||
} catch (e: SerializationException) {
|
||||
// Not an error
|
||||
}
|
||||
|
||||
return RsResult.ok(Unit)
|
||||
}
|
||||
|
|
@ -3,8 +3,10 @@ package chat.revolt.api.routes.sync
|
|||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import chat.revolt.api.RevoltJson
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.client.request.parameter
|
||||
import io.ktor.client.request.post
|
||||
import io.ktor.client.request.setBody
|
||||
import io.ktor.client.statement.bodyAsText
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.builtins.MapSerializer
|
||||
|
|
|
|||
|
|
@ -17,20 +17,24 @@ object SyncedSettings {
|
|||
get() = _android.value
|
||||
|
||||
suspend fun fetch() {
|
||||
val settings = getKeys("ordering", "android")
|
||||
try {
|
||||
val settings = getKeys("ordering", "android")
|
||||
|
||||
settings["ordering"]?.let {
|
||||
_ordering.value = RevoltJson.decodeFromString(
|
||||
OrderingSettings.serializer(),
|
||||
it.value
|
||||
)
|
||||
}
|
||||
settings["ordering"]?.let {
|
||||
_ordering.value = RevoltJson.decodeFromString(
|
||||
OrderingSettings.serializer(),
|
||||
it.value
|
||||
)
|
||||
}
|
||||
|
||||
settings["android"]?.let {
|
||||
_android.value = RevoltJson.decodeFromString(
|
||||
AndroidSpecificSettings.serializer(),
|
||||
it.value
|
||||
)
|
||||
settings["android"]?.let {
|
||||
_android.value = RevoltJson.decodeFromString(
|
||||
AndroidSpecificSettings.serializer(),
|
||||
it.value
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import chat.revolt.activities.WebChallengeActivity
|
|||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.RevoltHttp
|
||||
import chat.revolt.api.internals.WebChallenge
|
||||
import chat.revolt.api.routes.onboard.needsOnboarding
|
||||
import chat.revolt.api.settings.GlobalState
|
||||
import chat.revolt.api.settings.SyncedSettings
|
||||
import chat.revolt.components.screens.splash.DisconnectedScreen
|
||||
|
|
@ -112,6 +113,12 @@ class SplashScreenViewModel @Inject constructor(
|
|||
kvStorage.remove("sessionToken")
|
||||
setNavigateTo("login")
|
||||
} else {
|
||||
val onboard = needsOnboarding()
|
||||
if (onboard) {
|
||||
setNavigateTo("onboarding")
|
||||
return@launch
|
||||
}
|
||||
|
||||
RevoltAPI.loginAs(token)
|
||||
loadSettings()
|
||||
setNavigateTo("home")
|
||||
|
|
@ -183,6 +190,14 @@ fun SplashScreen(
|
|||
}
|
||||
}
|
||||
|
||||
"onboarding" -> {
|
||||
navController.navigate("register/onboarding") {
|
||||
popUpTo("splash") {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"webchallenge" -> {
|
||||
val intent = Intent(context, WebChallengeActivity::class.java)
|
||||
webChallengeActivityResult.launch(intent)
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import chat.revolt.api.REVOLT_SUPPORT
|
|||
import chat.revolt.api.RevoltAPI
|
||||
import chat.revolt.api.routes.account.EmailPasswordAssessment
|
||||
import chat.revolt.api.routes.account.negotiateAuthentication
|
||||
import chat.revolt.api.routes.onboard.needsOnboarding
|
||||
import chat.revolt.components.generic.AnyLink
|
||||
import chat.revolt.components.generic.FormTextField
|
||||
import chat.revolt.components.generic.Weblink
|
||||
|
|
@ -89,8 +90,17 @@ class LoginViewModel @Inject constructor(
|
|||
)
|
||||
|
||||
try {
|
||||
RevoltAPI.loginAs(response.firstUserHints!!.token)
|
||||
kvStorage.set("sessionToken", response.firstUserHints.token)
|
||||
val token = response.firstUserHints!!.token
|
||||
|
||||
kvStorage.set("sessionToken", token)
|
||||
|
||||
val onboard = needsOnboarding(token)
|
||||
if (onboard) {
|
||||
_navigateTo = "onboarding"
|
||||
return@launch
|
||||
}
|
||||
|
||||
RevoltAPI.loginAs(token)
|
||||
|
||||
_navigateTo = "home"
|
||||
} catch (e: Error) {
|
||||
|
|
@ -136,6 +146,12 @@ fun LoginScreen(
|
|||
popUpTo("login/greeting") { inclusive = true }
|
||||
}
|
||||
}
|
||||
|
||||
"onboarding" -> {
|
||||
navController.navigate("register/onboarding") {
|
||||
popUpTo("login/greeting") { inclusive = true }
|
||||
}
|
||||
}
|
||||
}
|
||||
if (viewModel.navigateTo != null) {
|
||||
viewModel.navigationComplete()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,174 @@
|
|||
package chat.revolt.screens.register
|
||||
|
||||
import android.util.Log
|
||||
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.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import chat.revolt.R
|
||||
import chat.revolt.api.routes.onboard.OnboardingCompletionBody
|
||||
import chat.revolt.api.routes.onboard.completeOnboarding
|
||||
import chat.revolt.components.generic.FormTextField
|
||||
import chat.revolt.persistence.KVStorage
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun OnboardingScreen(navController: NavController) {
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val context = LocalContext.current
|
||||
|
||||
val username = remember { mutableStateOf("") }
|
||||
val error = remember { mutableStateOf("") }
|
||||
|
||||
fun onboardingComplete() {
|
||||
navController.navigate("splash") {
|
||||
popUpTo("register/onboarding") { inclusive = true }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun onboard() {
|
||||
val body = OnboardingCompletionBody(
|
||||
username = username.value
|
||||
)
|
||||
|
||||
val sessionToken = KVStorage(context).get("sessionToken") ?: return
|
||||
val result = completeOnboarding(body, sessionToken)
|
||||
|
||||
Log.d("OnboardingScreen", "onboard: $result")
|
||||
|
||||
if (result.ok) {
|
||||
onboardingComplete()
|
||||
} else {
|
||||
error.value = result.error?.type ?: "Unknown error"
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(20.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.onboarding_welcome),
|
||||
style = MaterialTheme.typography.displaySmall.copy(
|
||||
fontSize = 30.sp,
|
||||
fontWeight = FontWeight.Black,
|
||||
textAlign = TextAlign.Center
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.fillMaxWidth(),
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.onboarding_lead),
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.5f
|
||||
),
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Normal,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.onboarding_others),
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.5f
|
||||
),
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Normal,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.onboarding_changeable),
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(
|
||||
alpha = 0.5f
|
||||
),
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
textAlign = TextAlign.Center,
|
||||
fontWeight = FontWeight.Normal,
|
||||
),
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 10.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(40.dp))
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
FormTextField(
|
||||
value = username.value,
|
||||
onChange = { username.value = it },
|
||||
label = stringResource(R.string.onboarding_username),
|
||||
)
|
||||
|
||||
if (error.value.isNotBlank()) {
|
||||
Text(
|
||||
text = error.value,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.padding(horizontal = 40.dp, vertical = 10.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
onboard()
|
||||
}
|
||||
},
|
||||
enabled = username.value.isNotBlank()
|
||||
) {
|
||||
Text(text = stringResource(R.string.next))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -46,10 +46,11 @@
|
|||
<string name="verify_then_choose_username">Verify your email, and then we\'ll get on with choosing your username.</string>
|
||||
<string name="open_mail_app">Open mail app</string>
|
||||
|
||||
<string name="welcome">Welcome!</string>
|
||||
<string name="username_choose_lead">It\'s time to choose a username!</string>
|
||||
<string name="username_choose_others">Others will find, recognise and mention you with this name.</string>
|
||||
<string name="username_choose_changeable">But if you change your mind, you can change your username at any time in your User Settings.</string>
|
||||
<string name="onboarding_welcome">Welcome!</string>
|
||||
<string name="onboarding_lead">It\'s time to choose a username!</string>
|
||||
<string name="onboarding_others">Others will find, recognise and mention you with this name.</string>
|
||||
<string name="onboarding_changeable">But if you change your mind, you can change your username at any time in your User Settings.</string>
|
||||
<string name="onboarding_username">Username</string>
|
||||
|
||||
<string name="token_invalid_toast">You have been logged out. Please log in again.</string>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue