AdoptVaultScreen.kt

10.7 KB · raw

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}