package com.peachpasswords.ui.settings import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box 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.size import androidx.compose.foundation.layout.systemBarsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider 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.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.peachpasswords.ui.components.RuneGridBackground import com.peachpasswords.ui.components.PeachTopBar import com.peachpasswords.ui.theme.LocalPeachAccent import com.peachpasswords.ui.theme.LocalPeachColors import com.peachpasswords.ui.theme.PeachSpacing import com.peachpasswords.ui.theme.peachColors import com.peachpasswords.ui.theme.peachDark import com.peachpasswords.ui.theme.PeachType import android.content.Context import dagger.hilt.android.qualifiers.ApplicationContext import com.peachpasswords.ui.onboarding.ONBOARD_KEY import com.peachpasswords.ui.onboarding.ONBOARD_PREFS import com.peachpasswords.sync.SyncFreshStartResetter import com.peachpasswords.vault.VaultRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class PanicViewModel @Inject constructor( private val vault: VaultRepository, private val syncFreshStart: SyncFreshStartResetter, @ApplicationContext private val appContext: Context, ) : ViewModel() { val entryCount: Int get() = vault.getEntries().size fun destroy(onDone: () -> Unit) = viewModelScope.launch { vault.destroyVault() syncFreshStart.reset() // Destroying the vault puts the app back to factory state, so clear the // "already onboarded" flag too — otherwise the next launch skipped the // intro and dropped straight onto create-vault, which is not what a // fresh start should look like. appContext.getSharedPreferences(ONBOARD_PREFS, Context.MODE_PRIVATE) .edit().remove(ONBOARD_KEY).apply() onDone() } } @Composable fun PanicScreen( onBack: () -> Unit, onDestroyed: () -> Unit, vm: PanicViewModel = hiltViewModel(), ) { var step by remember { mutableStateOf(1) } val entryCount = remember { vm.entryCount } // Panic always renders dark regardless of app theme: its tokens must stay // readable on the intentional near-black field below (keep the user's accent). val accent = LocalPeachAccent.current val darkPalette = remember(accent) { peachDark(accent) } CompositionLocalProvider(LocalPeachColors provides darkPalette) { // The panic flow is intentionally cold: no bloom, a flat near-black field so // it reads as a dead-end rather than another lit-up screen in the app. Box(modifier = Modifier.fillMaxSize().background(Color(0xFF0C0A06))) { // Browser parity: the final step runs a drifting rune field with runes // flaring and dying behind it. The earlier steps stay deliberately cold // and flat — the chaos is what marks this as the point of no return. if (step == 3) { RuneGridBackground( modifier = Modifier.fillMaxSize(), alpha = 0.22f, glowing = true, ) } Column( modifier = Modifier .fillMaxSize() .systemBarsPadding() .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md), ) { PeachTopBar( title = "Panic", leading = { Icon( Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "back", tint = peachColors.T0, modifier = Modifier .size(22.dp) .clip(RoundedCornerShape(PeachSpacing.sm)) .clickable(onClick = onBack), ) }, ) Box( modifier = Modifier.fillMaxSize().padding(horizontal = PeachSpacing.sm), contentAlignment = Alignment.Center, ) { Column(verticalArrangement = Arrangement.Center) { when (step) { 1 -> Step1(onCancel = onBack, onContinue = { step = 2 }) 2 -> Step2(entryCount = entryCount, onBack = { step = 1 }, onContinue = { step = 3 }) else -> Step3(onBack = { step = 2 }, onDestroy = { vm.destroy(onDestroyed) }) } } } } } } } @Composable private fun Step1(onCancel: () -> Unit, onContinue: () -> Unit) { Headline("destroy vault", 44.sp, peachColors.Breach) Spacer(Modifier.height(PeachSpacing.lg)) Text( "This will permanently delete your vault and all saved passwords from this device. This cannot be undone.", color = peachColors.T1, style = PeachType.body, ) Spacer(Modifier.height(PeachSpacing.xl)) Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm), ) { GhostButton("Cancel", peachColors.T1, onCancel) DangerButton("I understand, continue", enabled = true, onClick = onContinue) } } @Composable private fun Step2(entryCount: Int, onBack: () -> Unit, onContinue: () -> Unit) { Headline("are you sure?", 32.sp, peachColors.Breach) Spacer(Modifier.height(PeachSpacing.lg)) Text( "You will lose access to all $entryCount saved passwords.", color = peachColors.T1, style = PeachType.body, ) Spacer(Modifier.height(PeachSpacing.xl)) Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm), ) { GhostButton("Go back", peachColors.T1, onBack) DangerButton("Yes, destroy it", enabled = true, onClick = onContinue) } } @Composable private fun Step3(onBack: () -> Unit, onDestroy: () -> Unit) { // Chaos: the final step is designed to look like a point of no return. // Bright red header, three single-sentence warnings, one solid red button. // The user has already confirmed at Step1 and Step2; this screen is // deliberately stark rather than carrying a third confirmation gate, so // the only escape is the "No — take me back" line under the button. Column(modifier = Modifier.fillMaxSize()) { // Red header bar — the visual claim that this screen is different. Box( modifier = Modifier .fillMaxWidth() .background(peachColors.Breach) .padding(vertical = 12.dp, horizontal = PeachSpacing.lg), contentAlignment = Alignment.Center, ) { Text( "THIS IS YOUR LAST CHANCE", color = peachColors.ShellTopSolid, style = PeachType.sortPill.copy( fontSize = 12.sp, fontStyle = FontStyle.Italic, ), ) } Spacer(Modifier.height(PeachSpacing.lg)) Text( "Your vault is about to be permanently destroyed.", color = peachColors.T0, style = PeachType.body, ) Spacer(Modifier.height(PeachSpacing.lg)) Text("There is no recovery.", color = peachColors.Breach, style = PeachType.body) Text("There is no support.", color = peachColors.Breach, style = PeachType.body) Text("There is no undo.", color = peachColors.Breach, style = PeachType.body) Spacer(Modifier.height(PeachSpacing.lg)) // Hairline divider, matches the body-copy t3 token used elsewhere. Box( modifier = Modifier .fillMaxWidth() .height(1.dp) .background(peachColors.T3.copy(alpha = 0.25f)), ) Spacer(Modifier.weight(1f)) ChaosDestroyButton("DESTROY MY VAULT FOREVER", onClick = onDestroy) Spacer(Modifier.height(PeachSpacing.sm)) Box( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center, ) { Text( "No — take me back", color = peachColors.T2, style = PeachType.sortPill.copy(fontSize = 11.sp), modifier = Modifier .clip(RoundedCornerShape(999.dp)) .clickable(onClick = onBack) .padding(horizontal = PeachSpacing.lg, vertical = 8.dp), ) } } } @Composable private fun ChaosDestroyButton(label: String, onClick: () -> Unit) { Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(10.dp)) .background(peachColors.Breach) .clickable(onClick = onClick) .padding(vertical = 14.dp), contentAlignment = Alignment.Center, ) { Text( label, color = peachColors.ShellTopSolid, style = PeachType.sortPill.copy( fontSize = 12.sp, fontStyle = FontStyle.Italic, ), ) } } @Composable private fun Headline(text: String, fontSize: androidx.compose.ui.unit.TextUnit, color: Color) { Text( text, color = color, style = TextStyle( fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = fontSize, ), ) } @Composable private fun GhostButton(label: String, color: Color, onClick: () -> Unit) { Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(999.dp)) .border(1.dp, color.copy(alpha = 0.4f), RoundedCornerShape(999.dp)) .clickable(onClick = onClick) .padding(horizontal = PeachSpacing.lg, vertical = 11.dp), contentAlignment = Alignment.Center, ) { Text(label, color = color, style = PeachType.sortPill.copy(fontSize = 11.sp)) } } @Composable private fun DangerButton(label: String, enabled: Boolean, onClick: () -> Unit) { val alpha = if (enabled) 1f else 0.4f Box( modifier = Modifier .fillMaxWidth() .clip(RoundedCornerShape(999.dp)) .background(peachColors.BreachBg.copy(alpha = peachColors.BreachBg.alpha * alpha)) .border(1.dp, peachColors.Breach.copy(alpha = 0.55f * alpha), RoundedCornerShape(999.dp)) .clickable(enabled = enabled, onClick = onClick) .padding(horizontal = PeachSpacing.lg, vertical = 11.dp), contentAlignment = Alignment.Center, ) { Text( label, color = peachColors.Breach.copy(alpha = alpha), style = PeachType.sortPill.copy(fontSize = 11.sp), ) } }