c592f947ec77
feat: Add pending Pro passkey notice (v2.3.50)
- time
- 2026-06-26 14:14 UTC · 4 months ago
- machine
- koh2
- parent
- 6c48f1b25b6e
↓ peach-android-c592f947.tar.gzatlas at this save
files changed6 files
app/build.gradle+2 −2app/src/main/java/com/peachpasswords/license/ProNoticeStore.kt+53 −0app/src/main/java/com/peachpasswords/passkeys/PeachCredentialProviderService.kt+15 −0app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+49 −0app/src/main/java/com/peachpasswords/ui/vault/VaultListViewModel.kt+8 −0app/src/test/java/com/peachpasswords/license/ProNoticeStoreTest.kt+17 −0app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 23490020- versionName "2.3.49"19+ versionCode 23500020+ versionName "2.3.50"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/license/ProNoticeStore.kt+53 −0
@@ -0,0 +1,53 @@1+package com.peachpasswords.license2+3+import android.content.Context4+import dagger.hilt.android.qualifiers.ApplicationContext5+import kotlinx.coroutines.flow.MutableStateFlow6+import kotlinx.coroutines.flow.StateFlow7+import kotlinx.coroutines.flow.asStateFlow8+import javax.inject.Inject9+import javax.inject.Singleton10+11+data class PendingProNotice(12+ val feature: String,13+ val triggeredAt: Long,14+)15+16+@Singleton17+class ProNoticeStore @Inject constructor(18+ @ApplicationContext context: Context,19+) {20+ private val prefs = context.getSharedPreferences(PREFS_FILE, Context.MODE_PRIVATE)21+ private val _notice = MutableStateFlow(load())22+ val notice: StateFlow<PendingProNotice?> = _notice.asStateFlow()23+24+ fun record(feature: ProFeature, nowMs: Long = System.currentTimeMillis()) {25+ val next = PendingProNotice(feature.key, nowMs)26+ prefs.edit()27+ .putString(KEY_FEATURE, next.feature)28+ .putLong(KEY_TRIGGERED_AT, next.triggeredAt)29+ .apply()30+ _notice.value = next31+ }32+33+ fun dismiss() {34+ prefs.edit().remove(KEY_FEATURE).remove(KEY_TRIGGERED_AT).apply()35+ _notice.value = null36+ }37+38+ private fun load(): PendingProNotice? {39+ val feature = prefs.getString(KEY_FEATURE, null)?.takeIf { it.isNotBlank() } ?: return null40+ val triggeredAt = prefs.getLong(KEY_TRIGGERED_AT, 0L).takeIf { it > 0L } ?: return null41+ return PendingProNotice(feature, triggeredAt)42+ }43+44+ private companion object {45+ const val PREFS_FILE = "peach_pro_notice"46+ const val KEY_FEATURE = "feature"47+ const val KEY_TRIGGERED_AT = "triggered_at"48+ }49+}50+51+fun proNoticeText(notice: PendingProNotice): String =52+ if (notice.feature == ProFeature.Sync.key) "Sync requires Peach Pro"53+ else "A Pro feature requires Peach Pro"app/src/main/java/com/peachpasswords/passkeys/PeachCredentialProviderService.kt+15 −0
@@ -20,6 +20,9 @@2020 import androidx.credentials.exceptions.GetCredentialException2121 import androidx.annotation.RequiresApi2222 import com.peachpasswords.R23+import com.peachpasswords.license.LicenseRepository24+import com.peachpasswords.license.ProFeature25+import com.peachpasswords.license.ProNoticeStore2326 import com.peachpasswords.vault.VaultRepository2427 import dagger.hilt.android.AndroidEntryPoint2528 import java.time.Instant@@ -42,6 +45,8 @@4245 class PeachCredentialProviderService : CredentialProviderService() {4346 4447 @Inject lateinit var vault: VaultRepository48+ @Inject lateinit var license: LicenseRepository49+ @Inject lateinit var proNoticeStore: ProNoticeStore4550 4651 override fun onBeginGetCredentialRequest(4752 request: BeginGetCredentialRequest,@@ -52,6 +57,11 @@5257 callback.onResult(BeginGetCredentialResponse())5358 return5459 }60+ if (!license.hasFeature(ProFeature.Passkeys)) {61+ proNoticeStore.record(ProFeature.Passkeys)62+ callback.onResult(BeginGetCredentialResponse())63+ return64+ }5565 if (!vault.isUnlocked.value) {5666 callback.onResult(5767 BeginGetCredentialResponse(@@ -101,6 +111,11 @@101111 ) {102112 val publicKeyRequest = request as? BeginCreatePublicKeyCredentialRequest103113 if (publicKeyRequest == null || !vault.settings.value.passkeyProviderEnabled || !vault.isUnlocked.value) {114+ callback.onResult(BeginCreateCredentialResponse())115+ return116+ }117+ if (!license.hasFeature(ProFeature.Passkeys)) {118+ proNoticeStore.record(ProFeature.Passkeys)104119 callback.onResult(BeginCreateCredentialResponse())105120 return106121 }app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+49 −0
@@ -43,6 +43,7 @@4343 import androidx.compose.material.icons.rounded.Search4444 import androidx.compose.material.icons.rounded.Settings4545 import androidx.compose.material3.Icon46+import androidx.compose.material3.TextButton4647 import androidx.compose.material3.Snackbar4748 import androidx.compose.material3.SnackbarDuration4849 import androidx.compose.material3.SnackbarHost@@ -82,6 +83,8 @@8283 import androidx.compose.ui.unit.sp8384 import androidx.hilt.navigation.compose.hiltViewModel8485 import androidx.compose.runtime.collectAsState86+import com.peachpasswords.license.PendingProNotice87+import com.peachpasswords.license.proNoticeText8588 import com.peachpasswords.sync.SyncAlert8689 import com.peachpasswords.haptics.PeachHaptics8790 import com.peachpasswords.sync.syncAlertMessage@@ -117,6 +120,7 @@117120 val selectedIds by vm.selectedIds.collectAsState()118121 val undoEvent by vm.undoEvent.collectAsState()119122 val syncAlerts by vm.syncAlerts.collectAsState()123+ val proNotice by vm.proNotice.collectAsState()120124 val snackbarHostState = remember { SnackbarHostState() }121125 var menuEntry by remember { mutableStateOf<VaultEntry?>(null) }122126 val view = LocalView.current@@ -156,6 +160,14 @@156160 alerts = syncAlerts,157161 onDismiss = vm::dismissSyncAlert,158162 )163+ ProNoticeBanner(164+ notice = proNotice,165+ onLearnMore = {166+ vm.dismissProNotice()167+ onNavigate(PeachNav.Settings)168+ },169+ onDismiss = vm::dismissProNotice,170+ )159171 SearchBar(160172 value = search,161173 onChange = vm::setSearch,@@ -234,6 +246,43 @@234246 onEdit = { onNavigate(PeachNav.EntryDetail(e.id)) },235247 onMoveToTrash = { vm.softDelete(e) },236248 onDismiss = { menuEntry = null },249+ )250+ }251+}252+253+@Composable254+private fun ProNoticeBanner(255+ notice: PendingProNotice?,256+ onLearnMore: () -> Unit,257+ onDismiss: () -> Unit,258+) {259+ if (notice == null) return260+ Row(261+ modifier = Modifier262+ .fillMaxWidth()263+ .background(LocalPeachAccent.current.copy(alpha = 0.10f))264+ .border(0.5.dp, LocalPeachAccent.current.copy(alpha = 0.24f))265+ .padding(start = 16.dp, top = 4.dp, end = 8.dp, bottom = 4.dp),266+ verticalAlignment = Alignment.CenterVertically,267+ ) {268+ Text(269+ text = proNoticeText(notice),270+ color = peachColors.T1,271+ style = PeachType.labelMono.copy(fontSize = 10.sp),272+ modifier = Modifier.weight(1f),273+ )274+ TextButton(onClick = onLearnMore) {275+ Text("Learn more", color = LocalPeachAccent.current, style = PeachType.labelMono.copy(fontSize = 10.sp))276+ }277+ Icon(278+ Icons.Rounded.Close,279+ contentDescription = "Dismiss Pro notice",280+ tint = peachColors.T2,281+ modifier = Modifier282+ .size(28.dp)283+ .clip(CircleShape)284+ .clickable { onDismiss() }285+ .padding(7.dp),237286 )238287 }239288 }app/src/main/java/com/peachpasswords/ui/vault/VaultListViewModel.kt+8 −0
@@ -4,6 +4,8 @@44 import com.peachpasswords.shared.PeachClipboard55 import androidx.lifecycle.ViewModel66 import androidx.lifecycle.viewModelScope7+import com.peachpasswords.license.PendingProNotice8+import com.peachpasswords.license.ProNoticeStore79 import com.peachpasswords.sync.SyncAlert810 import com.peachpasswords.sync.SyncAlertStore911 import com.peachpasswords.vault.TOTPGenerator@@ -35,6 +37,7 @@3537 private val vault: VaultRepository,3638 private val lockTimer: VaultLockTimer,3739 private val syncAlertStore: SyncAlertStore,40+ private val proNoticeStore: ProNoticeStore,3841 @ApplicationContext private val appContext: Context,3942 ) : ViewModel() {4043 @@ -55,6 +58,7 @@5558 5659 val isUnlocked: StateFlow<Boolean> = vault.isUnlocked5760 val syncAlerts: StateFlow<List<SyncAlert>> = syncAlertStore.alerts61+ val proNotice: StateFlow<PendingProNotice?> = proNoticeStore.notice5862 5963 /** Live, filtered + sorted active entries. */6064 val entries: StateFlow<List<VaultEntry>> =@@ -84,6 +88,10 @@8488 8589 fun dismissSyncAlert(alert: SyncAlert) {8690 syncAlertStore.dismiss(alert)91+ }92+93+ fun dismissProNotice() {94+ proNoticeStore.dismiss()8795 }8896 8997 // ── Multi-select ────────────────────────────────────────────────────────app/src/test/java/com/peachpasswords/license/ProNoticeStoreTest.kt+17 −0
@@ -0,0 +1,17 @@1+package com.peachpasswords.license2+3+import org.junit.Assert.assertEquals4+import org.junit.Test5+6+class ProNoticeStoreTest {7+ @Test fun pro_notice_copy_matches_browser_feature_labels() {8+ assertEquals(9+ "Sync requires Peach Pro",10+ proNoticeText(PendingProNotice(ProFeature.Sync.key, triggeredAt = 1L)),11+ )12+ assertEquals(13+ "A Pro feature requires Peach Pro",14+ proNoticeText(PendingProNotice(ProFeature.Passkeys.key, triggeredAt = 1L)),15+ )16+ }17+}