Skip to content

Show account-creation modal recurrently (every 60 days) for logged-out users - #5763

Merged
yaelirub merged 10 commits into
mainfrom
eac-60-day-recurring
Aug 28, 2026
Merged

Show account-creation modal recurrently (every 60 days) for logged-out users#5763
yaelirub merged 10 commits into
mainfrom
eac-60-day-recurring

Conversation

@yaelirub

@yaelirub yaelirub commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

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-shot showFreeAccountEncouragement: Boolean with freeAccountEncouragementLastShown: Instant? (the cadence anchor).
  • MainActivity.encourageAccountCreation() — gates on the new ENCOURAGE_ACCOUNT_CREATION feature flag, computes eligibility (logged out and past onboarding via hasCompletedOnboarding()), 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 in AppDelegate's case .installed). Existing users upgrading leave the anchor null and see it on the first eligible launch.
  • Feature — added ENCOURAGE_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.

  1. Ensure the ENCOURAGE_ACCOUNT_CREATION feature flag is enabled (Beta Features screen).
  2. On a logged-out device that has completed initial onboarding, launch the app → confirm the EAC modal appears on this first eligible launch.
    • On a fresh install, the cadence is anchored at install time, so the modal does not appear on the next launch — it waits a full interval.
  3. Dismiss the modal and relaunch within the interval → confirm it does not reappear.
  4. Advance past the 60-day interval (device clock / data reset) and relaunch → confirm it appears again.
  5. Log in and relaunch → confirm the modal never appears for a signed-in user.
  6. Tablet: confirm the benefits dialog (AccountBenefitsFragment) shows instead of the full onboarding flow.

Note: unit tests for the new AccountEncouragement decision logic pass (:modules:services:utils:testDebugUnitTest --tests "*AccountEncouragementTest"). The :app module compiles cleanly. I was unable to run :modules:features:shared unit tests locally due to a pre-existing :modules:services:crashlogging KSP/Dagger build failure (cannot find symbol: class CrashLogging) that reproduces on main without these changes — flagging separately.

Screenshots or Screencast

Checklist

  • If this is a user-facing change, I have added an entry in CHANGELOG.md
  • Ensure the linter passes (./gradlew spotlessApply to automatically apply formatting/linting)
  • I have considered whether it makes sense to add tests for my changes
  • All strings that need to be localized are in modules/services/localization/src/main/res/values/strings.xml
  • Any jetpack compose components I added or changed are covered by compose previews
  • I have updated (or requested that someone edit) the Event Horizon schema to reflect any new or changed analytics.

I have tested any UI changes...

🤖 Generated with Claude Code

yaelirub and others added 2 commits August 19, 2026 20:32
…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>
@yaelirub
yaelirub requested a review from a team as a code owner August 20, 2026 20:36
@yaelirub
yaelirub requested review from geekygecko and a lite review from Copilot and removed request for a team August 20, 2026 20:36
@claude

This comment was marked as outdated.

This comment was marked as outdated.

Comment thread app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt Outdated
Comment thread app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt
Comment thread app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt Outdated
@yaelirub yaelirub added this to the 8.19 ❄️ milestone Aug 21, 2026
@yaelirub
yaelirub marked this pull request as draft August 21, 2026 02:38
@yaelirub
yaelirub marked this pull request as ready for review August 21, 2026 02:42
@claude

This comment was marked as outdated.

@yaelirub
yaelirub marked this pull request as draft August 21, 2026 02:42
yaelirub and others added 4 commits August 20, 2026 20:01
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>
@yaelirub yaelirub modified the milestones: 8.19 ❄️, 8.20 Aug 26, 2026
# Conflicts:
#	app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt
@yaelirub
yaelirub marked this pull request as ready for review August 26, 2026 01:35
@claude

This comment was marked as outdated.

Comment thread app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt
Comment thread app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt
Comment thread app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt Outdated
@geekygecko geekygecko added [Type] Enhancement Improve an existing feature. [Area] Onboarding labels Aug 27, 2026

@geekygecko geekygecko left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@claude

This comment was marked as outdated.

@yaelirub

yaelirub commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

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 AppLifecycleObserver (the direct analogue of the iOS hook) and removing the fragile completion-handler anchoring. Added coverage for both halves: new installs anchor, upgrades don't.

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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(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."

Comment thread app/src/main/java/au/com/shiftyjelly/pocketcasts/ui/MainActivity.kt Outdated
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>
@claude

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

claude Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Claude finished @yaelirub's task in 1m 13s —— View job


Review — commits since last pass (5999dad, 6fcf1ce)

The two new commits are the Firebase key rename (encourage_account_creationencourage_account_creation_recurring, unique in the enum) and comment trimming. Both are clean, and the trimming didn't remove anything load-bearing. No new findings.

Three findings from earlier passes are still open on the current tree:

Blocking

  • app/.../ui/MainActivity.kt:717 — signing out still re-triggers the modal on the next foreground (see the existing thread on this block). The anchor is only written on show / restore / fresh install, so a user signed in since before this ships has a null anchor; sign out → background → return gives them "We noticed you're not logged in" immediately. Either anchor on sign-out or confirm iOS does the same.

Non-blocking

  • app/.../ui/MainActivity.kt:499,717updateModifiedAt = true on both writes is a dead write; this pref isn't in SyncSettingsTask, and AppReviewManagerImpl uses false for the same pattern. Fix this →
  • app/.../ui/MainActivity.kt:361launchedInitialOnboarding is still not persisted in onSaveInstanceState (line 789). The fresh-install anchor added in 99efa04 closes the common case, so this now only bites a pre-99efa04 install that never finished onboarding and is recreated mid-onboarding — much narrower than when I first raised it, but the guard still silently disappears on recreation.
    · branch eac-60-day-recurring

@yaelirub

Copy link
Copy Markdown
Collaborator Author
  • app/.../ui/MainActivity.kt:717 — signing out still re-triggers the modal on the next foreground (see the existing thread on this block). The anchor is only written on show / restore / fresh install, so a user signed in since before this ships has a null anchor; sign out → background → return gives them "We noticed you're not logged in" immediately. Either anchor on sign-out or confirm iOS does the same.

I think it's good that it shows up in the next foreground after sign-out as most sign outs are accidental

@yaelirub
yaelirub merged commit 2c6e599 into main Aug 28, 2026
21 checks passed
@yaelirub
yaelirub deleted the eac-60-day-recurring branch August 28, 2026 01:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants