1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3629 Onboarding | Flow update (#2397)

This commit is contained in:
Konstantin Ivanov 2025-05-13 22:23:06 +02:00 committed by GitHub
parent a30d949792
commit 63ee890a0b
Signed by: github
GPG key ID: B5690EEEBB952194
11 changed files with 443 additions and 264 deletions

View file

@ -332,7 +332,8 @@ class OnboardingFragment : Fragment() {
Mnemonic(
mnemonicColorPalette = mnemonicColorPalette,
space = spaceId,
startingObject = startingObjectId
startingObject = startingObjectId,
navController = navController
)
} else {
Box(
@ -377,12 +378,13 @@ class OnboardingFragment : Fragment() {
BackHandler { onBackClicked() }
}
composable(
route = "${OnboardingNavigation.setEmail}?$ONBOARDING_NAME_PARAM={$ONBOARDING_NAME_PARAM}",
route = "${OnboardingNavigation.setEmail}?$ONBOARDING_SPACE_PARAM={$ONBOARDING_SPACE_PARAM}&$ONBOARDING_STARTING_OBJECT_PARAM={$ONBOARDING_STARTING_OBJECT_PARAM}",
arguments = listOf(
navArgument(ONBOARDING_NAME_PARAM) {
navArgument(ONBOARDING_SPACE_PARAM) { type = NavType.StringType },
navArgument(ONBOARDING_STARTING_OBJECT_PARAM) {
type = NavType.StringType
defaultValue = ""
nullable = false
nullable = true
defaultValue = null
}
),
enterTransition = {
@ -393,6 +395,8 @@ class OnboardingFragment : Fragment() {
}
) {
val focus = LocalFocusManager.current
val spaceId = it.arguments?.getString(ONBOARDING_SPACE_PARAM)
val startingObjectId = it.arguments?.getString(ONBOARDING_STARTING_OBJECT_PARAM)
val onBackClicked : () -> Unit = {
val lastDestination = navController.currentBackStackEntry
if (lastDestination?.destination?.route?.startsWith(OnboardingNavigation.setEmail) == true) {
@ -404,10 +408,23 @@ class OnboardingFragment : Fragment() {
}
currentPage.value = OnboardingPage.SET_EMAIL
backButtonCallback.value = onBackClicked
AddEmail(
navController = navController,
onBackClicked = onBackClicked
)
if (!spaceId.isNullOrEmpty()) {
AddEmail(
space = spaceId,
startingObject = startingObjectId,
onBackClicked = onBackClicked
)
} else {
Box(
modifier = Modifier.fillMaxSize()
) {
Text(
text = stringResource(R.string.onboarding_error_while_creating_account_space_is_missing),
modifier = Modifier.align(Alignment.Center),
textAlign = TextAlign.Center
)
}
}
BackHandler { onBackClicked() }
}
}
@ -586,17 +603,14 @@ class OnboardingFragment : Fragment() {
)
}
is OnboardingSetProfileNameViewModel.Navigation.GoBack -> {
//
// do nothing
}
is OnboardingSetProfileNameViewModel.Navigation.NavigateToAddEmailScreen -> {
if (keyboardInsets.getBottom(density) > 0) {
focusManager.clearFocus(force = true)
delay(KEYBOARD_HIDE_DELAY)
}
navController.navigate(
route = "${OnboardingNavigation.setEmail}?$ONBOARDING_NAME_PARAM=${command.name}",
)
is OnboardingSetProfileNameViewModel.Navigation.OpenStartingObject -> {
//do nothing
}
OnboardingSetProfileNameViewModel.Navigation.OpenVault -> {
//do nothing
}
}
}
@ -615,7 +629,8 @@ class OnboardingFragment : Fragment() {
private fun Mnemonic(
mnemonicColorPalette: List<Color>,
space: Id,
startingObject: Id?
startingObject: Id?,
navController: NavHostController
) {
val component = componentManager().onboardingMnemonicComponent
val vm = daggerViewModel { component.get().getViewModel() }
@ -667,6 +682,13 @@ class OnboardingFragment : Fragment() {
Timber.e(it, "Error while navigation to vault")
}
}
is OnboardingMnemonicViewModel.Command.NavigateToAddEmailScreen -> {
val startingObject = command.startingObject
val space = command.space
navController.navigate(
route = "${OnboardingNavigation.setEmail}?$ONBOARDING_SPACE_PARAM=$space&$ONBOARDING_STARTING_OBJECT_PARAM=$startingObject"
)
}
}
}
}
@ -776,7 +798,8 @@ class OnboardingFragment : Fragment() {
@Composable
private fun AddEmail(
navController: NavHostController,
space: Id,
startingObject: Id?,
onBackClicked: () -> Unit
) {
val component = componentManager().onboardingSoulCreationComponent
@ -786,11 +809,10 @@ class OnboardingFragment : Fragment() {
val keyboardInsets = WindowInsets.ime
val density = LocalDensity.current
val name = navController.currentBackStackEntry?.arguments?.getString(ONBOARDING_NAME_PARAM) ?: ""
SetEmailWrapper(
viewModel = vm,
name = name,
startingObject = startingObject,
space = space,
onBackClicked = onBackClicked
)
@ -798,25 +820,45 @@ class OnboardingFragment : Fragment() {
vm.navigation.collect { command ->
when (command) {
is OnboardingSetProfileNameViewModel.Navigation.NavigateToMnemonic -> {
if (keyboardInsets.getBottom(density) > 0) {
focusManager.clearFocus(force = true)
delay(KEYBOARD_HIDE_DELAY)
}
val space = command.space
val startingObject = command.startingObject
navController.navigate(
route = buildString {
append("${OnboardingNavigation.mnemonic}?$ONBOARDING_SPACE_PARAM=${space.id}")
startingObject?.let { append("&$ONBOARDING_STARTING_OBJECT_PARAM=${it}") }
}
)
//do nothing
}
is OnboardingSetProfileNameViewModel.Navigation.GoBack -> {
//
}
is OnboardingSetProfileNameViewModel.Navigation.NavigateToAddEmailScreen -> {
//do nothing
is OnboardingSetProfileNameViewModel.Navigation.OpenStartingObject -> {
runCatching {
findNavController().navigate(
R.id.actionOpenVault,
VaultFragment.args(deepLink)
)
findNavController().navigate(
R.id.actionOpenSpaceFromVault,
HomeScreenFragment.args(
space = command.space.id,
deeplink = null
)
)
findNavController().navigate(
R.id.objectNavigation,
EditorFragment.args(
ctx = command.startingObject,
space = command.space.id,
)
)
}.onFailure {
Timber.e(it, "Error while navigation to vault")
}
}
OnboardingSetProfileNameViewModel.Navigation.OpenVault -> {
runCatching {
findNavController().navigate(
R.id.actionOpenVault,
VaultFragment.args(deepLink)
)
}.onFailure {
Timber.e(it, "Error while navigation to vault")
}
}
}
}
@ -873,7 +915,7 @@ class OnboardingFragment : Fragment() {
private const val ONBOARDING_SPACE_PARAM = "space"
private const val ONBOARDING_STARTING_OBJECT_PARAM = "startingObject"
private const val ONBOARDING_NAME_PARAM = "name"
private const val ONBOARDING_NAME_PARAM = "startingObject"
}
}

View file

@ -20,11 +20,15 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Devices.PIXEL_7
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.em
import androidx.compose.ui.unit.sp
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_ui.ColorButtonInversion
@ -36,9 +40,15 @@ import com.anytypeio.anytype.core_ui.views.HeadlineOnBoardingTitle
import com.anytypeio.anytype.core_ui.views.OnBoardingButtonPrimary
import com.anytypeio.anytype.core_ui.views.OnBoardingButtonSecondary
import com.anytypeio.anytype.core_ui.views.TextOnBoardingDescription
import com.anytypeio.anytype.core_ui.views.fontRiccioneRegular
import com.anytypeio.anytype.presentation.onboarding.OnboardingStartViewModel
@Preview
@Preview(
showBackground = true,
backgroundColor = 0xFF000000,
showSystemUi = true,
device = PIXEL_7
)
@Composable
fun AuthScreenPreview() {
AuthScreen(
@ -77,7 +87,7 @@ fun AuthScreen(
Column(modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.Center) {
Title(modifier = Modifier.align(Alignment.CenterHorizontally))
Subtitle(modifier = Modifier)
Description()
//Description()
Spacer(modifier = Modifier.height(72.dp))
}
Column(
@ -108,7 +118,7 @@ fun AuthScreen(
@Composable
fun Title(modifier: Modifier = Modifier) {
Image(
painter = painterResource(id = R.drawable.ic_local_first_internet) ,
painter = painterResource(id = R.drawable.ic_local_first_internet),
contentDescription = "Everything app logo",
modifier = modifier
)
@ -126,12 +136,14 @@ fun Subtitle(modifier: Modifier = Modifier) {
Text(
text = stringResource(id = R.string.onboarding_auth_subtitle),
textAlign = TextAlign.Center,
style = HeadlineOnBoardingTitle
.copy(
color = OnboardingSubtitleColor,
fontSize = 43.sp,
lineHeight = 37.5.sp
)
color = OnboardingSubtitleColor,
style = TextStyle(
fontFamily = fontRiccioneRegular,
fontWeight = FontWeight.W400,
fontSize = 44.sp,
lineHeight = 44.sp,
letterSpacing = (-0.05).em
)
)
}
}

View file

@ -20,6 +20,7 @@ import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
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
@ -29,6 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
@ -39,6 +41,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.ButtonSize
@ -55,19 +58,22 @@ import kotlin.text.isNotEmpty
@Composable
fun SetEmailWrapper(
viewModel: OnboardingSetProfileNameViewModel,
name: String,
startingObject: String?,
space: Id,
onBackClicked: () -> Unit,
) {
OnboardingEmailScreen(
onContinueClicked = { email ->
viewModel.onEmailContinueClicked(
name = name,
space = space,
startingObject = startingObject,
email = email
)
},
onSkipClicked = {
viewModel.onEmailSkippedClicked(
name = name,
space = space,
startingObject = startingObject
)
},
isLoading = viewModel.state
@ -87,8 +93,13 @@ fun OnboardingEmailScreen(
var innerValue by remember { mutableStateOf(TextFieldValue()) }
var isError by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
val keyboardController = LocalSoftwareKeyboardController.current
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
fun isValidEmail(email: String): Boolean {
return Patterns.EMAIL_ADDRESS.matcher(email).matches()
}
@ -96,7 +107,7 @@ fun OnboardingEmailScreen(
fun validateAndSubmit() {
if (isValidEmail(innerValue.text)) {
isError = false
focusRequester.freeFocus()
focusManager.clearFocus()
keyboardController?.hide()
onContinueClicked(innerValue.text)
} else {
@ -108,11 +119,10 @@ fun OnboardingEmailScreen(
modifier = Modifier
.windowInsetsPadding(WindowInsets.navigationBars)
.imePadding()
//.background(color = colorResource(id = R.color.black))
.fillMaxSize()
) {
Column {
Spacer(modifier = Modifier.height(148.dp))
Spacer(modifier = Modifier.height(140.dp))
Text(
modifier = Modifier
.fillMaxWidth()
@ -148,7 +158,7 @@ fun OnboardingEmailScreen(
Text(
text = stringResource(id = R.string.onboarding_enter_email),
style = PreviewTitle1Regular,
color = colorResource(id = R.color.text_tertiary)
color = Color(0xFF646464)
)
},
textStyle = PreviewTitle1Regular.copy(
@ -216,9 +226,9 @@ fun OnboardingEmailScreen(
OnBoardingButtonSecondary(
text = stringResource(id = R.string.onboarding_button_skip),
onClick = {
onSkipClicked().also {
focusRequester.freeFocus()
}
focusManager.clearFocus()
keyboardController?.hide()
onSkipClicked()
},
textColor = colorResource(id = R.color.text_white),
size = ButtonSize.Large,

View file

@ -2,16 +2,21 @@ package com.anytypeio.anytype.ui.onboarding.screens.signup
import android.os.Build
import android.os.Build.VERSION_CODES
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
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.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Text
import androidx.compose.material3.ExperimentalMaterial3Api
@ -22,13 +27,15 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Devices.PIXEL_7
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -36,16 +43,14 @@ import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.ColorBackgroundField
import com.anytypeio.anytype.core_ui.ColorButtonRegular
import com.anytypeio.anytype.core_ui.OnBoardingTextPrimaryColor
import com.anytypeio.anytype.core_ui.OnBoardingTextSecondaryColor
import com.anytypeio.anytype.core_ui.extensions.conditional
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.ButtonMedium
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.HeadlineHeading
import com.anytypeio.anytype.core_ui.views.HeadlineOnBoardingDescription
import com.anytypeio.anytype.core_ui.views.HeadlineTitleSemibold
import com.anytypeio.anytype.core_ui.views.OnBoardingButtonPrimary
import com.anytypeio.anytype.core_ui.views.OnBoardingButtonSecondary
import com.anytypeio.anytype.core_ui.views.UXBody
import com.anytypeio.anytype.presentation.onboarding.signup.OnboardingMnemonicViewModel
import com.anytypeio.anytype.ui.onboarding.MnemonicPhraseWidget
import com.anytypeio.anytype.ui.onboarding.MnemonicStub
@ -81,8 +86,12 @@ fun MnemonicPhraseScreenWrapper(
}
@Preview
@Preview(
showBackground = true,
backgroundColor = 0xFF000000,
showSystemUi = true,
device = PIXEL_7
)
@Composable
fun PreviewMnemonicPhraseScreen() {
val fakeMnemonic = "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve"
@ -96,7 +105,12 @@ fun PreviewMnemonicPhraseScreen() {
)
}
@Preview
@Preview(
showBackground = true,
backgroundColor = 0xFF000000,
showSystemUi = true,
device = PIXEL_7
)
@Composable
fun PreviewMnemonicPhraseScreen2() {
val fakeMnemonic = "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve"
@ -125,25 +139,28 @@ fun MnemonicPhraseScreen(
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.Top
.padding(horizontal = 20.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(148.dp))
MnemonicTitle()
MnemonicDescription()
ReadMoreButton(showWhatIsRecoveryPhraseDialog)
Spacer(modifier = Modifier.height(31.dp))
MnemonicPhrase(
state = state,
copyMnemonicToClipboard = copyMnemonicToClipboard,
mnemonicColorPalette = mnemonicColorPalette
)
ReadMoreButton(showWhatIsRecoveryPhraseDialog)
}
MnemonicButtons(
modifier = Modifier.align(Alignment.BottomCenter),
openMnemonic = reviewMnemonic,
reviewMnemonic = reviewMnemonic,
onCheckLaterClicked = onCheckLaterClicked,
onGoToAppClicked = onGoToAppClicked,
state = state
state = state,
copyMnemonicToClipboard = copyMnemonicToClipboard
)
}
if (showWhatIsRecoveryPhraseDialog.value) {
@ -165,22 +182,27 @@ fun MnemonicPhraseScreen(
}
@Composable
private fun ReadMoreButton(showWhatIsRecoveryPhraseDialog: MutableState<Boolean>) {
Box(
private fun ColumnScope.ReadMoreButton(showWhatIsRecoveryPhraseDialog: MutableState<Boolean>) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(top = 8.dp, bottom = 16.dp)
.wrapContentWidth()
.padding(top = 16.dp)
.height(24.dp)
.noRippleClickable {
showWhatIsRecoveryPhraseDialog.value = true
}
},
verticalAlignment = Alignment.CenterVertically,
) {
Image(
modifier = Modifier.wrapContentSize(),
painter = painterResource(id = R.drawable.ic_plus_18),
contentDescription = "Read more about recovery phrase",
)
Text(
text = stringResource(id = R.string.onboarding_mnemonic_read_more),
style = BodyRegular.copy(
color = Color(0xFFDBDAD4)
),
modifier = Modifier.align(Alignment.Center)
style = ButtonMedium,
color = Color(0xFF909090),
modifier = Modifier.padding(start = 8.dp),
)
}
}
@ -188,8 +210,9 @@ private fun ReadMoreButton(showWhatIsRecoveryPhraseDialog: MutableState<Boolean>
@Composable
fun MnemonicButtons(
modifier: Modifier = Modifier,
openMnemonic: () -> Unit,
reviewMnemonic: () -> Unit,
onCheckLaterClicked: () -> Unit,
copyMnemonicToClipboard: (String) -> Unit,
onGoToAppClicked: () -> Unit,
state: OnboardingMnemonicViewModel.State
) {
@ -205,6 +228,7 @@ fun MnemonicButtons(
size = ButtonSize.Large
)
}
else -> {
OnBoardingButtonPrimary(
text = stringResource(id = R.string.onboarding_tap_to_reveal),
@ -212,8 +236,13 @@ fun MnemonicButtons(
.fillMaxWidth()
.padding(horizontal = 16.dp),
onClick = {
openMnemonic.invoke()
}, size = ButtonSize.Large
reviewMnemonic.invoke().also {
if (state is OnboardingMnemonicViewModel.State.Mnemonic) {
copyMnemonicToClipboard.invoke(state.mnemonicPhrase)
}
}
},
size = ButtonSize.Large
)
OnBoardingButtonSecondary(
text = stringResource(id = R.string.onboarding_key_not_now),
@ -248,9 +277,8 @@ fun MnemonicTitle() {
Text(
modifier = Modifier,
text = stringResource(R.string.onboarding_this_is_your_key_title),
style = HeadlineHeading.copy(
color = OnBoardingTextPrimaryColor
),
style = HeadlineTitleSemibold,
color = colorResource(id = R.color.text_white),
textAlign = TextAlign.Center
)
}
@ -273,8 +301,7 @@ fun MnemonicPhrase(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
.background(color = ColorBackgroundField, shape = RoundedCornerShape(24.dp))
.background(color = ColorBackgroundField, shape = RoundedCornerShape(16.dp))
.wrapContentHeight()
) {
if (Build.VERSION.SDK_INT <= VERSION_CODES.R && state is OnboardingMnemonicViewModel.State.Mnemonic) {
@ -298,19 +325,6 @@ fun MnemonicPhrase(
)
}
}
if (state is OnboardingMnemonicViewModel.State.MnemonicOpened) {
OnBoardingButtonSecondary(
text = stringResource(id = R.string.onboarding_key_copy),
modifier = Modifier
.align(CenterHorizontally)
.padding(bottom = 12.dp),
onClick = {
copyMnemonicToClipboard.invoke(state.mnemonicPhrase)
},
size = ButtonSize.SmallSecondary,
textColor = ColorButtonRegular
)
}
}
}
@ -322,16 +336,15 @@ fun MnemonicDescription() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp)
.padding(horizontal = 20.dp)
.wrapContentHeight(),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.onboarding_key_description),
style = HeadlineOnBoardingDescription.copy(
color = OnBoardingTextSecondaryColor,
textAlign = TextAlign.Center
)
textAlign = TextAlign.Center,
style = UXBody,
color = colorResource(id = R.color.text_white)
)
}
}

View file

@ -4,40 +4,55 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.material.Text
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
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.focus.FocusRequester
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Devices.PIXEL_7
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Name
import com.anytypeio.anytype.core_ui.OnBoardingTextPrimaryColor
import com.anytypeio.anytype.core_ui.OnBoardingTextSecondaryColor
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.HeadlineHeading
import com.anytypeio.anytype.core_ui.views.HeadlineOnBoardingDescription
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.HeadlineTitleSemibold
import com.anytypeio.anytype.core_ui.views.OnBoardingButtonPrimary
import com.anytypeio.anytype.core_ui.views.PreviewTitle1Regular
import com.anytypeio.anytype.core_ui.views.UXBody
import com.anytypeio.anytype.presentation.onboarding.signup.OnboardingSetProfileNameViewModel
import com.anytypeio.anytype.ui.onboarding.OnboardingInput
@Composable
@ -46,7 +61,7 @@ fun SetProfileNameWrapper(
onBackClicked: () -> Unit,
) {
val name = remember { mutableStateOf("") }
SetProfileNameScreen(
onNextClicked = { inputName ->
name.value = inputName
@ -67,45 +82,117 @@ private fun SetProfileNameScreen(
onBackClicked: () -> Unit,
isLoading: Boolean
) {
val text = remember { mutableStateOf("") }
val focus = LocalFocusManager.current
var innerValue by remember { mutableStateOf(TextFieldValue()) }
val focusRequester = remember { FocusRequester() }
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
var isError by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
fun validateAndSubmit() {
if (innerValue.text.isNotEmpty()) {
isError = false
focusManager.clearFocus()
keyboardController?.hide()
onNextClicked(innerValue.text)
} else {
isError = true
focusManager.clearFocus()
keyboardController?.hide()
}
}
Box(
modifier = Modifier.fillMaxSize()
modifier = Modifier
.windowInsetsPadding(WindowInsets.navigationBars)
.imePadding()
.fillMaxSize()
) {
Column {
Spacer(
modifier = Modifier.height(148.dp)
modifier = Modifier.height(140.dp)
)
SetProfileNameTitle(modifier = Modifier.padding(bottom = 12.dp))
SetProfileNameDescription()
Spacer(modifier = Modifier.height(16.dp))
SetProfileNameInput(
text = text,
onKeyboardActionDoneClicked = { onNextClicked(text.value) }
Spacer(modifier = Modifier.height(32.dp))
OutlinedTextField(
value = innerValue,
onValueChange = { input ->
innerValue = input
isError = false
},
shape = RoundedCornerShape(size = 16.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.focusRequester(focusRequester),
placeholder = {
Text(
text = stringResource(id = R.string.onboarding_your_name),
style = PreviewTitle1Regular,
color = Color(0xFF646464)
)
},
supportingText = {
if (isError) {
Text(
text = stringResource(id = R.string.onboarding_name_error),
color = colorResource(id = R.color.palette_system_red),
style = Caption1Regular
)
}
},
textStyle = PreviewTitle1Regular.copy(
color = Color(0xFFC2C2C2)
),
singleLine = true,
colors = TextFieldDefaults.colors(
disabledTextColor = colorResource(id = R.color.text_primary),
cursorColor = Color(0xFFC2C2C2),
focusedContainerColor = Color(0xFF212121),
unfocusedContainerColor = Color(0xFF212121),
errorContainerColor = Color(0xFF212121),
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
errorIndicatorColor = Color.Transparent
),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions {
validateAndSubmit()
}
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(32.dp))
}
Image(
modifier = Modifier
.align(Alignment.TopStart)
.padding(top = 16.dp, start = 9.dp)
.noRippleClickable {
focus.clearFocus()
focusManager.clearFocus()
onBackClicked()
},
painter = painterResource(id = R.drawable.ic_back_onboarding_32),
contentDescription = stringResource(R.string.content_description_back_button_icon)
)
SetProfileNameNextButton(
OnBoardingButtonPrimary(
text = stringResource(id = R.string.onboarding_button_continue),
onClick = {
validateAndSubmit()
},
size = ButtonSize.Large,
modifier = Modifier
.align(Alignment.BottomCenter)
.fillMaxWidth()
.padding(start = 20.dp, end = 20.dp, bottom = 12.dp)
,
onNextClicked = onNextClicked,
text = text,
isLoading = isLoading
.padding(
start = 20.dp,
end = 20.dp,
bottom = 20.dp
)
.align(Alignment.BottomCenter),
isLoading = isLoading,
enabled = innerValue.text.isNotEmpty()
)
}
}
@ -122,90 +209,36 @@ fun SetProfileNameTitle(modifier: Modifier) {
Text(
modifier = Modifier,
text = stringResource(R.string.onboarding_set_your_name_title),
style = HeadlineHeading.copy(
color = OnBoardingTextPrimaryColor
)
style = HeadlineTitleSemibold,
color = colorResource(id = R.color.text_white)
)
}
}
@Composable
fun SetProfileNameInput(
text: MutableState<String>,
onKeyboardActionDoneClicked: () -> Unit
) {
Box(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight(),
contentAlignment = Alignment.Center
) {
val focus = LocalFocusManager.current
val focusRequester = FocusRequester()
OnboardingInput(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.focusRequester(focusRequester)
,
text = text,
placeholder = stringResource(id = R.string.onboarding_your_name),
keyboardActions = KeyboardActions(
onDone = {
focus.clearFocus()
onKeyboardActionDoneClicked()
}
)
)
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}
@Composable
fun SetProfileNameDescription() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 48.dp)
.padding(horizontal = 20.dp)
.wrapContentHeight(),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(id = R.string.onboarding_soul_creation_description),
style = HeadlineOnBoardingDescription.copy(
color = OnBoardingTextSecondaryColor,
textAlign = TextAlign.Center
)
style = UXBody,
color = colorResource(id = R.color.text_white),
textAlign = TextAlign.Center
)
}
}
@Composable
fun SetProfileNameNextButton(
modifier: Modifier,
onNextClicked: (Name) -> Unit,
text: MutableState<String>,
isLoading: Boolean
) {
val focus = LocalFocusManager.current
Box(modifier = modifier) {
OnBoardingButtonPrimary(
text = stringResource(id = R.string.done),
onClick = {
onNextClicked(text.value).also {
focus.clearFocus(force = true)
}
},
size = ButtonSize.Large,
modifier = modifier,
isLoading = isLoading
)
}
}
@Preview
@Preview(
showBackground = true,
backgroundColor = 0xFF000000,
showSystemUi = true,
device = PIXEL_7
)
@Composable
private fun SetProfileNameScreenPreview() {
SetProfileNameScreen(

View file

@ -76,7 +76,7 @@ fun OnBoardingButtonPrimary(
Box() {
Text(
text = if (isLoading) "" else text,
style = size.textStyle
style = ButtonMedium
)
if (isLoading) {
val loadingAlpha by animateFloatAsState(targetValue = 1f)

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="18dp"
android:height="18dp"
android:viewportWidth="18"
android:viewportHeight="18">
<path
android:pathData="M9,17.474C7.832,17.474 6.737,17.252 5.713,16.81C4.689,16.372 3.79,15.766 3.015,14.992C2.24,14.211 1.632,13.312 1.189,12.294C0.752,11.27 0.533,10.175 0.533,9.007C0.533,7.839 0.752,6.743 1.189,5.72C1.632,4.696 2.24,3.797 3.015,3.022C3.79,2.247 4.689,1.641 5.713,1.204C6.737,0.761 7.832,0.54 9,0.54C10.168,0.54 11.263,0.761 12.287,1.204C13.311,1.641 14.21,2.247 14.985,3.022C15.76,3.797 16.366,4.696 16.803,5.72C17.245,6.743 17.467,7.839 17.467,9.007C17.467,10.175 17.245,11.27 16.803,12.294C16.366,13.312 15.76,14.211 14.985,14.992C14.21,15.766 13.311,16.372 12.287,16.81C11.263,17.252 10.168,17.474 9,17.474ZM4.949,9.015C4.949,9.225 5.018,9.397 5.157,9.53C5.295,9.657 5.472,9.721 5.688,9.721H8.286V12.327C8.286,12.543 8.35,12.72 8.477,12.858C8.61,12.991 8.781,13.058 8.992,13.058C9.208,13.058 9.382,12.991 9.515,12.858C9.653,12.72 9.722,12.543 9.722,12.327V9.721H12.329C12.539,9.721 12.713,9.657 12.852,9.53C12.99,9.397 13.059,9.225 13.059,9.015C13.059,8.799 12.99,8.625 12.852,8.492C12.719,8.354 12.544,8.285 12.329,8.285H9.722V5.687C9.722,5.471 9.653,5.294 9.515,5.155C9.382,5.017 9.208,4.948 8.992,4.948C8.781,4.948 8.61,5.017 8.477,5.155C8.35,5.294 8.286,5.471 8.286,5.687V8.285H5.688C5.472,8.285 5.295,8.354 5.157,8.492C5.018,8.625 4.949,8.799 4.949,9.015Z"
android:fillColor="@color/glyph_active"/>
</vector>

View file

@ -1111,24 +1111,24 @@
<!--region ONBOARDING -->
<string name="onboarding_auth_title">the everything app</string>
<string name="onboarding_auth_subtitle">for those\nwho celebrate\ntrust &amp; autonomy</string>
<string name="onboarding_auth_subtitle">Encrypted, offline\n&amp; open</string>
<string name="onboarding_auth_description">Create &amp; collaborate in spaces you own. Encrypted, offline &amp; open.</string>
<string name="onboarding_terms_and_policy_prefix">"By continuing you agree to "</string>
<string name="onboarding_terms_and_policy_terms">Terms\u00A0of\u00A0Use</string>
<string name="onboarding_terms_and_policy_infix">" and "</string>
<string name="onboarding_terms_and_policy_privacy">Privacy\u00A0Policy</string>
<string name="onboarding_new_vault_button_text">I am new here</string>
<string name="onboarding_enter_my_vault">Enter my Vault</string>
<string name="onboarding_have_key_button_text">Already have a Key</string>
<string name="onboarding_enter_my_vault">Next</string>
<string name="onboarding_have_key_button_text">I already have a key</string>
<string name="onboarding_get_my_key_button_text">Get my Key</string>
<string name="onboarding_invite_code_title">Enter your invite code</string>
<string name="onboarding_invite_code_description">If you don\'t have one just go to anytype.io\nand sign up to the waiting list.</string>
<string name="onboarding_void_title">This is your Void</string>
<string name="onboarding_void_description">It is an encrypted location for everything you create. Everything is stored on your device, and backed up to the distributed network.</string>
<string name="onboarding_this_is_your_key_title">This is your Key</string>
<string name="onboarding_key_description">It gives you full ownership over your vault.</string>
<string name="onboarding_key_description">It replaces login and password. Keep it safe — you control your data. You can find this Key later in app settings.</string>
<string name="onboarding_show_my_key">Show my Key</string>
<string name="onboarding_tap_to_reveal">Tap to Reveal</string>
<string name="onboarding_tap_to_reveal">Reveal and copy</string>
<string name="onboarding_key_skip">Skip</string>
<string name="onboarding_key_not_now">Not now</string>
<string name="onboarding_key_copy">Copy to clipboard</string>
@ -1138,9 +1138,9 @@
<string name="onboarding_login_or">OR</string>
<string name="onboarding_entering_void_title">Entering the Void</string>
<string name="onboarding_your_key_can_t_be_empty">Your Key can\'t be empty</string>
<string name="onboarding_your_name">Your name</string>
<string name="onboarding_your_name">Type your name</string>
<string name="onboarding_mnemonic_read_more">Read more about Key</string>
<string name="onboarding_mnemonic_read_more">Read more</string>
<string name="onboarding_key_additional_info">You can find this key later in app settings</string>
<string name="onboarding_what_is_the_key">What is the Key?</string>
<string name="onboarding_recovery_phrase_description">It is represented by a recovery phrase 12 random words from which your vault is magically generated on this device.</string>
@ -2050,5 +2050,6 @@ Please provide specific details of your needs here.</string>
<string name="onboarding_button_continue">Continue</string>
<string name="onboarding_button_skip">Skip</string>
<string name="onboarding_email_error">Incorrect email</string>
<string name="onboarding_name_error">Enter name</string>
</resources>

View file

@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.NetworkModeConfig
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.auth.interactor.GetMnemonic
import com.anytypeio.anytype.domain.config.ConfigStorage
@ -57,24 +58,35 @@ class OnboardingMnemonicViewModel @Inject constructor(
type = EventsDictionary.ClickOnboardingButton.CHECK_LATER,
step = EventsDictionary.ScreenOnboardingStep.PHRASE
)
viewModelScope.launch {
val config = configStorage.getOrNull()
if (config != null) {
analytics.sendOpenAccountEvent(
analytics = config.analytics
)
} else {
Timber.w("config was missing before the end of onboarding")
}
if (!startingObject.isNullOrEmpty()) {
if (shouldShowEmail()) {
viewModelScope.launch {
commands.emit(
Command.OpenStartingObject(
space = SpaceId(space),
startingObject = startingObject
Command.NavigateToAddEmailScreen(
startingObject = startingObject,
space = space
)
)
} else {
commands.emit(Command.OpenVault)
}
} else {
viewModelScope.launch {
val config = configStorage.getOrNull()
if (config != null) {
analytics.sendOpenAccountEvent(
analytics = config.analytics
)
} else {
Timber.w("config was missing before the end of onboarding")
}
if (!startingObject.isNullOrEmpty()) {
commands.emit(
Command.OpenStartingObject(
space = SpaceId(space),
startingObject = startingObject
)
)
} else {
commands.emit(Command.OpenVault)
}
}
}
}
@ -83,28 +95,44 @@ class OnboardingMnemonicViewModel @Inject constructor(
space: Id,
startingObject: Id?,
) {
viewModelScope.launch {
val config = configStorage.getOrNull()
if (config != null) {
analytics.sendOpenAccountEvent(
analytics = config.analytics
)
} else {
Timber.w("config was missing before the end of onboarding")
}
if (!startingObject.isNullOrEmpty()) {
if (shouldShowEmail()) {
viewModelScope.launch {
commands.emit(
Command.OpenStartingObject(
space = SpaceId(space),
startingObject = startingObject
Command.NavigateToAddEmailScreen(
startingObject = startingObject,
space = space
)
)
} else {
commands.emit(Command.OpenVault)
}
} else {
viewModelScope.launch {
val config = configStorage.getOrNull()
if (config != null) {
analytics.sendOpenAccountEvent(
analytics = config.analytics
)
} else {
Timber.w("config was missing before the end of onboarding")
}
if (!startingObject.isNullOrEmpty()) {
commands.emit(
Command.OpenStartingObject(
space = SpaceId(space),
startingObject = startingObject
)
)
} else {
commands.emit(Command.OpenVault)
}
}
}
}
private fun shouldShowEmail(): Boolean {
//todo: update with Local Only config
return true
}
private suspend fun proceedWithMnemonicPhrase() {
getMnemonic.invoke(Unit).proceed(
failure = { e -> Timber.e(e, "Error while getting mnemonic") },
@ -145,5 +173,9 @@ class OnboardingMnemonicViewModel @Inject constructor(
val space: SpaceId,
val startingObject: Id
) : Command()
data class NavigateToAddEmailScreen(
val startingObject: String?,
val space: String
) : Command()
}
}

View file

@ -28,6 +28,8 @@ import com.anytypeio.anytype.presentation.BuildConfig
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.extension.proceedWithAccountEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsOnboardingScreenEvent
import com.anytypeio.anytype.presentation.extension.sendOpenAccountEvent
import com.anytypeio.anytype.presentation.onboarding.signup.OnboardingSetProfileNameViewModel.Navigation.OpenStartingObject
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import javax.inject.Inject
import kotlinx.coroutines.delay
@ -71,11 +73,7 @@ class OnboardingSetProfileNameViewModel @Inject constructor(
) {
if (state.value !is ScreenState.Loading) {
viewModelScope.launch {
navigation.emit(
Navigation.NavigateToAddEmailScreen(
name = name
)
)
proceedWithCreatingWallet(name)
}
} else {
sendToast(LOADING_MSG)
@ -256,27 +254,54 @@ class OnboardingSetProfileNameViewModel @Inject constructor(
//region Email screen
fun onEmailContinueClicked(
email: String,
name: String,
space: Id,
startingObject: String?
) {
proceedWithSettingEmail(email = email)
if (state.value !is ScreenState.Loading) {
proceedWithCreatingWallet(
name = name
)
} else {
if (state.value is ScreenState.Loading) {
sendToast(LOADING_MSG)
return
}
state.value = ScreenState.Loading
proceedWithSettingEmail(email = email)
proceedWithNavigation(space, startingObject)
}
fun onEmailSkippedClicked(
name: String
space: Id,
startingObject: String?
) {
if (state.value !is ScreenState.Loading) {
proceedWithCreatingWallet(
name = name
if (state.value is ScreenState.Loading) {
sendToast(LOADING_MSG)
return
}
state.value = ScreenState.Loading
proceedWithNavigation(space, startingObject)
}
private fun proceedWithNavigation(space: Id, startingObject: String?) {
viewModelScope.launch {
sendOpenAccountAnalytics()
if (!startingObject.isNullOrEmpty()) {
navigation.emit(
OpenStartingObject(
space = SpaceId(space),
startingObject = startingObject
)
)
} else {
navigation.emit(Navigation.OpenVault)
}
}
}
private suspend fun sendOpenAccountAnalytics() {
val config = configStorage.getOrNull()
if (config != null) {
analytics.sendOpenAccountEvent(
analytics = config.analytics
)
} else {
sendToast(LOADING_MSG)
Timber.w("config was missing before the end of onboarding")
}
}
@ -288,12 +313,12 @@ class OnboardingSetProfileNameViewModel @Inject constructor(
)
viewModelScope.launch {
setMembershipEmail.async(params).fold(
onSuccess = { Timber.d("Email set") },
onSuccess = { Timber.d("Email set successfully") },
onFailure = { error ->
Timber.e(error, "Error setting email")
if (BuildConfig.DEBUG) {
sendToast("Error setting email")
sendToast("Error setting email: ${error.message}")
}
Timber.e("Error setting email: $error")
}
)
}
@ -348,10 +373,12 @@ class OnboardingSetProfileNameViewModel @Inject constructor(
sealed class Navigation {
data class NavigateToMnemonic(val space: SpaceId, val startingObject: Id?): Navigation()
data class NavigateToAddEmailScreen(
val name: String
) : Navigation()
data object GoBack: Navigation()
data class OpenStartingObject(
val space: SpaceId,
val startingObject: Id
) : Navigation()
data object OpenVault : Navigation()
}
sealed class ScreenState {