Enhance OnboardingScreen with smoother pager swipes and add pulse animation to Rosetta logo

- Implemented custom fling behavior for HorizontalPager to improve swipe experience.
- Added pulse animation effect to the Rosetta logo on the onboarding screen for better visual appeal.
- Updated the layout and animation specifications to enhance user interaction and aesthetics.
This commit is contained in:
k1ngsterr1
2026-01-08 20:04:51 +05:00
parent fc54cc89df
commit 307670e691
10 changed files with 1740 additions and 403 deletions

View File

@@ -6,6 +6,7 @@ import androidx.compose.runtime.*
import com.rosetta.messenger.data.DecryptedAccount
enum class AuthScreen {
SELECT_ACCOUNT,
WELCOME,
SEED_PHRASE,
CONFIRM_SEED,
@@ -18,12 +19,22 @@ enum class AuthScreen {
fun AuthFlow(
isDarkTheme: Boolean,
hasExistingAccount: Boolean,
onAuthComplete: (DecryptedAccount?) -> Unit
accounts: List<AccountInfo> = emptyList(),
onAuthComplete: (DecryptedAccount?) -> Unit,
onLogout: () -> Unit = {}
) {
var currentScreen by remember {
mutableStateOf(if (hasExistingAccount) AuthScreen.UNLOCK else AuthScreen.WELCOME)
mutableStateOf(
when {
hasExistingAccount && accounts.size > 1 -> AuthScreen.SELECT_ACCOUNT
hasExistingAccount -> AuthScreen.UNLOCK
else -> AuthScreen.WELCOME
}
)
}
var seedPhrase by remember { mutableStateOf<List<String>>(emptyList()) }
var selectedAccountId by remember { mutableStateOf<String?>(accounts.firstOrNull()?.id) }
var showCreateModal by remember { mutableStateOf(false) }
AnimatedContent(
targetState = currentScreen,
@@ -34,6 +45,29 @@ fun AuthFlow(
label = "authScreenTransition"
) { screen ->
when (screen) {
AuthScreen.SELECT_ACCOUNT -> {
SelectAccountScreen(
isDarkTheme = isDarkTheme,
accounts = accounts,
selectedAccountId = selectedAccountId,
onSelectAccount = { accountId ->
selectedAccountId = accountId
currentScreen = AuthScreen.UNLOCK
},
onAddAccount = { showCreateModal = true },
showCreateModal = showCreateModal,
onCreateNew = {
showCreateModal = false
currentScreen = AuthScreen.SEED_PHRASE
},
onImportSeed = {
showCreateModal = false
currentScreen = AuthScreen.IMPORT_SEED
},
onDismissModal = { showCreateModal = false }
)
}
AuthScreen.WELCOME -> {
WelcomeScreen(
isDarkTheme = isDarkTheme,
@@ -45,7 +79,11 @@ fun AuthFlow(
AuthScreen.SEED_PHRASE -> {
SeedPhraseScreen(
isDarkTheme = isDarkTheme,
onBack = { currentScreen = AuthScreen.WELCOME },
onBack = {
currentScreen = if (hasExistingAccount && accounts.size > 1)
AuthScreen.SELECT_ACCOUNT
else AuthScreen.WELCOME
},
onConfirm = { words ->
seedPhrase = words
currentScreen = AuthScreen.CONFIRM_SEED
@@ -74,7 +112,11 @@ fun AuthFlow(
AuthScreen.IMPORT_SEED -> {
ImportSeedPhraseScreen(
isDarkTheme = isDarkTheme,
onBack = { currentScreen = AuthScreen.WELCOME },
onBack = {
currentScreen = if (hasExistingAccount && accounts.size > 1)
AuthScreen.SELECT_ACCOUNT
else AuthScreen.WELCOME
},
onSeedPhraseImported = { words ->
seedPhrase = words
currentScreen = AuthScreen.SET_PASSWORD
@@ -85,7 +127,13 @@ fun AuthFlow(
AuthScreen.UNLOCK -> {
UnlockScreen(
isDarkTheme = isDarkTheme,
onUnlocked = { account -> onAuthComplete(account) }
selectedAccountId = selectedAccountId,
onUnlocked = { account -> onAuthComplete(account) },
onSwitchAccount = {
if (accounts.size > 1) {
currentScreen = AuthScreen.SELECT_ACCOUNT
}
}
)
}
}

View File

@@ -4,12 +4,8 @@ import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
@@ -17,19 +13,33 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.crypto.CryptoManager
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
// Beautiful solid colors that fit the theme
private val wordColors = listOf(
Color(0xFF5E9FFF), // Soft blue
Color(0xFFFF7EB3), // Soft pink
Color(0xFF7B68EE), // Medium purple
Color(0xFF50C878), // Emerald green
Color(0xFFFF6B6B), // Coral red
Color(0xFF4ECDC4), // Teal
Color(0xFFFFB347), // Pastel orange
Color(0xFFBA55D3), // Medium orchid
Color(0xFF87CEEB), // Sky blue
Color(0xFFDDA0DD), // Plum
Color(0xFF98D8C8), // Mint
Color(0xFFF7DC6F) // Soft yellow
)
@Composable
fun ConfirmSeedPhraseScreen(
seedPhrase: List<String>,
@@ -37,18 +47,16 @@ fun ConfirmSeedPhraseScreen(
onBack: () -> Unit,
onConfirmed: () -> Unit
) {
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec)
val textColor by animateColorAsState(if (isDarkTheme) Color.White else Color.Black, animationSpec = themeAnimSpec)
val secondaryTextColor by animateColorAsState(if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), animationSpec = themeAnimSpec)
val cardColor by animateColorAsState(if (isDarkTheme) AuthSurface else AuthSurfaceLight, animationSpec = themeAnimSpec)
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
// Select 4 random words to confirm
// Select 4 words at fixed positions to confirm (2, 5, 9, 12)
val wordsToConfirm = remember {
listOf(1, 4, 8, 11).map { index -> index to seedPhrase[index] }
}
var userInputs by remember { mutableStateOf(List(4) { "" }) }
var userInputs by remember { mutableStateOf(List(12) { "" }) }
var showError by remember { mutableStateOf(false) }
var visible by remember { mutableStateOf(false) }
@@ -56,43 +64,33 @@ fun ConfirmSeedPhraseScreen(
visible = true
}
val allCorrect = wordsToConfirm.mapIndexed { i, (_, word) ->
userInputs[i].trim().lowercase() == word.lowercase()
}.all { it }
// Check if the 4 confirmation words are correct
val allCorrect = wordsToConfirm.all { (index, word) ->
userInputs[index].trim().lowercase() == word.lowercase()
}
// Check if all 4 words have input
val allFilled = wordsToConfirm.all { (index, _) ->
userInputs[index].isNotBlank()
}
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.statusBarsPadding()
) {
Column(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
) {
// Top Bar
Column(modifier = Modifier.fillMaxSize()) {
// Top bar
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp),
.padding(4.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Back",
tint = textColor
)
Icon(Icons.Default.ArrowBack, "Back", tint = textColor)
}
Spacer(modifier = Modifier.weight(1f))
Text(
text = "Confirm Backup",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = textColor
)
Spacer(modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.width(48.dp))
}
Column(
@@ -104,82 +102,127 @@ fun ConfirmSeedPhraseScreen(
) {
Spacer(modifier = Modifier.height(16.dp))
// Info Card
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500)) + slideInVertically(
initialOffsetY = { -30 },
initialOffsetY = { -20 },
animationSpec = tween(500)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(cardColor)
.padding(16.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = Icons.Default.CheckCircle,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Enter the following words from your seed phrase to confirm you've backed it up correctly.",
fontSize = 14.sp,
color = secondaryTextColor,
lineHeight = 18.sp
text = "Confirm Backup",
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
color = textColor
)
}
Spacer(modifier = Modifier.height(12.dp))
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 100)) + slideInVertically(
initialOffsetY = { -20 },
animationSpec = tween(500, delayMillis = 100)
)
) {
Text(
text = "Enter words #${wordsToConfirm[0].first + 1}, #${wordsToConfirm[1].first + 1}, #${wordsToConfirm[2].first + 1}, #${wordsToConfirm[3].first + 1}\nto confirm you've backed up your phrase.",
fontSize = 15.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center,
lineHeight = 22.sp
)
}
Spacer(modifier = Modifier.height(32.dp))
// Word inputs
wordsToConfirm.forEachIndexed { index, (wordIndex, _) ->
val isCorrect = userInputs[index].trim().lowercase() ==
wordsToConfirm[index].second.lowercase()
val hasInput = userInputs[index].isNotBlank()
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 100 + (index * 100))) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 100 + (index * 100))
)
// Two column layout like SeedPhraseScreen
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 200))
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
WordInputField(
wordNumber = wordIndex + 1,
value = userInputs[index],
onValueChange = {
userInputs = userInputs.toMutableList().apply {
this[index] = it
}
showError = false
},
isCorrect = if (hasInput) isCorrect else null,
isDarkTheme = isDarkTheme
)
// Left column (words 1-6)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
for (i in 0..5) {
val needsInput = wordsToConfirm.any { it.first == i }
val correctWord = seedPhrase[i]
AnimatedConfirmWordItem(
number = i + 1,
displayWord = if (needsInput) "" else correctWord,
inputValue = if (needsInput) userInputs[i] else "",
isInput = needsInput,
onValueChange = { newValue ->
userInputs = userInputs.toMutableList().apply {
this[i] = newValue.lowercase().trim()
}
showError = false
},
isCorrect = if (needsInput && userInputs[i].isNotBlank()) {
userInputs[i].trim().lowercase() == correctWord.lowercase()
} else null,
isDarkTheme = isDarkTheme,
visible = visible,
delay = 250 + (i * 50)
)
}
}
// Right column (words 7-12)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
for (i in 6..11) {
val needsInput = wordsToConfirm.any { it.first == i }
val correctWord = seedPhrase[i]
AnimatedConfirmWordItem(
number = i + 1,
displayWord = if (needsInput) "" else correctWord,
inputValue = if (needsInput) userInputs[i] else "",
isInput = needsInput,
onValueChange = { newValue ->
userInputs = userInputs.toMutableList().apply {
this[i] = newValue.lowercase().trim()
}
showError = false
},
isCorrect = if (needsInput && userInputs[i].isNotBlank()) {
userInputs[i].trim().lowercase() == correctWord.lowercase()
} else null,
isDarkTheme = isDarkTheme,
visible = visible,
delay = 250 + (i * 50)
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
// Error message
AnimatedVisibility(
visible = showError,
enter = fadeIn() + slideInVertically { -10 },
exit = fadeOut()
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f),
exit = fadeOut(tween(200))
) {
Text(
text = "Some words don't match. Please check and try again.",
fontSize = 14.sp,
color = Color(0xFFE53935),
textAlign = TextAlign.Center
)
Column {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Some words don't match. Please check and try again.",
fontSize = 14.sp,
color = Color(0xFFE53935),
textAlign = TextAlign.Center
)
}
}
Spacer(modifier = Modifier.weight(1f))
@@ -187,90 +230,208 @@ fun ConfirmSeedPhraseScreen(
// Continue Button
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 500)) + slideInVertically(
enter = fadeIn(tween(500, delayMillis = 600)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 500)
animationSpec = tween(500, delayMillis = 600)
)
) {
Button(
onClick = {
if (allCorrect) {
onConfirmed()
} else {
showError = true
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (allCorrect) PrimaryBlue else PrimaryBlue.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = "Confirm",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
}
onClick = {
if (allCorrect) {
onConfirmed()
} else {
showError = true
}
},
enabled = allFilled,
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White,
disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8),
disabledContentColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFF999999)
),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = "Confirm",
fontSize = 17.sp,
fontWeight = FontWeight.Medium
)
}
}
Spacer(modifier = Modifier.height(32.dp))
Spacer(modifier = Modifier.height(40.dp))
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun WordInputField(
wordNumber: Int,
private fun AnimatedConfirmWordItem(
number: Int,
displayWord: String,
inputValue: String,
isInput: Boolean,
onValueChange: (String) -> Unit,
isCorrect: Boolean?,
isDarkTheme: Boolean,
visible: Boolean,
delay: Int,
modifier: Modifier = Modifier
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = delay)) + slideInHorizontally(
initialOffsetX = { if (number <= 6) -50 else 50 },
animationSpec = tween(400, delayMillis = delay)
)
) {
if (isInput) {
ConfirmWordInputItem(
number = number,
value = inputValue,
onValueChange = onValueChange,
isCorrect = isCorrect,
isDarkTheme = isDarkTheme,
modifier = modifier
)
} else {
DisplayWordItem(
number = number,
word = displayWord,
isDarkTheme = isDarkTheme,
modifier = modifier
)
}
}
}
@Composable
private fun DisplayWordItem(
number: Int,
word: String,
isDarkTheme: Boolean,
modifier: Modifier = Modifier
) {
val numberColor = if (isDarkTheme) Color(0xFF888888) else Color(0xFF999999)
val bgColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
val wordColor = wordColors[(number - 1) % wordColors.size]
Box(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(bgColor)
.padding(horizontal = 16.dp, vertical = 14.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "$number.",
fontSize = 15.sp,
color = numberColor,
modifier = Modifier.width(28.dp)
)
Text(
text = word,
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = wordColor,
fontFamily = FontFamily.Monospace
)
}
}
}
@Composable
private fun ConfirmWordInputItem(
number: Int,
value: String,
onValueChange: (String) -> Unit,
isCorrect: Boolean?,
isDarkTheme: Boolean
isDarkTheme: Boolean,
modifier: Modifier = Modifier
) {
val textColor = if (isDarkTheme) Color.White else Color.Black
val labelColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val numberColor = if (isDarkTheme) Color(0xFF888888) else Color(0xFF999999)
val hintColor = if (isDarkTheme) Color(0xFF555555) else Color(0xFFBBBBBB)
val bgColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
val wordColor = wordColors[(number - 1) % wordColors.size]
var isFocused by remember { mutableStateOf(false) }
// Border color based on correctness
val borderColor = when (isCorrect) {
true -> Color(0xFF4CAF50)
false -> Color(0xFFE53935)
null -> if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0)
true -> Color(0xFF4CAF50) // Green
false -> Color(0xFFE53935) // Red
null -> if (isFocused) PrimaryBlue else Color.Transparent
}
val trailingIcon: @Composable (() -> Unit)? = when (isCorrect) {
true -> {
{ Icon(Icons.Default.Check, null, tint = Color(0xFF4CAF50)) }
Box(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.border(
width = 2.dp,
color = borderColor,
shape = RoundedCornerShape(12.dp)
)
.background(bgColor)
.padding(horizontal = 16.dp, vertical = 14.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "$number.",
fontSize = 15.sp,
color = numberColor,
modifier = Modifier.width(28.dp)
)
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = if (value.isNotEmpty()) wordColor else hintColor,
fontFamily = FontFamily.Monospace
),
singleLine = true,
cursorBrush = SolidColor(PrimaryBlue),
modifier = Modifier
.weight(1f)
.onFocusChanged { isFocused = it.isFocused },
decorationBox = { innerTextField ->
Box {
if (value.isEmpty()) {
Text(
"enter word",
fontSize = 17.sp,
color = hintColor,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.SemiBold
)
}
innerTextField()
}
}
)
// Show check/cross icon
if (isCorrect != null) {
Icon(
imageVector = if (isCorrect) Icons.Default.Check else Icons.Default.Close,
contentDescription = null,
tint = if (isCorrect) Color(0xFF4CAF50) else Color(0xFFE53935),
modifier = Modifier.size(20.dp)
)
}
}
false -> {
{ Icon(Icons.Default.Close, null, tint = Color(0xFFE53935)) }
}
null -> null
}
OutlinedTextField(
value = value,
onValueChange = { onValueChange(it.lowercase().trim()) },
label = { Text("Word #$wordNumber") },
placeholder = { Text("Enter word $wordNumber") },
singleLine = true,
trailingIcon = trailingIcon,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = borderColor,
focusedLabelColor = PrimaryBlue,
unfocusedLabelColor = labelColor,
cursorColor = PrimaryBlue,
focusedTextColor = textColor,
unfocusedTextColor = textColor
),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Next
)
)
}

