Refactor UI components in ChatsListScreen, ForwardChatPickerBottomSheet, and SearchScreen for improved readability and maintainability; adjust text color alpha values, streamline imports, and enhance keyboard handling functionality.

This commit is contained in:
2026-01-17 21:09:47 +05:00
parent c9136ed499
commit a3810af4a0
11 changed files with 3763 additions and 3226 deletions

View File

@@ -5,7 +5,6 @@ import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
@@ -35,21 +34,38 @@ import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SetPasswordScreen(
seedPhrase: List<String>,
isDarkTheme: Boolean,
onBack: () -> Unit,
onAccountCreated: (DecryptedAccount) -> Unit
seedPhrase: List<String>,
isDarkTheme: Boolean,
onBack: () -> Unit,
onAccountCreated: (DecryptedAccount) -> 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 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 context = LocalContext.current
val accountManager = remember { AccountManager(context) }
val scope = rememberCoroutineScope()
var password by remember { mutableStateOf("") }
var confirmPassword by remember { mutableStateOf("") }
var passwordVisible by remember { mutableStateOf(false) }
@@ -57,492 +73,540 @@ fun SetPasswordScreen(
var isCreating by remember { mutableStateOf(false) }
var error by remember { mutableStateOf<String?>(null) }
var visible by remember { mutableStateOf(false) }
// Track keyboard visibility
val view = androidx.compose.ui.platform.LocalView.current
var isKeyboardVisible by remember { mutableStateOf(false) }
DisposableEffect(view) {
val listener = android.view.ViewTreeObserver.OnGlobalLayoutListener {
val rect = android.graphics.Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
isKeyboardVisible = keypadHeight > screenHeight * 0.15
}
val listener =
android.view.ViewTreeObserver.OnGlobalLayoutListener {
val rect = android.graphics.Rect()
view.getWindowVisibleDisplayFrame(rect)
val screenHeight = view.rootView.height
val keypadHeight = screenHeight - rect.bottom
isKeyboardVisible = keypadHeight > screenHeight * 0.15
}
view.viewTreeObserver.addOnGlobalLayoutListener(listener)
onDispose {
view.viewTreeObserver.removeOnGlobalLayoutListener(listener)
}
onDispose { view.viewTreeObserver.removeOnGlobalLayoutListener(listener) }
}
LaunchedEffect(Unit) {
visible = true
}
LaunchedEffect(Unit) { visible = true }
val passwordsMatch = password == confirmPassword && password.isNotEmpty()
val isPasswordWeak = password.isNotEmpty() && password.length < 6
val canContinue = passwordsMatch && !isCreating
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
) {
Column(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
) {
Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
Column(modifier = Modifier.fillMaxSize().statusBarsPadding()) {
// Top Bar
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
IconButton(onClick = onBack, enabled = !isCreating) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = "Back",
tint = textColor.copy(alpha = 0.6f)
imageVector = Icons.Default.ArrowBack,
contentDescription = "Back",
tint = textColor.copy(alpha = 0.6f)
)
}
Spacer(modifier = Modifier.weight(1f))
Text(
text = "Set Password",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = textColor
text = "Set Password",
fontSize = 18.sp,
fontWeight = FontWeight.SemiBold,
color = textColor
)
Spacer(modifier = Modifier.weight(1f))
Spacer(modifier = Modifier.width(48.dp))
}
Column(
modifier = Modifier
.fillMaxSize()
.imePadding()
.padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
modifier =
Modifier.fillMaxSize()
.imePadding()
.padding(horizontal = 24.dp)
.verticalScroll(rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 8.dp else 16.dp))
// Lock Icon - smaller when keyboard is visible
val iconSize by animateDpAsState(
targetValue = if (isKeyboardVisible) 48.dp else 80.dp,
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
val iconInnerSize by animateDpAsState(
targetValue = if (isKeyboardVisible) 24.dp else 40.dp,
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
val iconSize by
animateDpAsState(
targetValue = if (isKeyboardVisible) 48.dp else 80.dp,
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
val iconInnerSize by
animateDpAsState(
targetValue = if (isKeyboardVisible) 24.dp else 40.dp,
animationSpec = tween(300, easing = FastOutSlowInEasing)
)
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500)) + scaleIn(
initialScale = 0.5f,
animationSpec = tween(500, easing = FastOutSlowInEasing)
)
visible = visible,
enter =
fadeIn(tween(500)) +
scaleIn(
initialScale = 0.5f,
animationSpec =
tween(500, easing = FastOutSlowInEasing)
)
) {
Box(
modifier = Modifier
.size(iconSize)
.clip(RoundedCornerShape(if (isKeyboardVisible) 12.dp else 20.dp))
.background(PrimaryBlue.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
modifier =
Modifier.size(iconSize)
.clip(
RoundedCornerShape(
if (isKeyboardVisible) 12.dp else 20.dp
)
)
.background(PrimaryBlue.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Lock,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(iconInnerSize)
Icons.Default.Lock,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(iconInnerSize)
)
}
}
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 12.dp else 24.dp))
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 100)) + slideInVertically(
initialOffsetY = { -20 },
animationSpec = tween(500, delayMillis = 100)
)
visible = visible,
enter =
fadeIn(tween(500, delayMillis = 100)) +
slideInVertically(
initialOffsetY = { -20 },
animationSpec = tween(500, delayMillis = 100)
)
) {
Text(
text = "Protect Your Account",
fontSize = if (isKeyboardVisible) 20.sp else 24.sp,
fontWeight = FontWeight.Bold,
color = textColor
text = "Protect Your Account",
fontSize = if (isKeyboardVisible) 20.sp else 24.sp,
fontWeight = FontWeight.Bold,
color = textColor
)
}
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 6.dp else 8.dp))
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 200))
visible = visible,
enter = fadeIn(tween(500, delayMillis = 200))
) {
Text(
text = "This password encrypts your keys locally.\nYou'll need it to unlock Rosetta.",
fontSize = if (isKeyboardVisible) 12.sp else 14.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center,
lineHeight = if (isKeyboardVisible) 16.sp else 20.sp
text =
"This password encrypts your keys locally.\nYou'll need it to unlock Rosetta.",
fontSize = if (isKeyboardVisible) 12.sp else 14.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center,
lineHeight = if (isKeyboardVisible) 16.sp else 20.sp
)
}
Spacer(modifier = Modifier.height(if (isKeyboardVisible) 16.dp else 32.dp))
// Password Field
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 300)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 300)
)
visible = visible,
enter =
fadeIn(tween(500, delayMillis = 300)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 300)
)
) {
OutlinedTextField(
value = password,
onValueChange = {
password = it
error = null
},
label = { Text("Password") },
placeholder = { Text("Enter password") },
singleLine = true,
visualTransformation = if (passwordVisible)
VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
imageVector = if (passwordVisible)
Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = if (passwordVisible) "Hide" else "Show"
)
}
},
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0),
focusedLabelColor = PrimaryBlue,
cursorColor = PrimaryBlue,
focusedTextColor = textColor,
unfocusedTextColor = textColor
),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
value = password,
onValueChange = {
password = it
error = null
},
label = { Text("Password") },
placeholder = { Text("Enter password") },
singleLine = true,
visualTransformation =
if (passwordVisible) VisualTransformation.None
else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
imageVector =
if (passwordVisible) Icons.Default.VisibilityOff
else Icons.Default.Visibility,
contentDescription =
if (passwordVisible) "Hide" else "Show"
)
}
},
colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor =
if (isDarkTheme) Color(0xFF4A4A4A)
else Color(0xFFD0D0D0),
focusedLabelColor = PrimaryBlue,
cursorColor = PrimaryBlue,
focusedTextColor = textColor,
unfocusedTextColor = textColor
),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Next
)
)
)
}
// Password strength indicator
if (password.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = 350)) + slideInHorizontally(
initialOffsetX = { -30 },
animationSpec = tween(400, delayMillis = 350)
)
visible = visible,
enter =
fadeIn(tween(400, delayMillis = 350)) +
slideInHorizontally(
initialOffsetX = { -30 },
animationSpec = tween(400, delayMillis = 350)
)
) {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
val strength = when {
password.length < 6 -> "Weak"
password.length < 10 -> "Medium"
else -> "Strong"
}
val strengthColor = when {
password.length < 6 -> Color(0xFFE53935)
password.length < 10 -> Color(0xFFFFA726)
else -> Color(0xFF4CAF50)
}
val strength =
when {
password.length < 6 -> "Weak"
password.length < 10 -> "Medium"
else -> "Strong"
}
val strengthColor =
when {
password.length < 6 -> Color(0xFFE53935)
password.length < 10 -> Color(0xFFFFA726)
else -> Color(0xFF4CAF50)
}
Icon(
imageVector = Icons.Default.Shield,
contentDescription = null,
tint = strengthColor,
modifier = Modifier.size(16.dp)
imageVector = Icons.Default.Shield,
contentDescription = null,
tint = strengthColor,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = "Password strength: $strength",
fontSize = 12.sp,
color = strengthColor
text = "Password strength: $strength",
fontSize = 12.sp,
color = strengthColor
)
}
// Warning for weak passwords
if (isPasswordWeak) {
Spacer(modifier = Modifier.height(4.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(Color(0xFFE53935).copy(alpha = 0.1f))
.padding(8.dp),
verticalAlignment = Alignment.Top
modifier =
Modifier.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.background(
Color(0xFFE53935).copy(alpha = 0.1f)
)
.padding(8.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = Color(0xFFE53935),
modifier = Modifier.size(16.dp)
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = Color(0xFFE53935),
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Your password is too weak. Consider using at least 6 characters for better security.",
fontSize = 11.sp,
color = Color(0xFFE53935),
lineHeight = 14.sp
text =
"Your password is too weak. Consider using at least 6 characters for better security.",
fontSize = 11.sp,
color = Color(0xFFE53935),
lineHeight = 14.sp
)
}
}
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// Confirm Password Field
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 400)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 400)
)
visible = visible,
enter =
fadeIn(tween(500, delayMillis = 400)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 400)
)
) {
OutlinedTextField(
value = confirmPassword,
onValueChange = {
confirmPassword = it
error = null
},
label = { Text("Confirm Password") },
placeholder = { Text("Re-enter password") },
singleLine = true,
visualTransformation = if (confirmPasswordVisible)
VisualTransformation.None else PasswordVisualTransformation(),
trailingIcon = {
IconButton(onClick = { confirmPasswordVisible = !confirmPasswordVisible }) {
Icon(
imageVector = if (confirmPasswordVisible)
Icons.Default.VisibilityOff else Icons.Default.Visibility,
contentDescription = if (confirmPasswordVisible) "Hide" else "Show"
)
}
},
isError = confirmPassword.isNotEmpty() && !passwordsMatch,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0),
focusedLabelColor = PrimaryBlue,
cursorColor = PrimaryBlue,
focusedTextColor = textColor,
unfocusedTextColor = textColor
),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
value = confirmPassword,
onValueChange = {
confirmPassword = it
error = null
},
label = { Text("Confirm Password") },
placeholder = { Text("Re-enter password") },
singleLine = true,
visualTransformation =
if (confirmPasswordVisible) VisualTransformation.None
else PasswordVisualTransformation(),
trailingIcon = {
IconButton(
onClick = {
confirmPasswordVisible = !confirmPasswordVisible
}
) {
Icon(
imageVector =
if (confirmPasswordVisible)
Icons.Default.VisibilityOff
else Icons.Default.Visibility,
contentDescription =
if (confirmPasswordVisible) "Hide" else "Show"
)
}
},
isError = confirmPassword.isNotEmpty() && !passwordsMatch,
colors =
OutlinedTextFieldDefaults.colors(
focusedBorderColor = PrimaryBlue,
unfocusedBorderColor =
if (isDarkTheme) Color(0xFF4A4A4A)
else Color(0xFFD0D0D0),
focusedLabelColor = PrimaryBlue,
cursorColor = PrimaryBlue,
focusedTextColor = textColor,
unfocusedTextColor = textColor
),
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(12.dp),
keyboardOptions =
KeyboardOptions(
keyboardType = KeyboardType.Password,
imeAction = ImeAction.Done
)
)
)
}
// Match indicator
if (confirmPassword.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(400, delayMillis = 450)) + slideInHorizontally(
initialOffsetX = { -30 },
animationSpec = tween(400, delayMillis = 450)
)
visible = visible,
enter =
fadeIn(tween(400, delayMillis = 450)) +
slideInHorizontally(
initialOffsetX = { -30 },
animationSpec = tween(400, delayMillis = 450)
)
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
val matchIcon = if (passwordsMatch) Icons.Default.Check else Icons.Default.Close
val matchColor = if (passwordsMatch) Color(0xFF4CAF50) else Color(0xFFE53935)
val matchText = if (passwordsMatch) "Passwords match" else "Passwords don't match"
val matchIcon =
if (passwordsMatch) Icons.Default.Check else Icons.Default.Close
val matchColor =
if (passwordsMatch) Color(0xFF4CAF50) else Color(0xFFE53935)
val matchText =
if (passwordsMatch) "Passwords match"
else "Passwords don't match"
Icon(
imageVector = matchIcon,
contentDescription = null,
tint = matchColor,
modifier = Modifier.size(16.dp)
imageVector = matchIcon,
contentDescription = null,
tint = matchColor,
modifier = Modifier.size(16.dp)
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = matchText,
fontSize = 12.sp,
color = matchColor
)
}
Text(text = matchText, fontSize = 12.sp, color = matchColor)
}
}
}
// Error message
error?.let { errorMsg ->
Spacer(modifier = Modifier.height(16.dp))
AnimatedVisibility(
visible = true,
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f)
visible = true,
enter = fadeIn(tween(300)) + scaleIn(initialScale = 0.9f)
) {
Text(
text = errorMsg,
fontSize = 14.sp,
color = Color(0xFFE53935),
textAlign = TextAlign.Center
text = errorMsg,
fontSize = 14.sp,
color = Color(0xFFE53935),
textAlign = TextAlign.Center
)
}
}
Spacer(modifier = Modifier.weight(1f))
// Info - hide when keyboard is visible
AnimatedVisibility(
visible = visible && !isKeyboardVisible,
enter = fadeIn(tween(400)) + slideInVertically(
initialOffsetY = { 30 },
animationSpec = tween(400)
) + scaleIn(
initialScale = 0.9f,
animationSpec = tween(400)
),
exit = fadeOut(tween(300)) + slideOutVertically(
targetOffsetY = { 30 },
animationSpec = tween(300)
) + scaleOut(
targetScale = 0.9f,
animationSpec = tween(300)
)
visible = visible && !isKeyboardVisible,
enter =
fadeIn(tween(400)) +
slideInVertically(
initialOffsetY = { 30 },
animationSpec = tween(400)
) +
scaleIn(initialScale = 0.9f, animationSpec = tween(400)),
exit =
fadeOut(tween(300)) +
slideOutVertically(
targetOffsetY = { 30 },
animationSpec = tween(300)
) +
scaleOut(targetScale = 0.9f, animationSpec = tween(300))
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(cardColor)
.padding(16.dp),
verticalAlignment = Alignment.Top
modifier =
Modifier.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(cardColor)
.padding(16.dp),
verticalAlignment = Alignment.Top
) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(20.dp)
imageVector = Icons.Default.Info,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Your password is never stored or sent anywhere. It's only used to encrypt your keys locally.",
fontSize = 13.sp,
color = secondaryTextColor,
lineHeight = 18.sp
text =
"Your password is never stored or sent anywhere. It's only used to encrypt your keys locally.",
fontSize = 13.sp,
color = secondaryTextColor,
lineHeight = 18.sp
)
}
}
Spacer(modifier = Modifier.height(16.dp))
// Create Account Button
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(500, delayMillis = 600)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 600)
)
visible = visible,
enter =
fadeIn(tween(500, delayMillis = 600)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(500, delayMillis = 600)
)
) {
Button(
onClick = {
if (!passwordsMatch) {
error = "Passwords don't match"
return@Button
onClick = {
if (!passwordsMatch) {
error = "Passwords don't match"
return@Button
}
isCreating = true
scope.launch {
try {
// Generate keys from seed phrase
val keyPair =
CryptoManager.generateKeyPairFromSeed(seedPhrase)
// Encrypt private key and seed phrase
val encryptedPrivateKey =
CryptoManager.encryptWithPassword(
keyPair.privateKey,
password
)
val encryptedSeedPhrase =
CryptoManager.encryptWithPassword(
seedPhrase.joinToString(" "),
password
)
// Save account with truncated public key as name
val truncatedKey =
"${keyPair.publicKey.take(6)}...${keyPair.publicKey.takeLast(4)}"
val account =
EncryptedAccount(
publicKey = keyPair.publicKey,
encryptedPrivateKey = encryptedPrivateKey,
encryptedSeedPhrase = encryptedSeedPhrase,
name = truncatedKey
)
accountManager.saveAccount(account)
accountManager.setCurrentAccount(keyPair.publicKey)
// 🔌 Connect to server and authenticate
val privateKeyHash =
CryptoManager.generatePrivateKeyHash(
keyPair.privateKey
)
ProtocolManager.connect()
// Give WebSocket time to connect before authenticating
kotlinx.coroutines.delay(500)
ProtocolManager.authenticate(
keyPair.publicKey,
privateKeyHash
)
// Create DecryptedAccount to pass to callback
val decryptedAccount =
DecryptedAccount(
publicKey = keyPair.publicKey,
privateKey = keyPair.privateKey,
seedPhrase = seedPhrase,
privateKeyHash = privateKeyHash,
name = truncatedKey
)
onAccountCreated(decryptedAccount)
} catch (e: Exception) {
error = "Failed to create account: ${e.message}"
isCreating = false
}
}
},
enabled = canContinue,
modifier = Modifier.fillMaxWidth().height(56.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White,
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
disabledContentColor = Color.White.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(12.dp)
) {
if (isCreating) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Text(
text = "Create Account",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
}
isCreating = true
scope.launch {
try {
// Generate keys from seed phrase
val keyPair = CryptoManager.generateKeyPairFromSeed(seedPhrase)
// Encrypt private key and seed phrase
val encryptedPrivateKey = CryptoManager.encryptWithPassword(
keyPair.privateKey, password
)
val encryptedSeedPhrase = CryptoManager.encryptWithPassword(
seedPhrase.joinToString(" "), password
)
// Save account with truncated public key as name
val truncatedKey = "${keyPair.publicKey.take(6)}...${keyPair.publicKey.takeLast(4)}"
val account = EncryptedAccount(
publicKey = keyPair.publicKey,
encryptedPrivateKey = encryptedPrivateKey,
encryptedSeedPhrase = encryptedSeedPhrase,
name = truncatedKey
)
accountManager.saveAccount(account)
accountManager.setCurrentAccount(keyPair.publicKey)
// 🔌 Connect to server and authenticate
val privateKeyHash = CryptoManager.generatePrivateKeyHash(keyPair.privateKey)
ProtocolManager.connect()
// Give WebSocket time to connect before authenticating
kotlinx.coroutines.delay(500)
ProtocolManager.authenticate(keyPair.publicKey, privateKeyHash)
// Create DecryptedAccount to pass to callback
val decryptedAccount = DecryptedAccount(
publicKey = keyPair.publicKey,
privateKey = keyPair.privateKey,
seedPhrase = seedPhrase,
privateKeyHash = privateKeyHash,
name = truncatedKey
)
onAccountCreated(decryptedAccount)
} catch (e: Exception) {
error = "Failed to create account: ${e.message}"
isCreating = false
}
}
},
enabled = canContinue,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White,
disabledContainerColor = PrimaryBlue.copy(alpha = 0.5f),
disabledContentColor = Color.White.copy(alpha = 0.5f)
),
shape = RoundedCornerShape(12.dp)
) {
if (isCreating) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Text(
text = "Create Account",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
}
} }
}
Spacer(modifier = Modifier.height(32.dp))
}
}

