1package com.peachpasswords.ui.pairing23import androidx.compose.foundation.clickable4import androidx.compose.foundation.layout.Arrangement5import androidx.compose.foundation.layout.Column6import androidx.compose.foundation.layout.Spacer7import androidx.compose.foundation.layout.fillMaxSize8import androidx.compose.foundation.layout.fillMaxWidth9import androidx.compose.foundation.layout.height10import androidx.compose.foundation.layout.padding11import androidx.compose.foundation.layout.systemBarsPadding12import androidx.compose.foundation.rememberScrollState13import androidx.compose.foundation.verticalScroll14import androidx.compose.material3.OutlinedTextField15import androidx.compose.material3.Text16import androidx.compose.runtime.Composable17import androidx.compose.runtime.getValue18import androidx.compose.runtime.mutableStateOf19import androidx.compose.runtime.remember20import androidx.compose.runtime.setValue21import androidx.compose.ui.Alignment22import androidx.compose.ui.Modifier23import androidx.compose.ui.text.input.PasswordVisualTransformation24import androidx.compose.ui.text.input.VisualTransformation25import androidx.compose.ui.text.style.TextAlign26import androidx.compose.ui.unit.dp27import androidx.compose.ui.unit.sp28import androidx.hilt.navigation.compose.hiltViewModel29import androidx.lifecycle.compose.collectAsStateWithLifecycle30import com.peachpasswords.ui.security.StrengthBar31import com.peachpasswords.ui.security.scorePassword32import com.peachpasswords.ui.security.strengthLabel33import com.peachpasswords.ui.theme.LocalPeachAccent34import com.peachpasswords.ui.theme.PeachSpacing35import com.peachpasswords.ui.theme.PeachType36import com.peachpasswords.ui.theme.ShellBackground37import com.peachpasswords.ui.theme.peachColors3839@Composable40fun AdoptVaultScreen(41 onBack: () -> Unit,42 onComplete: () -> Unit,43 vm: AdoptVaultViewModel = hiltViewModel(),44) {45 val state by vm.state.collectAsStateWithLifecycle()46 var password by remember { mutableStateOf("") }47 var confirmation by remember { mutableStateOf("") }48 var reveal by remember { mutableStateOf(false) }49 var invitation by remember { mutableStateOf("") }50 val score = scorePassword(password)51 val passwordReady =52 password.length >= 12 && password == confirmation && score >= 25354 ShellBackground {55 Column(56 modifier = Modifier57 .fillMaxSize()58 .systemBarsPadding()59 .verticalScroll(rememberScrollState())60 .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.lg),61 horizontalAlignment = Alignment.CenterHorizontally,62 verticalArrangement = Arrangement.Center,63 ) {64 Text(65 "I already have Peach",66 color = peachColors.T0,67 fontFamily = PeachType.Display,68 fontSize = 26.sp,69 )70 Spacer(Modifier.height(PeachSpacing.sm))7172 when (val current = state) {73 AdoptVaultUiState.Password -> {74 Body(75 "Choose the password this phone will use. It never leaves this device; " +76 "your existing device will share the vault only after both screens confirm the same code.",77 )78 Spacer(Modifier.height(PeachSpacing.lg))79 OutlinedTextField(80 value = password,81 onValueChange = { password = it },82 label = { Text("new password") },83 singleLine = true,84 visualTransformation =85 if (reveal) VisualTransformation.None else PasswordVisualTransformation(),86 modifier = Modifier.fillMaxWidth(),87 )88 Spacer(Modifier.height(PeachSpacing.sm))89 OutlinedTextField(90 value = confirmation,91 onValueChange = { confirmation = it },92 label = { Text("confirm password") },93 singleLine = true,94 visualTransformation =95 if (reveal) VisualTransformation.None else PasswordVisualTransformation(),96 modifier = Modifier.fillMaxWidth(),97 )98 Text(99 if (reveal) "hide passwords" else "show passwords",100 color = LocalPeachAccent.current,101 fontFamily = PeachType.Mono,102 fontSize = 11.sp,103 modifier = Modifier104 .align(Alignment.End)105 .clickable { reveal = !reveal }106 .padding(PeachSpacing.sm),107 )108 StrengthBar(score, Modifier.fillMaxWidth())109 Spacer(Modifier.height(PeachSpacing.xs))110 Text(111 "strength: ${strengthLabel(score)} · at least 12 characters",112 color = peachColors.T2,113 fontFamily = PeachType.Mono,114 fontSize = 10.sp,115 )116 Spacer(Modifier.height(PeachSpacing.lg))117 PillButton(118 "Continue",119 LocalPeachAccent.current,120 filled = true,121 ) {122 if (passwordReady && vm.acceptPassword(password, confirmation)) {123 password = ""124 confirmation = ""125 }126 }127 if (!passwordReady) {128 Spacer(Modifier.height(PeachSpacing.xs))129 Text(130 "Continue unlocks when both passwords match and pass the strength check.",131 color = peachColors.T2,132 fontFamily = PeachType.Mono,133 fontSize = 10.sp,134 textAlign = TextAlign.Center,135 )136 }137 }138139 AdoptVaultUiState.Pair -> {140 Body(141 "Connecting via secure relay — works anywhere. Scan the QR shown by " +142 "Peach on your existing device. It contains only the relay address and a single-use token.",143 )144 Spacer(Modifier.height(PeachSpacing.lg))145 PairingQrCamera(onInvitation = vm::adopt)146 Spacer(Modifier.height(PeachSpacing.lg))147 Text(148 "or enter the invitation manually",149 color = peachColors.T2,150 fontFamily = PeachType.Mono,151 fontSize = 10.sp,152 )153 OutlinedTextField(154 value = invitation,155 onValueChange = { invitation = it },156 label = { Text("peach-pair-v1:…") },157 minLines = 3,158 modifier = Modifier.fillMaxWidth(),159 )160 Spacer(Modifier.height(PeachSpacing.md))161 PillButton(162 "Connect",163 LocalPeachAccent.current,164 filled = true,165 ) { vm.adopt(invitation) }166 }167168 AdoptVaultUiState.Connecting -> {169 Body("Opening the single-use relay room and establishing an encrypted session…")170 }171172 is AdoptVaultUiState.ConfirmSas -> {173 Body(174 "Only continue if this code is identical on the device sharing the vault.",175 )176 Spacer(Modifier.height(PeachSpacing.lg))177 Text(178 current.sas,179 color = LocalPeachAccent.current,180 fontFamily = PeachType.Mono,181 fontSize = 38.sp,182 letterSpacing = 6.sp,183 )184 Spacer(Modifier.height(PeachSpacing.lg))185 PillButton(186 "They match — adopt vault",187 LocalPeachAccent.current,188 filled = true,189 ) { vm.confirmSas(true) }190 Spacer(Modifier.height(PeachSpacing.sm))191 PillButton("Cancel", peachColors.T2, filled = false) {192 vm.confirmSas(false)193 }194 }195196 AdoptVaultUiState.Adopting -> {197 Body(198 "Both devices confirmed. Sealing the adopted vault under this phone's password…",199 )200 }201202 is AdoptVaultUiState.Complete -> {203 Body("${current.sourceName} shared the vault successfully. Encrypted sync is now active.")204 Spacer(Modifier.height(PeachSpacing.lg))205 PillButton("Open vault", LocalPeachAccent.current, filled = true, onClick = onComplete)206 }207208 is AdoptVaultUiState.Failed -> {209 Body(adoptionFailureCopy(current.reason))210 Spacer(Modifier.height(PeachSpacing.lg))211 PillButton("Start again", LocalPeachAccent.current, filled = true) {212 invitation = ""213 vm.restart()214 }215 }216 }217218 if (state !is AdoptVaultUiState.Adopting &&219 state !is AdoptVaultUiState.Complete220 ) {221 Spacer(Modifier.height(PeachSpacing.md))222 PillButton("Back", peachColors.T2, filled = false, onClick = onBack)223 }224 }225 }226}227228@Composable229private fun Body(value: String) {230 Text(231 value,232 color = peachColors.T2,233 fontFamily = PeachType.Body,234 fontSize = 13.sp,235 textAlign = TextAlign.Center,236 modifier = Modifier.fillMaxWidth(),237 )238}239240private fun adoptionFailureCopy(reason: String): String = when (reason) {241 "invalid_pairing_invitation" ->242 "That invitation isn't valid. Scan a fresh QR or copy the whole invitation again."243 "ADOPT_SAS_REJECTED", "adoption_rejected" ->244 "The confirmation was cancelled. No vault key was accepted."245 "ADOPT_VAULT_ID_MISMATCH" ->246 "The received vault did not pass its integrity check, so Peach refused it."247 else ->248 "The secure adoption session ended before it completed. Start again with a fresh invitation."249}