31256f603665

feat: Show active Pro license details (v2.3.27)

- Parse license issued date from the signed payload - Expose active license key metadata to settings - Add masked reveal/copy controls and active-since copy to the Pro card

time
2026-06-26 08:57 UTC · 4 months ago
machine
koh2
parent
becd92652892
4 files app/build.gradle+2 −2app/src/main/java/com/peachpasswords/license/LicenseRepository.kt+14 −0app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+39 −1app/src/main/java/com/peachpasswords/ui/settings/SettingsViewModel.kt+10 −0
app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616         applicationId "com.peachpasswords"1717         minSdk 261818         targetSdk 3519-        versionCode 23260020-        versionName "2.3.26"19+        versionCode 23270020+        versionName "2.3.27"2121 2222         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323         vectorDrawables {

view file

app/src/main/java/com/peachpasswords/license/LicenseRepository.kt+14 −0
@@ -52,6 +52,8 @@5252 5353     fun getLicenseKey(): String? = prefs.getString(KEY_LICENSE, null)5454 55+    fun getLicenseIssuedAtMillis(): Long? = getLicenseKey()?.let(::licenseIssuedAtMillis)56+5557     fun clearLicense() = prefs.edit().remove(KEY_LICENSE).apply()5658 5759     fun isProCelebrated(): Boolean = prefs.getBoolean(KEY_PRO_CELEBRATED, false)@@ -75,6 +77,18 @@7577         } catch (_: Exception) {7678             false7779         }80+    }81+82+    fun licenseIssuedAtMillis(licenseKey: String): Long? {83+        val decoded = decodeLicenseKey(licenseKey) ?: return null84+        if (decoded.size != TOTAL_SIZE) return null85+        val payload = decoded.copyOfRange(0, PAYLOAD_SIZE)86+        if (payload[0] != PRODUCT_TAG || payload[1] != PLAN_TAG || payload[2] != VERSION_TAG) return null87+        val days = (payload[3].toInt() and 0xff) or88+            ((payload[4].toInt() and 0xff) shl 8) or89+            ((payload[5].toInt() and 0xff) shl 16) or90+            ((payload[6].toInt() and 0xff) shl 24)91+        return days.toLong() * 86_400_000L7892     }7993 8094     private fun decodeLicenseKey(key: String): ByteArray? {

view file

app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+39 −1
@@ -29,6 +29,7 @@2929 import androidx.compose.runtime.collectAsState3030 import androidx.compose.runtime.getValue3131 import com.peachpasswords.feedback.FeedbackState32+import com.peachpasswords.shared.PeachClipboard3233 import com.peachpasswords.sync.SyncStatus3334 import com.peachpasswords.ui.theme.LocalPeachAccent3435 import com.peachpasswords.vault.DuressAction@@ -59,6 +60,7 @@5960 import com.peachpasswords.vault.VaultSettings6061 import com.peachpasswords.vault.VaultSnapshot6162 import kotlinx.coroutines.launch63+import kotlinx.coroutines.delay6264 import java.text.SimpleDateFormat6365 import java.util.Date6466 import java.util.Locale@@ -854,7 +856,13 @@854856 855857 @Composable856858 private fun ProActiveBody(fg: Color, vm: SettingsViewModel) {859+    val ctx = LocalContext.current857860     val features = listOf("Peach Sync", "Peach Codex", "Passkeys", "Advanced Vault Health", "Canary entries", "Duress vault")861+    val licenseKey by vm.licenseKey.collectAsState()862+    val issuedAt by vm.licenseIssuedAt.collectAsState()863+    var showKey by remember { mutableStateOf(false) }864+    var copied by remember { mutableStateOf(false) }865+    val scope = rememberCoroutineScope()858866     Box(Modifier.fillMaxWidth().padding(vertical = 8.dp), contentAlignment = Alignment.Center) {859867         com.peachpasswords.ui.components.PeachMark(size = 60.dp)860868     }@@ -865,11 +873,35 @@865873         textAlign = androidx.compose.ui.text.style.TextAlign.Center,866874     )867875     Text(868-        "active · yours forever",876+        formatLicenseActiveSince(issuedAt),869877         modifier = Modifier.fillMaxWidth().padding(top = 4.dp),870878         color = fg.copy(alpha = 0.6f), fontFamily = PeachType.Mono, fontSize = 9.sp, letterSpacing = 0.18.em,871879         textAlign = androidx.compose.ui.text.style.TextAlign.Center,872880     )881+    licenseKey?.let { key ->882+        Spacer(Modifier.height(12.dp))883+        Text("LICENSE KEY", color = fg.copy(alpha = 0.6f), fontFamily = PeachType.Mono, fontSize = 8.sp, letterSpacing = 0.22.em)884+        RowLine(fg) {885+            Text(886+                if (showKey) key else "************************",887+                color = fg.copy(alpha = if (showKey) 0.82f else 0.58f),888+                fontFamily = PeachType.Mono,889+                fontSize = 10.sp,890+                modifier = Modifier.weight(1f),891+            )892+            Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {893+                SubtleButton(if (copied) "copied" else "copy", fg) {894+                    PeachClipboard.copy(ctx, "peach license", key)895+                    copied = true896+                    scope.launch {897+                        delay(1_500)898+                        copied = false899+                    }900+                }901+                SubtleButton(if (showKey) "hide" else "reveal", fg) { showKey = !showKey }902+            }903+        }904+    }873905     Spacer(Modifier.height(12.dp))874906     features.forEachIndexed { i, name ->875907         RowLine(fg) {@@ -888,6 +920,12 @@888920             .padding(vertical = 12.dp),889921         contentAlignment = Alignment.Center,890922     ) { Text("Deactivate", color = com.peachpasswords.ui.theme.peachColors.Breach, fontFamily = PeachType.Mono, fontSize = 12.sp) }923+}924+925+private fun formatLicenseActiveSince(issuedAt: Long?): String {926+    if (issuedAt == null) return "Active"927+    val monthYear = SimpleDateFormat("MMMM yyyy", Locale.getDefault()).format(Date(issuedAt))928+    return "Active since $monthYear"891929 }892930 893931 /* ──────────────────────────────  Feedback  ───────────────────────────────── */

view file

app/src/main/java/com/peachpasswords/ui/settings/SettingsViewModel.kt+10 −0
@@ -188,6 +188,12 @@188188     private val _isPro = MutableStateFlow(license.isProActive())189189     val isPro: StateFlow<Boolean> = _isPro.asStateFlow()190190 191+    private val _licenseKey = MutableStateFlow(license.getLicenseKey())192+    val licenseKey: StateFlow<String?> = _licenseKey.asStateFlow()193+194+    private val _licenseIssuedAt = MutableStateFlow(license.getLicenseIssuedAtMillis())195+    val licenseIssuedAt: StateFlow<Long?> = _licenseIssuedAt.asStateFlow()196+191197     private val _activating = MutableStateFlow(false)192198     val activating: StateFlow<Boolean> = _activating.asStateFlow()193199 @@ -212,6 +218,8 @@212218             if (license.validateLicense(key)) {213219                 license.storeLicense(key)214220                 _isPro.value = true221+                _licenseKey.value = key222+                _licenseIssuedAt.value = license.licenseIssuedAtMillis(key)215223                 _showCelebration.value = true216224             } else {217225                 _activationError.value = "Invalid license key"@@ -230,6 +238,8 @@230238     fun deactivate() {231239         license.clearLicense()232240         _isPro.value = false241+        _licenseKey.value = null242+        _licenseIssuedAt.value = null233243     }234244 235245     private fun mutate(block: (VaultSettings) -> VaultSettings) {

view file