View File

@@ -16,6 +16,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
@@ -35,12 +36,10 @@ fun ImportSeedPhraseScreen(
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val cardBackground = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
val clipboardManager = LocalClipboardManager.current
var words by remember { mutableStateOf(List(12) { "" }) }
var error by remember { mutableStateOf<String?>(null) }
var pastedText by remember { mutableStateOf("") }
var showPasteDialog by remember { mutableStateOf(false) }
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
@@ -49,18 +48,6 @@ fun ImportSeedPhraseScreen(
val allWordsFilled = words.all { it.isNotBlank() }
// Parse pasted text
LaunchedEffect(pastedText) {
if (pastedText.isNotBlank()) {
val parsed = pastedText.trim().lowercase().split("\\s+".toRegex())
if (parsed.size == 12) {
words = parsed
showPasteDialog = false
error = null
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
@@ -68,7 +55,7 @@ fun ImportSeedPhraseScreen(
.statusBarsPadding()
) {
Column(modifier = Modifier.fillMaxSize()) {
// Simple top bar
// Top bar
Row(
modifier = Modifier
.fillMaxWidth()
@@ -80,17 +67,13 @@ fun ImportSeedPhraseScreen(
}
}
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, easing = FastOutSlowInEasing))
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(16.dp))
AnimatedVisibility(
visible = visible,
@@ -111,98 +94,140 @@ fun ImportSeedPhraseScreen(
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 100))
enter = fadeIn(tween(500, delayMillis = 100)) + slideInVertically(
initialOffsetY = { -20 },
animationSpec = tween(500, delayMillis = 100)
)
) {
Text(
text = "Enter your 12-word recovery phrase",
text = "Enter your 12-word recovery phrase\nto restore your account.",
fontSize = 15.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center
textAlign = TextAlign.Center,
lineHeight = 22.sp
)
}
Spacer(modifier = Modifier.height(24.dp))
// Paste button
// Paste button - directly reads from clipboard
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 200)) + scaleIn(
enter = fadeIn(tween(500, delayMillis = 150)) + scaleIn(
initialScale = 0.9f,
animationSpec = tween(500, delayMillis = 200)
animationSpec = tween(500, delayMillis = 150)
)
) {
Button(
onClick = { showPasteDialog = true },
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White
),
shape = RoundedCornerShape(12.dp)
) {
Icon(
Icons.Default.ContentPaste,
contentDescription = null,
modifier = Modifier.size(22.dp)
)
Spacer(modifier = Modifier.width(10.dp))
Text(
"Paste All 12 Words",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
}
TextButton(
onClick = {
val clipboardText = clipboardManager.getText()?.text
if (clipboardText != null && clipboardText.isNotBlank()) {
val parsed = clipboardText.trim().lowercase()
.split(Regex("[\\s,;]+"))
.filter { it.isNotBlank() }
.map { it.trim() }
when {
parsed.size == 12 -> {
words = parsed.toList()
error = null
}
parsed.size > 12 -> {
words = parsed.take(12).toList()
error = null
}
parsed.isNotEmpty() -> {
error = "Clipboard contains ${parsed.size} words, need 12"
}
else -> {
error = "No valid words found in clipboard"
}
}
} else {
error = "Clipboard is empty"
}
}
) {
Icon(
Icons.Default.ContentPaste,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
"Paste all 12 words",
color = PrimaryBlue,
fontSize = 15.sp
)
}
}
Spacer(modifier = Modifier.height(24.dp))
Spacer(modifier = Modifier.height(20.dp))
// Clean grid
// Two column layout like SeedPhraseScreen
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 300))
enter = fadeIn(tween(500, delayMillis = 200))
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(cardBackground)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
for (row in 0..3) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
// Left column (words 1-6)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
for (col in 0..2) {
val index = row * 3 + col
WordInputItem(
number = index + 1,
value = words[index],
for (i in 0..5) {
AnimatedWordInputItem(
number = i + 1,
value = words[i],
onValueChange = { newValue ->
words = words.toMutableList().apply {
this[index] = newValue.lowercase().trim()
this[i] = newValue.lowercase().trim()
}
error = null
},
isDarkTheme = isDarkTheme,
modifier = Modifier.weight(1f)
visible = visible,
delay = 250 + (i * 50)
)
}
}
// Right column (words 7-12)
Column(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
for (i in 6..11) {
AnimatedWordInputItem(
number = i + 1,
value = words[i],
onValueChange = { newValue ->
words = words.toMutableList().apply {
this[i] = newValue.lowercase().trim()
}
error = null
},
isDarkTheme = isDarkTheme,
visible = visible,
delay = 250 + (i * 50)
)
}
}
}
}
}
// Error
if (error != null) {
Spacer(modifier = Modifier.height(12.dp))
AnimatedVisibility(
visible = true,
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f)
) {
AnimatedVisibility(
visible = error != null,
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f),
exit = fadeOut(tween(200))
) {
Column {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = error ?: "",
fontSize = 14.sp,
@@ -217,95 +242,80 @@ fun ImportSeedPhraseScreen(
// Import button
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 400)) + slideInVertically(
enter = fadeIn(tween(500, delayMillis = 600)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 400)
animationSpec = tween(500, delayMillis = 600)
)
) {
Button(
onClick = {
val seedPhrase = words.map { it.trim() }
if (seedPhrase.any { it.isBlank() }) {
error = "Please fill in all words"
return@Button
}
if (!CryptoManager.validateSeedPhrase(seedPhrase)) {
error = "Invalid recovery phrase"
return@Button
}
onSeedPhraseImported(seedPhrase)
},
enabled = allWordsFilled,
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White,
disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8),
disabledContentColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFF999999)
),
shape = RoundedCornerShape(12.dp)
) {
Text("Continue", fontSize = 17.sp, fontWeight = FontWeight.Medium)
}
}
Spacer(modifier = Modifier.height(40.dp))
}
}
}
// Paste dialog
if (showPasteDialog) {
AlertDialog(
onDismissRequest = { showPasteDialog = false },
title = {
Text("Paste Recovery Phrase", fontWeight = FontWeight.Bold)
},
text = {
OutlinedTextField(
value = pastedText,
onValueChange = { pastedText = it },
placeholder = { Text("Paste your 12 words here") },
onClick = {
val seedPhrase = words.map { it.trim() }
if (seedPhrase.any { it.isBlank() }) {
error = "Please fill in all words"
return@Button
}
if (!CryptoManager.validateSeedPhrase(seedPhrase)) {
error = "Invalid recovery phrase"
return@Button
}
onSeedPhraseImported(seedPhrase)
},
enabled = allWordsFilled,
modifier = Modifier
.fillMaxWidth()
.height(120.dp),
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
cursorColor = PrimaryBlue
.height(50.dp),
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White,
disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8),
disabledContentColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFF999999)
),
shape = RoundedCornerShape(12.dp)
)
},
confirmButton = {
TextButton(
onClick = {
val parsed = pastedText.trim().lowercase().split("\\s+".toRegex())
if (parsed.size == 12) {
words = parsed
showPasteDialog = false
}
}
) {
Text("Import", color = PrimaryBlue, fontWeight = FontWeight.Medium)
Text(
text = "Continue",
fontSize = 17.sp,
fontWeight = FontWeight.Medium
)
}
},
dismissButton = {
TextButton(onClick = { showPasteDialog = false }) {
Text("Cancel")
}
},
containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color.White,
shape = RoundedCornerShape(20.dp)
)
}
Spacer(modifier = Modifier.height(40.dp))
}
}
}
}
@Composable
private fun AnimatedWordInputItem(
number: Int,
value: String,
onValueChange: (String) -> Unit,
isDarkTheme: Boolean,
visible: Boolean,
delay: Int,
modifier: Modifier = Modifier
) {
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = delay)) + slideInHorizontally(
initialOffsetX = { if (number <= 6) -50 else 50 },
animationSpec = tween(400, delayMillis = delay)
)
) {
WordInputItem(
number = number,
value = value,
onValueChange = onValueChange,
isDarkTheme = isDarkTheme,
modifier = modifier
)
}
}
@Composable
private fun WordInputItem(
number: Int,
@@ -314,55 +324,81 @@ private fun WordInputItem(
isDarkTheme: Boolean,
modifier: Modifier = Modifier
) {
val itemBg = if (isDarkTheme) Color(0xFF1E1E1E) else Color.White
val numberColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFFAAAAAA)
val textColor = if (isDarkTheme) Color.White else Color.Black
val hintColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFFAAAAAA)
val numberColor = if (isDarkTheme) Color(0xFF888888) else Color(0xFF999999)
val hintColor = if (isDarkTheme) Color(0xFF555555) else Color(0xFFBBBBBB)
val bgColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
var isFocused by remember { mutableStateOf(false) }
val borderColor = if (isFocused) PrimaryBlue else Color.Transparent
Row(
// Beautiful solid colors that fit the theme - same as SeedPhraseScreen
val wordColors = listOf(
Color(0xFF5E9FFF), // Soft blue
Color(0xFFFF7EB3), // Soft pink
Color(0xFF7B68EE), // Medium purple
Color(0xFF50C878), // Emerald green
Color(0xFFFF6B6B), // Coral red
Color(0xFF4ECDC4), // Teal
Color(0xFFFFB347), // Pastel orange
Color(0xFFBA55D3), // Medium orchid
Color(0xFF87CEEB), // Sky blue
Color(0xFFDDA0DD), // Plum
Color(0xFF98D8C8), // Mint
Color(0xFFF7DC6F) // Soft yellow
)
val wordColor = wordColors[(number - 1) % wordColors.size]
Box(
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.border(1.dp, borderColor, RoundedCornerShape(8.dp))
.background(itemBg)
.padding(horizontal = 10.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.border(
width = if (isFocused) 2.dp else 0.dp,
color = if (isFocused) PrimaryBlue else Color.Transparent,
shape = RoundedCornerShape(12.dp)
)
.background(bgColor)
.padding(horizontal = 16.dp, vertical = 14.dp)
) {
Text(
text = "$number.",
fontSize = 13.sp,
color = numberColor,
modifier = Modifier.width(22.dp)
)
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = TextStyle(
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "$number.",
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = textColor,
fontFamily = FontFamily.Monospace
),
singleLine = true,
cursorBrush = SolidColor(PrimaryBlue),
modifier = Modifier
.weight(1f)
.onFocusChanged { isFocused = it.isFocused },
decorationBox = { innerTextField ->
Box {
if (value.isEmpty()) {
Text(
"word",
fontSize = 15.sp,
color = hintColor.copy(alpha = 0.5f),
fontFamily = FontFamily.Monospace
)
color = numberColor,
modifier = Modifier.width(28.dp)
)
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = TextStyle(
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = if (value.isNotEmpty()) wordColor else hintColor,
fontFamily = FontFamily.Monospace
),
singleLine = true,
cursorBrush = SolidColor(PrimaryBlue),
modifier = Modifier
.weight(1f)
.onFocusChanged { isFocused = it.isFocused },
decorationBox = { innerTextField ->
Box {
if (value.isEmpty()) {
Text(
"word",
fontSize = 17.sp,
color = hintColor,
fontFamily = FontFamily.Monospace,
fontWeight = FontWeight.SemiBold
)
}
innerTextField()
}
innerTextField()
}
}
)
)
}
}
}

