1package com.peachpasswords.ui.settings23import androidx.compose.foundation.background4import androidx.compose.foundation.border5import androidx.compose.foundation.clickable6import androidx.compose.foundation.layout.Arrangement7import androidx.compose.foundation.layout.Box8import androidx.compose.foundation.layout.Column9import androidx.compose.foundation.layout.Spacer10import androidx.compose.foundation.layout.fillMaxSize11import androidx.compose.foundation.layout.fillMaxWidth12import androidx.compose.foundation.layout.height13import androidx.compose.foundation.layout.padding14import androidx.compose.foundation.layout.size15import androidx.compose.foundation.layout.systemBarsPadding16import androidx.compose.foundation.shape.RoundedCornerShape17import androidx.compose.material.icons.Icons18import androidx.compose.material.icons.automirrored.rounded.ArrowBack19import androidx.compose.material3.Icon20import androidx.compose.material3.Text21import androidx.compose.runtime.Composable22import androidx.compose.runtime.CompositionLocalProvider23import androidx.compose.runtime.collectAsState24import androidx.compose.runtime.getValue25import androidx.compose.runtime.mutableStateOf26import androidx.compose.runtime.remember27import androidx.compose.runtime.setValue28import androidx.compose.ui.Alignment29import androidx.compose.ui.Modifier30import androidx.compose.ui.draw.clip31import androidx.compose.ui.graphics.Color32import androidx.compose.ui.text.TextStyle33import androidx.compose.ui.text.font.FontStyle34import androidx.compose.ui.unit.sp35import androidx.compose.ui.unit.dp36import androidx.hilt.navigation.compose.hiltViewModel37import androidx.lifecycle.ViewModel38import androidx.lifecycle.viewModelScope39import com.peachpasswords.ui.components.RuneGridBackground40import com.peachpasswords.ui.components.PeachTopBar41import com.peachpasswords.ui.theme.LocalPeachAccent42import com.peachpasswords.ui.theme.LocalPeachColors43import com.peachpasswords.ui.theme.PeachSpacing44import com.peachpasswords.ui.theme.peachColors45import com.peachpasswords.ui.theme.peachDark46import com.peachpasswords.ui.theme.PeachType47import android.content.Context48import dagger.hilt.android.qualifiers.ApplicationContext49import com.peachpasswords.ui.onboarding.ONBOARD_KEY50import com.peachpasswords.ui.onboarding.ONBOARD_PREFS51import com.peachpasswords.sync.SyncFreshStartResetter52import com.peachpasswords.vault.VaultRepository53import dagger.hilt.android.lifecycle.HiltViewModel54import kotlinx.coroutines.launch55import javax.inject.Inject5657@HiltViewModel58class PanicViewModel @Inject constructor(59 private val vault: VaultRepository,60 private val syncFreshStart: SyncFreshStartResetter,61 @ApplicationContext private val appContext: Context,62) : ViewModel() {63 val entryCount: Int get() = vault.getEntries().size64 private val _destroyFailed = kotlinx.coroutines.flow.MutableStateFlow(false)65 val destroyFailed = _destroyFailed6667 fun destroy(onDone: () -> Unit) = viewModelScope.launch {68 if (!vault.destroyVault()) {69 _destroyFailed.value = true70 return@launch71 }72 syncFreshStart.reset()73 appContext.getSharedPreferences(ONBOARD_PREFS, Context.MODE_PRIVATE)74 .edit().remove(ONBOARD_KEY).apply()75 onDone()76 }77}7879@Composable80fun PanicScreen(81 onBack: () -> Unit,82 onDestroyed: () -> Unit,83 vm: PanicViewModel = hiltViewModel(),84) {85 val destroyFailed by vm.destroyFailed.collectAsState()86 var step by remember { mutableStateOf(1) }87 val entryCount = remember { vm.entryCount }88 89 90 val accent = LocalPeachAccent.current91 val darkPalette = remember(accent) { peachDark(accent) }9293 CompositionLocalProvider(LocalPeachColors provides darkPalette) {94 95 96 Box(modifier = Modifier.fillMaxSize().background(Color(0xFF0C0A06))) {97 98 99 100 if (step == 3) {101 RuneGridBackground(102 modifier = Modifier.fillMaxSize(),103 alpha = 0.22f,104 glowing = true,105 )106 }107 Column(108 modifier = Modifier109 .fillMaxSize()110 .systemBarsPadding()111 .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),112 ) {113 PeachTopBar(114 title = "Panic",115 leading = {116 Icon(117 Icons.AutoMirrored.Rounded.ArrowBack,118 contentDescription = "back",119 tint = peachColors.T0,120 modifier = Modifier121 .size(22.dp)122 .clip(RoundedCornerShape(PeachSpacing.sm))123 .clickable(onClick = onBack),124 )125 },126 )127 Box(128 modifier = Modifier.fillMaxSize().padding(horizontal = PeachSpacing.sm),129 contentAlignment = Alignment.Center,130 ) {131 when {132 destroyFailed -> Text(133 "Peach could not remove every vault artifact. Nothing was marked destroyed; try again.",134 color = peachColors.Breach,135 style = PeachType.body,136 )137 step == 1 -> Step1(onCancel = onBack, onContinue = { step = 2 })138 step == 2 -> Step2(entryCount = entryCount, onBack = { step = 1 }, onContinue = { step = 3 })139 else -> Step3(onBack = { step = 2 }, onDestroy = { vm.destroy(onDestroyed) })140 }141 }142 }143 }144 }145}146147@Composable148private fun Step1(onCancel: () -> Unit, onContinue: () -> Unit) {149 Headline("destroy vault", 44.sp, peachColors.Breach)150 Spacer(Modifier.height(PeachSpacing.lg))151 Text(152 "This will permanently delete your vault and all saved passwords from this device. This cannot be undone.",153 color = peachColors.T1,154 style = PeachType.body,155 )156 Spacer(Modifier.height(PeachSpacing.xl))157 Column(158 modifier = Modifier.fillMaxWidth(),159 verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm),160 ) {161 GhostButton("Cancel", peachColors.T1, onCancel)162 DangerButton("I understand, continue", enabled = true, onClick = onContinue)163 }164}165166@Composable167private fun Step2(entryCount: Int, onBack: () -> Unit, onContinue: () -> Unit) {168 Headline("are you sure?", 32.sp, peachColors.Breach)169 Spacer(Modifier.height(PeachSpacing.lg))170 Text(171 "You will lose access to all $entryCount saved passwords.",172 color = peachColors.T1,173 style = PeachType.body,174 )175 Spacer(Modifier.height(PeachSpacing.xl))176 Column(177 modifier = Modifier.fillMaxWidth(),178 verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm),179 ) {180 GhostButton("Go back", peachColors.T1, onBack)181 DangerButton("Yes, destroy it", enabled = true, onClick = onContinue)182 }183}184185@Composable186private fun Step3(onBack: () -> Unit, onDestroy: () -> Unit) {187 188 189 190 191 192 Column(modifier = Modifier.fillMaxSize()) {193 194 Box(195 modifier = Modifier196 .fillMaxWidth()197 .background(peachColors.Breach)198 .padding(vertical = 12.dp, horizontal = PeachSpacing.lg),199 contentAlignment = Alignment.Center,200 ) {201 Text(202 "THIS IS YOUR LAST CHANCE",203 color = peachColors.ShellTopSolid,204 style = PeachType.sortPill.copy(205 fontSize = 12.sp,206 fontStyle = FontStyle.Italic,207 ),208 )209 }210 Spacer(Modifier.height(PeachSpacing.lg))211 Text(212 "Your vault is about to be permanently destroyed.",213 color = peachColors.T0,214 style = PeachType.body,215 )216 Spacer(Modifier.height(PeachSpacing.lg))217 Text("There is no recovery.", color = peachColors.Breach, style = PeachType.body)218 Text("There is no support.", color = peachColors.Breach, style = PeachType.body)219 Text("There is no undo.", color = peachColors.Breach, style = PeachType.body)220 Spacer(Modifier.height(PeachSpacing.lg))221 222 Box(223 modifier = Modifier224 .fillMaxWidth()225 .height(1.dp)226 .background(peachColors.T3.copy(alpha = 0.25f)),227 )228 Spacer(Modifier.weight(1f))229 ChaosDestroyButton("DESTROY MY VAULT FOREVER", onClick = onDestroy)230 Spacer(Modifier.height(PeachSpacing.sm))231 Box(232 modifier = Modifier.fillMaxWidth(),233 contentAlignment = Alignment.Center,234 ) {235 Text(236 "No — take me back",237 color = peachColors.T2,238 style = PeachType.sortPill.copy(fontSize = 11.sp),239 modifier = Modifier240 .clip(RoundedCornerShape(999.dp))241 .clickable(onClick = onBack)242 .padding(horizontal = PeachSpacing.lg, vertical = 8.dp),243 )244 }245 }246}247248@Composable249private fun ChaosDestroyButton(label: String, onClick: () -> Unit) {250 Box(251 modifier = Modifier252 .fillMaxWidth()253 .clip(RoundedCornerShape(10.dp))254 .background(peachColors.Breach)255 .clickable(onClick = onClick)256 .padding(vertical = 14.dp),257 contentAlignment = Alignment.Center,258 ) {259 Text(260 label,261 color = peachColors.ShellTopSolid,262 style = PeachType.sortPill.copy(263 fontSize = 12.sp,264 fontStyle = FontStyle.Italic,265 ),266 )267 }268}269270@Composable271private fun Headline(text: String, fontSize: androidx.compose.ui.unit.TextUnit, color: Color) {272 Text(273 text,274 color = color,275 style = TextStyle(276 fontFamily = PeachType.Display,277 fontStyle = FontStyle.Italic,278 fontSize = fontSize,279 ),280 )281}282283@Composable284private fun GhostButton(label: String, color: Color, onClick: () -> Unit) {285 Box(286 modifier = Modifier287 .fillMaxWidth()288 .clip(RoundedCornerShape(999.dp))289 .border(1.dp, color.copy(alpha = 0.4f), RoundedCornerShape(999.dp))290 .clickable(onClick = onClick)291 .padding(horizontal = PeachSpacing.lg, vertical = 11.dp),292 contentAlignment = Alignment.Center,293 ) {294 Text(label, color = color, style = PeachType.sortPill.copy(fontSize = 11.sp))295 }296}297298@Composable299private fun DangerButton(label: String, enabled: Boolean, onClick: () -> Unit) {300 val alpha = if (enabled) 1f else 0.4f301 Box(302 modifier = Modifier303 .fillMaxWidth()304 .clip(RoundedCornerShape(999.dp))305 .background(peachColors.BreachBg.copy(alpha = peachColors.BreachBg.alpha * alpha))306 .border(1.dp, peachColors.Breach.copy(alpha = 0.55f * alpha), RoundedCornerShape(999.dp))307 .clickable(enabled = enabled, onClick = onClick)308 .padding(horizontal = PeachSpacing.lg, vertical = 11.dp),309 contentAlignment = Alignment.Center,310 ) {311 Text(312 label,313 color = peachColors.Breach.copy(alpha = alpha),314 style = PeachType.sortPill.copy(fontSize = 11.sp),315 )316 }317}