View File

@@ -3,7 +3,6 @@ package com.rosetta.messenger.ui.auth
import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -14,7 +13,6 @@ 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.graphics.toArgb
import androidx.compose.ui.platform.LocalView
@@ -24,7 +22,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.airbnb.lottie.compose.*
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
// Auth colors
val AuthBackground = Color(0xFF1B1B1B)
@@ -34,229 +31,232 @@ val AuthSurfaceLight = Color(0xFFF5F5F5)
@Composable
fun WelcomeScreen(
isDarkTheme: Boolean,
hasExistingAccount: Boolean = false,
onBack: () -> Unit = {},
onCreateSeed: () -> Unit,
onImportSeed: () -> Unit
isDarkTheme: Boolean,
hasExistingAccount: Boolean = false,
onBack: () -> Unit = {},
onCreateSeed: () -> Unit,
onImportSeed: () -> 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 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
)
// Sync navigation bar color with background
val view = LocalView.current
SideEffect {
val window = (view.context as? android.app.Activity)?.window
window?.navigationBarColor = backgroundColor.toArgb()
}
// Animation for Lottie
val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
val lockProgress by animateLottieCompositionAsState(
composition = lockComposition,
iterations = 1, // Play once
speed = 1f
)
val lockComposition by
rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
val lockProgress by
animateLottieCompositionAsState(
composition = lockComposition,
iterations = 1, // Play once
speed = 1f
)
// Entry animation
var visible by remember { mutableStateOf(false) }
LaunchedEffect(Unit) { visible = true }
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
) {
Box(modifier = Modifier.fillMaxSize().background(backgroundColor)) {
// Back button when coming from UnlockScreen
if (hasExistingAccount) {
IconButton(
onClick = onBack,
modifier = Modifier
.statusBarsPadding()
.padding(4.dp)
) {
IconButton(onClick = onBack, modifier = Modifier.statusBarsPadding().padding(4.dp)) {
Icon(
Icons.Default.ArrowBack,
contentDescription = "Back",
tint = textColor.copy(alpha = 0.6f)
Icons.Default.ArrowBack,
contentDescription = "Back",
tint = textColor.copy(alpha = 0.6f)
)
}
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 24.dp)
.statusBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally
modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp).statusBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(0.15f))
// Animated Lock Icon
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600)) + scaleIn(tween(600, easing = FastOutSlowInEasing))
visible = visible,
enter = fadeIn(tween(600)) + scaleIn(tween(600, easing = FastOutSlowInEasing))
) {
Box(
modifier = Modifier.size(180.dp),
contentAlignment = Alignment.Center
) {
Box(modifier = Modifier.size(180.dp), contentAlignment = Alignment.Center) {
lockComposition?.let { comp ->
LottieAnimation(
composition = comp,
progress = { lockProgress },
modifier = Modifier.fillMaxSize()
composition = comp,
progress = { lockProgress },
modifier = Modifier.fillMaxSize()
)
}
}
}
Spacer(modifier = Modifier.height(32.dp))
// Title
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 200)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 200)
)
visible = visible,
enter =
fadeIn(tween(600, delayMillis = 200)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 200)
)
) {
Text(
text = "Your Keys,\nYour Messages",
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = textColor,
textAlign = TextAlign.Center,
lineHeight = 40.sp
text = "Your Keys,\nYour Messages",
fontSize = 32.sp,
fontWeight = FontWeight.Bold,
color = textColor,
textAlign = TextAlign.Center,
lineHeight = 40.sp
)
}
Spacer(modifier = Modifier.height(16.dp))
// Subtitle
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 300)) + slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 300)
)
visible = visible,
enter =
fadeIn(tween(600, delayMillis = 300)) +
slideInVertically(
initialOffsetY = { 50 },
animationSpec = tween(600, delayMillis = 300)
)
) {
Text(
text = "Secure messaging with\ncryptographic keys",
fontSize = 16.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center,
lineHeight = 24.sp,
modifier = Modifier.padding(horizontal = 8.dp)
text = "Secure messaging with\ncryptographic keys",
fontSize = 16.sp,
color = secondaryTextColor,
textAlign = TextAlign.Center,
lineHeight = 24.sp,
modifier = Modifier.padding(horizontal = 8.dp)
)
}
Spacer(modifier = Modifier.height(24.dp))
// Features list with icons - placed above buttons
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 400))
) {
AnimatedVisibility(visible = visible, enter = fadeIn(tween(600, delayMillis = 400))) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
CompactFeatureItem(
icon = Icons.Default.Security,
text = "Encrypted",
isDarkTheme = isDarkTheme,
textColor = textColor
icon = Icons.Default.Security,
text = "Encrypted",
isDarkTheme = isDarkTheme,
textColor = textColor
)
CompactFeatureItem(
icon = Icons.Default.NoAccounts,
text = "No Phone",
isDarkTheme = isDarkTheme,
textColor = textColor
icon = Icons.Default.NoAccounts,
text = "No Phone",
isDarkTheme = isDarkTheme,
textColor = textColor
)
CompactFeatureItem(
icon = Icons.Default.Key,
text = "Your Keys",
isDarkTheme = isDarkTheme,
textColor = textColor
icon = Icons.Default.Key,
text = "Your Keys",
isDarkTheme = isDarkTheme,
textColor = textColor
)
}
}
Spacer(modifier = Modifier.height(32.dp))
// Create Seed Button
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 500)) + slideInVertically(
initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 500)
)
visible = visible,
enter =
fadeIn(tween(600, delayMillis = 500)) +
slideInVertically(
initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 500)
)
) {
Button(
onClick = onCreateSeed,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
colors = ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White
),
shape = RoundedCornerShape(16.dp),
elevation = ButtonDefaults.buttonElevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp
)
onClick = onCreateSeed,
modifier = Modifier.fillMaxWidth().height(56.dp),
colors =
ButtonDefaults.buttonColors(
containerColor = PrimaryBlue,
contentColor = Color.White
),
shape = RoundedCornerShape(16.dp),
elevation =
ButtonDefaults.buttonElevation(
defaultElevation = 0.dp,
pressedElevation = 0.dp
)
) {
Icon(
imageVector = Icons.Default.Key,
contentDescription = null,
modifier = Modifier.size(22.dp)
imageVector = Icons.Default.Key,
contentDescription = null,
modifier = Modifier.size(22.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = "Generate New Seed Phrase",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
text = "Generate New Seed Phrase",
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold
)
}
}
Spacer(modifier = Modifier.height(12.dp))
// Import Seed Button
AnimatedVisibility(
visible = visible,
enter = fadeIn(tween(600, delayMillis = 600)) + slideInVertically(
initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 600)
)
visible = visible,
enter =
fadeIn(tween(600, delayMillis = 600)) +
slideInVertically(
initialOffsetY = { 100 },
animationSpec = tween(600, delayMillis = 600)
)
) {
TextButton(
onClick = onImportSeed,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(16.dp)
onClick = onImportSeed,
modifier = Modifier.fillMaxWidth().height(56.dp),
shape = RoundedCornerShape(16.dp)
) {
Icon(
imageVector = Icons.Default.Download,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = PrimaryBlue
imageVector = Icons.Default.Download,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = PrimaryBlue
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "I Already Have a Seed Phrase",
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = PrimaryBlue
text = "I Already Have a Seed Phrase",
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
color = PrimaryBlue
)
}
}
Spacer(modifier = Modifier.weight(0.15f))
}
}
@@ -264,70 +264,62 @@ fun WelcomeScreen(
@Composable
private fun CompactFeatureItem(
icon: androidx.compose.ui.graphics.vector.ImageVector,
text: String,
isDarkTheme: Boolean,
textColor: Color
icon: androidx.compose.ui.graphics.vector.ImageVector,
text: String,
isDarkTheme: Boolean,
textColor: Color
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Box(
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.12f)),
contentAlignment = Alignment.Center
modifier =
Modifier.size(48.dp)
.clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.12f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(24.dp)
imageVector = icon,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(24.dp)
)
}
Text(
text = text,
fontSize = 13.sp,
color = textColor.copy(alpha = 0.8f),
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center
text = text,
fontSize = 13.sp,
color = textColor.copy(alpha = 0.8f),
fontWeight = FontWeight.Medium,
textAlign = TextAlign.Center
)
}
}
@Composable
private fun FeatureItem(
icon: androidx.compose.ui.graphics.vector.ImageVector,
text: String,
isDarkTheme: Boolean,
textColor: Color
icon: androidx.compose.ui.graphics.vector.ImageVector,
text: String,
isDarkTheme: Boolean,
textColor: Color
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.15f)),
contentAlignment = Alignment.Center
modifier =
Modifier.size(40.dp)
.clip(CircleShape)
.background(PrimaryBlue.copy(alpha = 0.15f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(20.dp)
imageVector = icon,
contentDescription = null,
tint = PrimaryBlue,
modifier = Modifier.size(20.dp)
)
}
Spacer(modifier = Modifier.width(16.dp))
Text(
text = text,
fontSize = 15.sp,
color = textColor,
fontWeight = FontWeight.Medium
)
Text(text = text, fontSize = 15.sp, color = textColor, fontWeight = FontWeight.Medium)
}
}