View File

@@ -12,6 +12,7 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
@@ -248,31 +249,52 @@ private fun WordItem(
isDarkTheme: Boolean,
modifier: Modifier = Modifier
) {
val itemBg = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
val numberColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFFAAAAAA)
val wordColor = if (isDarkTheme) Color.White else Color.Black
val numberColor = if (isDarkTheme) Color(0xFF888888) else Color(0xFF999999)
Row(
// Beautiful solid colors that fit the theme
val wordColors = listOf(
Color(0xFF5E9FFF), // Soft blue
Color(0xFFFF7EB3), // Soft pink
Color(0xFF7B68EE), // Medium purple
Color(0xFF50C878), // Emerald green
Color(0xFFFF6B6B), // Coral red
Color(0xFF4ECDC4), // Teal
Color(0xFFFFB347), // Pastel orange
Color(0xFFBA55D3), // Medium orchid
Color(0xFF87CEEB), // Sky blue
Color(0xFFDDA0DD), // Plum
Color(0xFF98D8C8), // Mint
Color(0xFFF7DC6F) // Soft yellow
)
val wordColor = wordColors[(number - 1) % wordColors.size]
val bgColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
Box(
modifier = modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(itemBg)
.padding(horizontal = 16.dp, vertical = 14.dp),
verticalAlignment = Alignment.CenterVertically
.background(bgColor)
.padding(horizontal = 16.dp, vertical = 14.dp)
) {
Text(
text = "$number.",
fontSize = 15.sp,
color = numberColor,
modifier = Modifier.width(28.dp)
)
Text(
text = word,
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = wordColor,
fontFamily = FontFamily.Monospace
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
Text(
text = "$number.",
fontSize = 15.sp,
color = numberColor,
modifier = Modifier.width(28.dp)
)
Text(
text = word,
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = wordColor,
fontFamily = FontFamily.Monospace
)
}
}
}

