f7363adae266

feat: Gate Codex generation behind Pro (v2.3.34)

- Add defensive Pro gate to Codex generation ViewModel and screen - Keep Codex restore free while routing non-Pro generation users toward Settings - Pin gate ordering with a focused test Verified: ./gradlew compileDebugKotlin testDebugUnitTest --tests com.peachpasswords.ui.codex.CodexGenerateScreenTest Verified: ./gradlew testDebugUnitTest :app:assembleDebug lintDebug

time
2026-06-26 10:18 UTC · 4 months ago
machine
koh2
parent
bca92a34882a
5 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 −0
app/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 {

view file

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) }

view file

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) {

view file

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 {

view file

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+}

view file