CodePairScreen.kt

6.3 KB · raw

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.peachColors3132/**33 * Pair a device by code. The phone shows the code; the browser types it in and34 * dials. Mirrors the extension's own phase structure (enter → connecting → sas35 * → paired), minus the entry field — the phone never types a code, it publishes36 * one.37 */38@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}