View File

@@ -0,0 +1,447 @@
package com.rosetta.messenger.ui.auth
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
data class AccountInfo(
val id: String,
val name: String,
val initials: String,
val publicKey: String
)
// Avatar colors for accounts
private val accountColors = listOf(
Color(0xFF5E9FFF), // Blue
Color(0xFFFF7EB3), // Pink
Color(0xFF7B68EE), // Purple
Color(0xFF50C878), // Green
Color(0xFFFF6B6B), // Red
Color(0xFF4ECDC4), // Teal
Color(0xFFFFB347), // Orange
Color(0xFFBA55D3) // Orchid
)
fun getAccountColor(name: String): Color {
val index = name.hashCode().mod(accountColors.size).let { if (it < 0) it + accountColors.size else it }
return accountColors[index]
}
@Composable
fun SelectAccountScreen(
isDarkTheme: Boolean,
accounts: List<AccountInfo>,
selectedAccountId: String?,
onSelectAccount: (String) -> Unit,
onAddAccount: () -> Unit,
showCreateModal: Boolean,
onCreateNew: () -> Unit,
onImportSeed: () -> Unit,
onDismissModal: () -> Unit
) {
val backgroundColor = if (isDarkTheme) Color(0xFF1E1E1E) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
visible = true
}
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
.statusBarsPadding()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 32.dp)
) {
Spacer(modifier = Modifier.height(60.dp))
// Header
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500)) + slideInVertically(
initialOffsetY = { -30 },
animationSpec = tween(500)
)
) {
Column {
Row {
Text(
text = "Select ",
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = PrimaryBlue
)
Text(
text = "account",
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = textColor
)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Select your account for login,\nor add new account",
fontSize = 15.sp,
color = secondaryTextColor,
lineHeight = 22.sp
)
}
}
Spacer(modifier = Modifier.height(40.dp))
// Accounts grid
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 200))
) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier.fillMaxWidth()
) {
items(accounts, key = { it.id }) { account ->
val index = accounts.indexOf(account)
AccountCard(
account = account,
isSelected = account.id == selectedAccountId,
isDarkTheme = isDarkTheme,
onClick = { onSelectAccount(account.id) },
animationDelay = 250 + (index * 100)
)
}
// Add Account card
item {
AddAccountCard(
isDarkTheme = isDarkTheme,
onClick = onAddAccount,
animationDelay = 250 + (accounts.size * 100)
)
}
}
}
}
// Create Account Modal
if (showCreateModal) {
CreateAccountModal(
isDarkTheme = isDarkTheme,
onCreateNew = onCreateNew,
onImportSeed = onImportSeed,
onDismiss = onDismissModal
)
}
}
}
@Composable
private fun AccountCard(
account: AccountInfo,
isSelected: Boolean,
isDarkTheme: Boolean,
onClick: () -> Unit,
animationDelay: Int
) {
val surfaceColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
val borderColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
val avatarColor = getAccountColor(account.name)
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
kotlinx.coroutines.delay(animationDelay.toLong())
visible = true
}
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400)) + scaleIn(
initialScale = 0.9f,
animationSpec = tween(400)
)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.85f)
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = if (isSelected) PrimaryBlue.copy(alpha = 0.1f) else surfaceColor
),
border = BorderStroke(
width = 2.dp,
color = if (isSelected) PrimaryBlue else borderColor
)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
// Checkmark
if (isSelected) {
Box(
modifier = Modifier
.align(Alignment.TopStart)
.padding(12.dp)
.size(24.dp)
.clip(CircleShape)
.background(PrimaryBlue),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Check,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(14.dp)
)
}
} else {
Box(
modifier = Modifier
.align(Alignment.TopStart)
.padding(12.dp)
.size(24.dp)
.clip(CircleShape)
.border(2.dp, borderColor, CircleShape)
)
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(20.dp)
) {
// Avatar
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(avatarColor.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
Text(
text = account.initials,
fontSize = 28.sp,
fontWeight = FontWeight.SemiBold,
color = avatarColor
)
}
Spacer(modifier = Modifier.height(16.dp))
// Name
Text(
text = account.name,
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = if (isSelected) PrimaryBlue else textColor,
textAlign = TextAlign.Center,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
}
@Composable
private fun AddAccountCard(
isDarkTheme: Boolean,
onClick: () -> Unit,
animationDelay: Int
) {
val surfaceColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5)
val borderColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE0E0E0)
val textColor = if (isDarkTheme) Color.White else Color.Black
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
kotlinx.coroutines.delay(animationDelay.toLong())
visible = true
}
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400)) + scaleIn(
initialScale = 0.9f,
animationSpec = tween(400)
)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.85f)
.clickable(onClick = onClick),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = surfaceColor),
border = BorderStroke(
width = 2.dp,
color = borderColor,
)
) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(20.dp)
) {
// Plus icon
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.border(2.dp, PrimaryBlue, CircleShape),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Add,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(40.dp)
)
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Add Account",
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = PrimaryBlue,
textAlign = TextAlign.Center
)
}
}
}
}
}
@Composable
private fun CreateAccountModal(
isDarkTheme: Boolean,
onCreateNew: () -> Unit,
onImportSeed: () -> Unit,
onDismiss: () -> Unit
) {
val backgroundColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFFFFFFF)
val textColor = if (isDarkTheme) Color.White else Color.Black
val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666)
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.5f))
.clickable(onClick = onDismiss),
contentAlignment = Alignment.Center
) {
Card(
modifier = Modifier
.padding(32.dp)
.clickable(enabled = false, onClick = {}),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(containerColor = backgroundColor)
) {
Column(
modifier = Modifier.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = "Create account",
fontSize = 20.sp,
fontWeight = FontWeight.Bold,
color = textColor
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "You may be create new account\nor import existing",
fontSize = 15.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(24.dp))
// Create New button
Button(
onClick = onCreateNew,
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White
),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = "Create new",
fontSize = 16.sp,
fontWeight = FontWeight.Medium
)
}
Spacer(modifier = Modifier.height(12.dp))
// Import (Recover) button
OutlinedButton(
onClick = onImportSeed,
modifier = Modifier
.fillMaxWidth()
.height(50.dp),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = PrimaryBlue
),
border = BorderStroke(1.dp, PrimaryBlue),
shape = RoundedCornerShape(12.dp)
) {
Text(
text = "Recover",
fontSize = 16.sp,
fontWeight = FontWeight.Medium
)
}
}
}
}
}

