-
Notifications
You must be signed in to change notification settings - Fork 306
Show account-creation modal recurrently (every 60 days) for logged-out users #5763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4ee452a
151c63d
77d4395
86c5a0f
c300eeb
d5e9295
0fb8748
99efa04
5999dad
6fcf1ce
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ import au.com.shiftyjelly.pocketcasts.utils.featureflag.providers.FirebaseRemote | |
| import au.com.shiftyjelly.pocketcasts.utils.featureflag.providers.PreferencesFeatureProvider | ||
| import au.com.shiftyjelly.pocketcasts.utils.getVersionCode | ||
| import dagger.hilt.android.qualifiers.ApplicationContext | ||
| import java.time.Instant | ||
| import javax.inject.Inject | ||
| import kotlinx.coroutines.CoroutineScope | ||
| import kotlinx.coroutines.cancel | ||
|
|
@@ -129,7 +130,8 @@ class AppLifecycleObserver( | |
| // new installations default to not displaying the tooltip | ||
| settings.showPodcastsRecentlyPlayedSortOrderTooltip.set(false, updateModifiedAt = false) | ||
|
|
||
| settings.showFreeAccountEncouragement.set(false, updateModifiedAt = false) | ||
| // Anchor the cadence so new installs wait a full interval; upgrading users leave it null. | ||
| settings.freeAccountEncouragementLastShown.set(Instant.now(), updateModifiedAt = false) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (non-blocking) This block is guarded by In practice the worker runs within the first session, so the drift is minutes. Worth knowing, though, that "anchored at install time" is really "anchored at the last process start before migrations ran." |
||
|
|
||
| when (getAppPlatform()) { | ||
| // do nothing because this already defaults to true for all users on automotive | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| package au.com.shiftyjelly.pocketcasts.utils | ||
|
|
||
| import java.time.Duration | ||
| import java.time.Instant | ||
|
|
||
| /** Cadence logic for the recurring account-encouragement modal, kept Android-free so it can be unit-tested. */ | ||
| object AccountEncouragement { | ||
| val interval: Duration = Duration.ofDays(60) | ||
|
|
||
| enum class Decision { | ||
| Show, | ||
| Wait, | ||
| } | ||
|
|
||
| /** | ||
| * Callers record [now] as the new anchor whenever this returns [Decision.Show]. | ||
| * A [lastShown] in the future (backwards clock / skewed-backup restore) shows rather than suppressing indefinitely. | ||
| */ | ||
| fun decide( | ||
| isEligible: Boolean, | ||
| lastShown: Instant?, | ||
| now: Instant, | ||
| interval: Duration = this.interval, | ||
| ): Decision { | ||
| if (!isEligible) return Decision.Wait | ||
| if (lastShown == null || lastShown.isAfter(now)) return Decision.Show | ||
| return if (!now.isBefore(lastShown.plus(interval))) Decision.Show else Decision.Wait | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| package au.com.shiftyjelly.pocketcasts.utils | ||
|
|
||
| import java.time.Duration | ||
| import java.time.Instant | ||
| import org.junit.Assert.assertEquals | ||
| import org.junit.Test | ||
|
|
||
| class AccountEncouragementTest { | ||
| private val interval: Duration = Duration.ofDays(60) | ||
| private val now: Instant = Instant.ofEpochSecond(1_700_000_000) | ||
|
|
||
| @Test | ||
| fun `waits when not eligible`() { | ||
| val decision = AccountEncouragement.decide( | ||
| isEligible = false, | ||
| lastShown = now.minus(interval.multipliedBy(2)), | ||
| now = now, | ||
| interval = interval, | ||
| ) | ||
|
|
||
| assertEquals(AccountEncouragement.Decision.Wait, decision) | ||
| } | ||
|
|
||
| @Test | ||
| fun `shows on first eligible launch`() { | ||
| val decision = AccountEncouragement.decide( | ||
| isEligible = true, | ||
| lastShown = null, | ||
| now = now, | ||
| interval = interval, | ||
| ) | ||
|
|
||
| assertEquals(AccountEncouragement.Decision.Show, decision) | ||
| } | ||
|
|
||
| @Test | ||
| fun `waits before interval elapses`() { | ||
| val decision = AccountEncouragement.decide( | ||
| isEligible = true, | ||
| lastShown = now.minus(interval).plusSeconds(1), | ||
| now = now, | ||
| interval = interval, | ||
| ) | ||
|
|
||
| assertEquals(AccountEncouragement.Decision.Wait, decision) | ||
| } | ||
|
|
||
| @Test | ||
| fun `shows when interval elapsed`() { | ||
| val decision = AccountEncouragement.decide( | ||
| isEligible = true, | ||
| lastShown = now.minus(interval), | ||
| now = now, | ||
| interval = interval, | ||
| ) | ||
|
|
||
| assertEquals(AccountEncouragement.Decision.Show, decision) | ||
| } | ||
|
|
||
| @Test | ||
| fun `shows when interval well exceeded`() { | ||
| val decision = AccountEncouragement.decide( | ||
| isEligible = true, | ||
| lastShown = now.minus(interval.multipliedBy(3)), | ||
| now = now, | ||
| interval = interval, | ||
| ) | ||
|
|
||
| assertEquals(AccountEncouragement.Decision.Show, decision) | ||
| } | ||
|
|
||
| @Test | ||
| fun `shows when anchor is in the future`() { | ||
| // A future anchor means a backwards device clock or a restored skewed backup. | ||
| val decision = AccountEncouragement.decide( | ||
| isEligible = true, | ||
| lastShown = now.plus(interval), | ||
| now = now, | ||
| interval = interval, | ||
| ) | ||
|
|
||
| assertEquals(AccountEncouragement.Decision.Show, decision) | ||
| } | ||
|
|
||
| @Test | ||
| fun `default interval is 60 days`() { | ||
| assertEquals(Duration.ofDays(60), AccountEncouragement.interval) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.