Show account-creation modal recurrently (every 60 days) for logged-out users - #5763
Conversation
…t users Port the iOS recurring "Encourage Account Creation" behaviour (PCIOS-900) to Android. Previously the modal was shown once (a one-shot boolean disabled after the first show, and suppressed entirely for new installs), tied to app updates via AppLifecycleObserver. Replace the one-shot boolean with a 60-day recurring cadence anchored at the first eligible launch: - Add AccountEncouragement (utils): a pure, testable decide(isEligible, lastShown, now, interval) -> Show/Anchor/Wait, with a future-anchor re-anchor guard for clock skew. - Replace Settings.showFreeAccountEncouragement (Bool) with freeAccountEncouragementLastShown (Instant?) — the cadence anchor. - MainActivity.encourageAccountCreation() now gates on the new ENCOURAGE_ACCOUNT_CREATION feature flag, computes eligibility (logged out and past onboarding), and drives the Show/Anchor/Wait decision on a 60-day clock; the first eligible launch only anchors so we don't collide with onboarding. - Drop the new-install suppression in AppLifecycleObserver so logged-out fresh installs become eligible after 60 days, matching iOS "every logged-out user". - Add the ENCOURAGE_ACCOUNT_CREATION feature flag (Firebase remote + dev toggle). Adds AccountEncouragementTest covering all decision branches (including clock skew) and updates AppLifecycleObserverTest for the removed setting. Part of DSGPOC-88. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
This comment was marked as outdated.
Previously the first eligible launch only anchored the 60-day clock, so nothing showed for 60 days after release. Show the modal on the first eligible launch instead (eligibility already requires completed onboarding via hasCompletedOnboarding()), then every 60 days. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hasCompletedOnboarding() flips to true the moment initial onboarding finishes, so with the show-on-first-launch behaviour the modal appeared the instant a fresh install dismissed onboarding. Track whether initial onboarding was launched this session and skip the modal on that launch; it shows on the next launch instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
With show-on-first-launch, a fresh install that finished initial onboarding without signing up saw the account-creation modal on the very next launch. Anchor the 60-day clock when initial onboarding finishes without an account (logged out), so the modal waits a full interval. Existing logged-out users keep the first-launch prompt. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Encourage Account Creation modal was presented unconditionally and reset its 60-day cadence clock before presenting. If another bottom sheet (What's New, End of Year, etc.) was already showing, the EAC modal stacked over it and the clock was still reset — burning the interval for a modal the user never saw. Guard on bottomSheetTag so EAC defers and retries on the next eligible launch, mirroring the End-of-Year modal's isWhatsNewShowing() check. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
# Conflicts: # app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt
This comment was marked as outdated.
This comment was marked as outdated.
geekygecko
left a comment
There was a problem hiding this comment.
Looks good!
Is it showing the message too soon after they first install the app? They’ve already decided not to create an account, but then the next time they open the app, they’re presented with this page.
account.mp4
| } else { | ||
| openOnboardingFlow(OnboardingFlow.AccountEncouragement) | ||
| // Eligible = logged out and past initial onboarding. Shown on the first eligible | ||
| // launch, then every 60 days while the user stays logged out. |
There was a problem hiding this comment.
Would it be possible to add to the prompt for Opus to reduce the comments to one line? It seems to be overexplaining and producing comments that might not be necessary.
There was a problem hiding this comment.
Yes! Updated here: 6fcf1ce. I wish we could use a shared Claude memory
Fresh installs now stamp freeAccountEncouragementLastShown at install time in AppLifecycleObserver, so the modal waits a full 60-day interval before its first show. This matches iOS, which anchors the cadence in AppDelegate's `case .installed`; existing users upgrading leave the anchor null and still see the modal on their first eligible launch. Replaces the previous anchoring in MainActivity's onboarding-completion handler, which was gated on the in-memory launchedInitialOnboarding flag and so failed to anchor across process death or an exit that skipped the callback -- leaving lastShown null and showing the modal on the next launch after a fresh install. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
|
Thanks for the review, @geekygecko . You're right about the modal appearing first launch after declining account creation in onboarding, and this turned out to be a divergence from iOS rather than intended behaviour. Fixed in 99efa04 by anchoring on fresh install in I've also corrected the PR description, which claimed dropping the new-install suppression "matched iOS" — that was backwards. |
| // Anchor the account-encouragement cadence on fresh install so the modal waits a full | ||
| // interval before its first show (existing users upgrading leave it null and see it | ||
| // immediately). | ||
| settings.freeAccountEncouragementLastShown.set(Instant.now(), updateModifiedAt = false) |
There was a problem hiding this comment.
(non-blocking) This block is guarded by getMigratedVersionCode() == 0, and that value is only written by VersionMigrationsWorker (VersionMigrationsWorker.kt:207), which is enqueued asynchronously from PocketCastsApplication.setupApp(). Every process start before the worker completes re-runs handleNewInstallOrUpgrade() — the other new-install writes here are idempotent booleans, but a timestamp isn't, so the anchor slides forward on each of those starts.
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."
The `encourage_account_creation` Remote Config parameter still exists from the 2025 one-shot experiment (added in #3865, code removed in #4595). Removing the enum did not remove the parameter, so reusing the key means its stale value overrides `defaultValue = true` and the recurring modal could ship silently disabled or partially rolled out. Point the flag at `encourage_account_creation_recurring` for a clean slate with no inherited value or rollout condition. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This comment was marked as outdated.
This comment was marked as outdated.
Drop comments from `when` branches and enum cases, remove test comments that restated their own test names, and cut the remaining blocks to a single line. Also removes a stale KDoc reference to a Decision.Anchor case that no longer exists. The rationale for the deferral and clock-reset ordering lives in the commit history and PR description rather than inline. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Claude finished @yaelirub's task in 1m 13s —— View job Review — commits since last pass (
|
I think it's good that it shows up in the next foreground after sign-out as most sign outs are accidental |
Description
Port the iOS recurring "Encourage Account Creation" (EAC) modal to Android.
Background: signup conversions have been trending down. The EAC modal — the "We noticed you're not logged in" prompt with the sync / backups / recommendations benefits — had a positive impact on signups when it ran in 2025, but it was a one-shot (shown once then disabled, and suppressed entirely for new installs) tied to app updates. This makes it recurring: shown to logged-out users on a 60-day cadence, decoupled from app updates, matching the iOS change (pocket-casts-ios#4985 / PCIOS-900).
What changed
AccountEncouragement(utils, new) — pure, unit-testable cadence logic:decide(isEligible, lastShown, now, interval) → Show / Wait, with a 60-day interval. Shows on the first eligible launch (no anchor yet), once the interval elapses, or when the stored anchor is in the future (backwards device clock / restored skewed backup, which would otherwise suppress the modal indefinitely).Settings— replaced the one-shotshowFreeAccountEncouragement: BooleanwithfreeAccountEncouragementLastShown: Instant?(the cadence anchor).MainActivity.encourageAccountCreation()— gates on the newENCOURAGE_ACCOUNT_CREATIONfeature flag, computes eligibility (logged out and past onboarding viahasCompletedOnboarding()), and shows the modal on the first eligible launch, then every 60 days while the user stays logged out. The anchor is recorded when the modal is shown. The post-restore login prompt anchors the clock so it doesn't double-show.AppLifecycleObserver— fresh installs anchor the cadence at install time, so the modal waits a full 60-day interval before its first show (matching iOS, which anchors inAppDelegate'scase .installed). Existing users upgrading leave the anchornulland see it on the first eligible launch.Feature— addedENCOURAGE_ACCOUNT_CREATION(default on, Firebase remote flag + dev toggle) for parity with the iOS feature flag / a remote kill-switch.Signed-in users never see it; existing inline login prompts are unchanged.
Part of DSGPOC-88.
Fixes PCDROID-728
Testing Instructions
The real interval is 60 days; to exercise the cadence quickly, use a debug build and adjust the device clock, or clear app data between runs.
ENCOURAGE_ACCOUNT_CREATIONfeature flag is enabled (Beta Features screen).AccountBenefitsFragment) shows instead of the full onboarding flow.Screenshots or Screencast
Checklist
./gradlew spotlessApplyto automatically apply formatting/linting)modules/services/localization/src/main/res/values/strings.xmlI have tested any UI changes...
🤖 Generated with Claude Code