View File

@@ -37,7 +37,9 @@ import kotlinx.coroutines.launch
@Composable
fun UnlockScreen(
isDarkTheme: Boolean,
onUnlocked: (DecryptedAccount) -> Unit
selectedAccountId: String? = null,
onUnlocked: (DecryptedAccount) -> Unit,
onSwitchAccount: () -> Unit = {}
) {
val themeAnimSpec = tween<Color>(durationMillis = 500, easing = CubicBezierEasing(0.4f, 0f, 0.2f, 1f))
val backgroundColor by animateColorAsState(if (isDarkTheme) AuthBackground else AuthBackgroundLight, animationSpec = themeAnimSpec)
@@ -56,7 +58,7 @@ fun UnlockScreen(
// Load current account
LaunchedEffect(Unit) {
currentPublicKey = accountManager.currentPublicKey.first()
currentPublicKey = selectedAccountId ?: accountManager.currentPublicKey.first()
}
// Entry animation
@@ -290,6 +292,34 @@ fun UnlockScreen(
}
}
Spacer(modifier = Modifier.height(16.dp))
// Switch Account button
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 600)
)
) {
TextButton(
onClick = onSwitchAccount
) {
Icon(
imageVector = Icons.Default.SwapHoriz,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Switch Account",
color = PrimaryBlue,
fontSize = 15.sp
)
}
}
Spacer(modifier = Modifier.weight(0.3f))
}
}

View File

@@ -45,7 +45,8 @@ fun WelcomeScreen(
val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
val lockProgress by animateLottieCompositionAsState(
composition = lockComposition,
iterations = LottieConstants.IterateForever
iterations = 1, // Play once
speed = 1f
)
// Entry animation