f7363adae266
feat: Gate Codex generation behind Pro (v2.3.34)
- time
- 2026-06-26 10:18 UTC · 4 months ago
- machine
- koh2
- parent
- bca92a34882a
↓ peach-android-f7363ada.tar.gzatlas at this save
files changed5 files
app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+5 −1app/src/main/java/com/peachpasswords/ui/codex/CodexGenerateScreen.kt+24 −2app/src/main/java/com/peachpasswords/ui/codex/CodexViewModel.kt+9 −0app/src/test/java/com/peachpasswords/ui/codex/CodexGenerateScreenTest.kt+15 −0app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 23330020- versionName "2.3.33"19+ versionCode 23340020+ versionName "2.3.34"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+5 −1
@@ -184,7 +184,11 @@184184 }185185 composable(PeachNav.Settings.route) { SettingsScreen(onBack = back, onNavigate = nav) }186186 composable(PeachNav.CodexGenerate.route) {187- CodexGenerateScreen(onBack = back, onSetupRecovery = { nav(PeachNav.RecoverySetup) })187+ CodexGenerateScreen(188+ onBack = back,189+ onSetupRecovery = { nav(PeachNav.RecoverySetup) },190+ onUpgradePro = { nav(PeachNav.Settings) },191+ )188192 }189193 composable(PeachNav.CodexRestore.route) { CodexRestoreScreen(onBack = back, onRestored = back) }190194 composable(PeachNav.ChirpPair.route) { ChirpPairScreen(onBack = back) }app/src/main/java/com/peachpasswords/ui/codex/CodexGenerateScreen.kt+24 −2
@@ -45,6 +45,7 @@4545 fun CodexGenerateScreen(4646 onBack: () -> Unit,4747 onSetupRecovery: () -> Unit = {},48+ onUpgradePro: () -> Unit = {},4849 vm: CodexViewModel = hiltViewModel(),4950 ) {5051 val state by vm.generate.collectAsStateWithLifecycle()@@ -67,7 +68,18 @@6768 6869 when (val s = state) {6970 is GenerateState.Idle -> {70- if (!vm.recoveryWordsSet) {71+ when (codexGenerateGate(vm.isPro, vm.recoveryWordsSet)) {72+ CodexGenerateGate.PRO_REQUIRED -> {73+ Text("Codex generation is a Pro feature", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)74+ Spacer(Modifier.height(6.dp))75+ Text(76+ "Restoring from an existing Codex is always free. Creating a new printable Codex backup requires Peach Pro.",77+ color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,78+ )79+ Spacer(Modifier.height(20.dp))80+ CodexButton("Open Pro", accent, filled = true, onClick = onUpgradePro)81+ }82+ CodexGenerateGate.RECOVERY_REQUIRED -> {7183 Text("Set up recovery words first", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)7284 Spacer(Modifier.height(6.dp))7385 Text(@@ -76,7 +88,8 @@7688 )7789 Spacer(Modifier.height(20.dp))7890 CodexButton("Set up recovery words", accent, filled = true, onClick = onSetupRecovery)79- } else {91+ }92+ CodexGenerateGate.READY -> {8093 var w3 by remember { mutableStateOf("") }8194 var w7 by remember { mutableStateOf("") }8295 var w11 by remember { mutableStateOf("") }@@ -94,6 +107,7 @@94107 if (ok) accent else peachColors.T2,95108 filled = ok,96109 ) { if (ok) vm.startGenerate(System.currentTimeMillis()) }110+ }97111 }98112 }99113 @@ -133,6 +147,14 @@133147 }134148 }135149 }150+}151+152+internal enum class CodexGenerateGate { PRO_REQUIRED, RECOVERY_REQUIRED, READY }153+154+internal fun codexGenerateGate(isPro: Boolean, recoveryWordsSet: Boolean): CodexGenerateGate = when {155+ !isPro -> CodexGenerateGate.PRO_REQUIRED156+ !recoveryWordsSet -> CodexGenerateGate.RECOVERY_REQUIRED157+ else -> CodexGenerateGate.READY136158 }137159 138160 private fun stepLabel(step: CodexStep): String = when (step) {app/src/main/java/com/peachpasswords/ui/codex/CodexViewModel.kt+9 −0
@@ -13,6 +13,7 @@1313 import com.peachpasswords.codex.CodexPayload1414 import com.peachpasswords.codex.CodexRestore1515 import com.peachpasswords.codex.CodexStep16+import com.peachpasswords.license.LicenseRepository1617 import com.peachpasswords.vault.UnlockResult1718 import com.peachpasswords.vault.VaultRepository1819 import dagger.hilt.android.lifecycle.HiltViewModel@@ -56,6 +57,7 @@5657 class CodexViewModel @Inject constructor(5758 @ApplicationContext private val context: Context,5859 private val vault: VaultRepository,60+ private val license: LicenseRepository,5961 ) : ViewModel() {6062 6163 private val _generate = MutableStateFlow<GenerateState>(GenerateState.Idle)@@ -64,6 +66,9 @@6466 /** D9 gate: Codex generation is allowed only once recovery words are set up. */6567 val recoveryWordsSet: Boolean get() = vault.isRecoveryWordsSet()6668 69+ /** Browser parity: Codex generation is Pro-only, while restore remains free. */70+ val isPro: Boolean get() = license.isProActive()71+6772 private val recoveryWords: List<String> by lazy {6873 vault.syncMasterKey()?.let { k ->6974 try { com.peachpasswords.vault.rootKeyToWords(k) }@@ -84,6 +89,10 @@8489 val restore: StateFlow<RestoreState> = _restore.asStateFlow()8590 8691 fun startGenerate(now: Long) {92+ if (!isPro) {93+ _generate.value = GenerateState.Error("Codex generation is a Pro feature.")94+ return95+ }8796 _generate.value = GenerateState.Running(CodexStep.COMPRESS, 0, 1)8897 viewModelScope.launch {8998 try {app/src/test/java/com/peachpasswords/ui/codex/CodexGenerateScreenTest.kt+15 −0
@@ -0,0 +1,15 @@1+package com.peachpasswords.ui.codex2+3+import org.junit.Assert.assertEquals4+import org.junit.Test5+6+class CodexGenerateScreenTest {7+8+ @Test9+ fun codexGenerateGate_requiresProBeforeRecoveryWords() {10+ assertEquals(CodexGenerateGate.PRO_REQUIRED, codexGenerateGate(isPro = false, recoveryWordsSet = false))11+ assertEquals(CodexGenerateGate.PRO_REQUIRED, codexGenerateGate(isPro = false, recoveryWordsSet = true))12+ assertEquals(CodexGenerateGate.RECOVERY_REQUIRED, codexGenerateGate(isPro = true, recoveryWordsSet = false))13+ assertEquals(CodexGenerateGate.READY, codexGenerateGate(isPro = true, recoveryWordsSet = true))14+ }15+}