package com.peachpasswords.ui.pairing import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.peachpasswords.ui.security.StrengthBar import com.peachpasswords.ui.security.scorePassword import com.peachpasswords.ui.security.strengthLabel import com.peachpasswords.ui.theme.LocalPeachAccent import com.peachpasswords.ui.theme.PeachSpacing import com.peachpasswords.ui.theme.PeachType import com.peachpasswords.ui.theme.ShellBackground import com.peachpasswords.ui.theme.peachColors @Composable fun AdoptVaultScreen( onBack: () -> Unit, onComplete: () -> Unit, vm: AdoptVaultViewModel = hiltViewModel(), ) { val state by vm.state.collectAsStateWithLifecycle() var password by remember { mutableStateOf("") } var confirmation by remember { mutableStateOf("") } var reveal by remember { mutableStateOf(false) } var invitation by remember { mutableStateOf("") } val score = scorePassword(password) val passwordReady = password.length >= 12 && password == confirmation && score >= 2 ShellBackground { Column( modifier = Modifier .fillMaxSize() .systemBarsPadding() .verticalScroll(rememberScrollState()) .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.lg), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center, ) { Text( "I already have Peach", color = peachColors.T0, fontFamily = PeachType.Display, fontSize = 26.sp, ) Spacer(Modifier.height(PeachSpacing.sm)) when (val current = state) { AdoptVaultUiState.Password -> { Body( "Choose the password this phone will use. It never leaves this device; " + "your existing device will share the vault only after both screens confirm the same code.", ) Spacer(Modifier.height(PeachSpacing.lg)) OutlinedTextField( value = password, onValueChange = { password = it }, label = { Text("new password") }, singleLine = true, visualTransformation = if (reveal) VisualTransformation.None else PasswordVisualTransformation(), modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(PeachSpacing.sm)) OutlinedTextField( value = confirmation, onValueChange = { confirmation = it }, label = { Text("confirm password") }, singleLine = true, visualTransformation = if (reveal) VisualTransformation.None else PasswordVisualTransformation(), modifier = Modifier.fillMaxWidth(), ) Text( if (reveal) "hide passwords" else "show passwords", color = LocalPeachAccent.current, fontFamily = PeachType.Mono, fontSize = 11.sp, modifier = Modifier .align(Alignment.End) .clickable { reveal = !reveal } .padding(PeachSpacing.sm), ) StrengthBar(score, Modifier.fillMaxWidth()) Spacer(Modifier.height(PeachSpacing.xs)) Text( "strength: ${strengthLabel(score)} · at least 12 characters", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp, ) Spacer(Modifier.height(PeachSpacing.lg)) PillButton( "Continue", LocalPeachAccent.current, filled = true, ) { if (passwordReady && vm.acceptPassword(password, confirmation)) { password = "" confirmation = "" } } if (!passwordReady) { Spacer(Modifier.height(PeachSpacing.xs)) Text( "Continue unlocks when both passwords match and pass the strength check.", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp, textAlign = TextAlign.Center, ) } } AdoptVaultUiState.Pair -> { Body( "Connecting via secure relay — works anywhere. Scan the QR shown by " + "Peach on your existing device. It contains only the relay address and a single-use token.", ) Spacer(Modifier.height(PeachSpacing.lg)) PairingQrCamera(onInvitation = vm::adopt) Spacer(Modifier.height(PeachSpacing.lg)) Text( "or enter the invitation manually", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp, ) OutlinedTextField( value = invitation, onValueChange = { invitation = it }, label = { Text("peach-pair-v1:…") }, minLines = 3, modifier = Modifier.fillMaxWidth(), ) Spacer(Modifier.height(PeachSpacing.md)) PillButton( "Connect", LocalPeachAccent.current, filled = true, ) { vm.adopt(invitation) } } AdoptVaultUiState.Connecting -> { Body("Opening the single-use relay room and establishing an encrypted session…") } is AdoptVaultUiState.ConfirmSas -> { Body( "Only continue if this code is identical on the device sharing the vault.", ) Spacer(Modifier.height(PeachSpacing.lg)) Text( current.sas, color = LocalPeachAccent.current, fontFamily = PeachType.Mono, fontSize = 38.sp, letterSpacing = 6.sp, ) Spacer(Modifier.height(PeachSpacing.lg)) PillButton( "They match — adopt vault", LocalPeachAccent.current, filled = true, ) { vm.confirmSas(true) } Spacer(Modifier.height(PeachSpacing.sm)) PillButton("Cancel", peachColors.T2, filled = false) { vm.confirmSas(false) } } AdoptVaultUiState.Adopting -> { Body( "Both devices confirmed. Sealing the adopted vault under this phone's password…", ) } is AdoptVaultUiState.Complete -> { Body("${current.sourceName} shared the vault successfully. Encrypted sync is now active.") Spacer(Modifier.height(PeachSpacing.lg)) PillButton("Open vault", LocalPeachAccent.current, filled = true, onClick = onComplete) } is AdoptVaultUiState.Failed -> { Body(adoptionFailureCopy(current.reason)) Spacer(Modifier.height(PeachSpacing.lg)) PillButton("Start again", LocalPeachAccent.current, filled = true) { invitation = "" vm.restart() } } } if (state !is AdoptVaultUiState.Adopting && state !is AdoptVaultUiState.Complete ) { Spacer(Modifier.height(PeachSpacing.md)) PillButton("Back", peachColors.T2, filled = false, onClick = onBack) } } } } @Composable private fun Body(value: String) { Text( value, color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp, textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth(), ) } private fun adoptionFailureCopy(reason: String): String = when (reason) { "invalid_pairing_invitation" -> "That invitation isn't valid. Scan a fresh QR or copy the whole invitation again." "ADOPT_SAS_REJECTED", "adoption_rejected" -> "The confirmation was cancelled. No vault key was accepted." "ADOPT_VAULT_ID_MISMATCH" -> "The received vault did not pass its integrity check, so Peach refused it." else -> "The secure adoption session ended before it completed. Start again with a fresh invitation." }