31256f603665
feat: Show active Pro license details (v2.3.27)
- time
- 2026-06-26 08:57 UTC · 4 months ago
- machine
- koh2
- parent
- becd92652892
↓ peach-android-31256f60.tar.gzatlas at this save
files changed4 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 −0app/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 {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? {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 ───────────────────────────────── */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) {