1package com.peachpasswords.ui.pairing23import androidx.compose.foundation.layout.Arrangement4import androidx.compose.foundation.layout.Box5import 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.material3.Text13import androidx.compose.runtime.Composable14import androidx.compose.runtime.LaunchedEffect15import androidx.compose.runtime.getValue16import androidx.compose.ui.Alignment17import androidx.compose.ui.Modifier18import androidx.compose.ui.text.font.FontStyle19import androidx.compose.ui.text.style.TextAlign20import androidx.compose.ui.unit.dp21import androidx.compose.ui.unit.sp22import androidx.hilt.navigation.compose.hiltViewModel23import androidx.lifecycle.compose.collectAsStateWithLifecycle24import com.peachpasswords.ui.components.BirdAnimation25import com.peachpasswords.ui.components.PairingAnimState26import com.peachpasswords.ui.theme.LocalPeachAccent27import com.peachpasswords.ui.theme.PeachSpacing28import com.peachpasswords.ui.theme.PeachType29import com.peachpasswords.ui.theme.ShellBackground30import com.peachpasswords.ui.theme.peachColors3132333435363738@Composable39fun CodePairScreen(40 onBack: () -> Unit,41 vm: CodePairViewModel = hiltViewModel(),42) {43 val state by vm.state.collectAsStateWithLifecycle()4445 LaunchedEffect(Unit) { vm.start() }4647 val animState = when (state) {48 is CodePairUiState.Paired -> PairingAnimState.CONNECTED49 is CodePairUiState.ConfirmSas -> PairingAnimState.LISTENING50 else -> PairingAnimState.WAITING51 }5253 ShellBackground {54 Column(55 modifier = Modifier56 .fillMaxSize()57 .systemBarsPadding()58 .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),59 ) {60 Box(Modifier.fillMaxWidth().weight(1f), contentAlignment = Alignment.Center) {61 BirdAnimation(animState, Modifier.fillMaxWidth().height(220.dp))62 }6364 when (val s = state) {65 is CodePairUiState.Starting -> Headline("Starting…", "Opening a pairing window on your network.")6667 is CodePairUiState.ShowingCode -> {68 Headline(69 "Type this on your computer",70 "Open Peach in your browser, choose “Pair a device”, and enter this code. " +71 "Both devices need to be on the same Wi-Fi.",72 )73 Spacer(Modifier.height(PeachSpacing.lg))74 Text(75 text = s.code,76 color = LocalPeachAccent.current,77 fontFamily = PeachType.Mono,78 fontSize = 26.sp,79 letterSpacing = 2.sp,80 textAlign = TextAlign.Center,81 modifier = Modifier.fillMaxWidth(),82 )83 }8485 is CodePairUiState.ConfirmSas -> {86 Headline(87 "Verify the code",88 "Only continue if this code is identical on both screens. " +89 "This is what proves you're talking to your own device.",90 )91 Spacer(Modifier.height(PeachSpacing.lg))92 Text(93 text = s.sas,94 color = LocalPeachAccent.current,95 fontFamily = PeachType.Mono,96 fontSize = 38.sp,97 letterSpacing = 6.sp,98 textAlign = TextAlign.Center,99 modifier = Modifier.fillMaxWidth(),100 )101 Spacer(Modifier.height(PeachSpacing.lg))102 PillButton("They match — pair", LocalPeachAccent.current, filled = true) { vm.confirmSas(true) }103 Spacer(Modifier.height(PeachSpacing.sm))104 PillButton("Cancel", peachColors.T2, filled = false) { vm.confirmSas(false); onBack() }105 }106107 is CodePairUiState.Paired -> {108 Headline("Paired", "${s.deviceName} will now keep your vault in step over the local network.")109 Spacer(Modifier.height(PeachSpacing.lg))110 PillButton("Done", LocalPeachAccent.current, filled = true, onClick = onBack)111 }112113 is CodePairUiState.Failed -> {114 Headline("Couldn't pair", failureCopy(s.reason))115 Spacer(Modifier.height(PeachSpacing.lg))116 PillButton("Back", LocalPeachAccent.current, filled = true, onClick = onBack)117 }118 }119 Spacer(Modifier.height(PeachSpacing.md))120 }121 }122}123124private fun failureCopy(reason: String): String = when (reason) {125 "vault_locked" -> "Unlock your vault first — pairing shares the key that protects it."126 "no_local_network" -> "This phone isn't on a Wi-Fi network, so there's no address to hand out."127 "sas_mismatch" -> "The codes didn't match, so nothing was saved. That's the right outcome if you didn't expect a connection."128 else -> "Something interrupted the connection. Try again."129}130131@Composable132private fun Headline(title: String, body: String) {133 Column(134 modifier = Modifier.fillMaxWidth(),135 horizontalAlignment = Alignment.CenterHorizontally,136 verticalArrangement = Arrangement.Center,137 ) {138 Text(139 title,140 color = peachColors.T0,141 fontFamily = PeachType.Display,142 fontStyle = FontStyle.Italic,143 fontSize = 22.sp,144 textAlign = TextAlign.Center,145 )146 Spacer(Modifier.height(PeachSpacing.xs))147 Text(148 body,149 color = peachColors.T2,150 fontFamily = PeachType.Body,151 fontSize = 13.sp,152 textAlign = TextAlign.Center,153 )154 }155}