Skip to content

Sell a yearly pass in Lite, and ask for support in the reader - #740

Merged
andiwand merged 4 commits into
mainfrom
yearly-pass
Oct 25, 2026
Merged

andiwand merged 4 commits into
mainfrom
yearly-pass

Conversation

@andiwand

@andiwand andiwand commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

🤖 Generated with Claude Code

Summary

Lite sells one auto-renewing subscription, pro_pass. It removes the ads and unlocks the Pro features for one year. The decision note is in headquarters: notes/2026-10-25_yearly-pass.md.

  • The sheet. MainActivity.offerPass shows a title, the gate's feature text (only from the gate), one statement about what the money pays for, the pass with the price from Play, the renewal terms, and a link to the Pro app. The sheet replaces the "Part of Pro" dialog. Without Play or without the product, the sheet shows only the Pro link.
  • Entry points.
    • Lite and Foss show "Support the app" above the folds on the landing screen, and as the last row of the document actions.
    • In Lite, the house ad and the fullscreen snackbar also open the sheet.
    • Foss goes to GitHub Sponsors.
    • Pro keeps only its sponsor row in the folded settings.
  • Entitlement. BillingManager asks Play at each launch, because a pass ends. Entitlement.of maps the owned products:
    • The pass gives Pro.
    • The old lifetime purchases (remove_ads_for_eva, love_and_everything) give Pro.
    • The old remove_ads_for_1y and remove_ads_subscription keep only the ad removal.
  • When a pass ends. The change takes effect at the next launch, because AdManager does not turn back on once it removes the ads.
  • Editing scope. Features.advancedEditing now reads the entitlement. A new pass renders the open document again, because CoreLoader sets the editing scope while it translates. If the edit mode is on, the render waits until the edit mode ends, so no edit is lost.
  • Features.onPlay. This flag comes from the review/noReview source sets and separates Pro from Foss.
  • Removed. OwnedPurchases, the "Remove advertisements" string in all locales, and ic_block.

The reader entry is a row in the document actions, not a link beside the banner. AdMob policy discourages tappable elements close to an ad, because they cause accidental clicks.

Before release

  1. Create the subscription pro_pass in the Play Console for Lite. Done: base plan yearly, auto-renewing, active in 178 countries at €2.99 (US $2.79 before sales tax, UK £2.49).
  2. Test a purchase, a restore, and a cancellation from the internal test track with a license tester account.
  3. Reword crouton_remove_ads. Done in e2ac51c: the new key crouton_pass says that OK shows how to remove the ads.
  4. Translate the new pass_*, menu_support, and crouton_pass strings.

Test plan

  • ./gradlew spotlessCheck assembleDebug lintProDebug lintLiteDebug lintFossDebug
  • testProDebugUnitTest testLiteDebugUnitTest, including the new EntitlementTest
  • Lite on a Pixel_6_Pro emulator: the landing row shows, and the sheet opens. The emulator has no Play billing, so the sheet shows only the Pro link.
  • A real purchase on the internal test track
  • connectedAndroidTest

andiwand and others added 4 commits October 25, 2026 17:05
Lite sells one auto-renewing subscription, `pro_pass`, which removes
the ads and unlocks the Pro features for one year. A sheet offers it
with one statement about what the money pays for, and a link to the
Pro app for users who want to pay once. The sheet replaces the "Part
of Pro" dialog, so the gate keeps its feature text as the sheet's
first line.

"Support the app" opens the sheet from the landing screen, from the
document actions, from the house ad, and from the snackbar after the
user enters fullscreen. Foss shows the same row, but it goes to
GitHub Sponsors. Pro keeps only its sponsor row in the folded
settings, because its users have paid already.

BillingManager now asks Play at each launch, because a pass ends.
Old one-time purchases for life give the Pro features. The old
one-year purchase and the old subscription keep only the ad removal.
An ended pass takes effect at the next launch, because AdManager does
not turn back on once it removes the ads. A new pass renders the open
document again, because the editing scope is set while translating.
If the edit mode is on, the render waits until the edit mode ends, so
no edit is lost.

Without Play or without the product, the sheet shows only the Pro
link.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUF78xeGqXrWLtQovh2RqU
The snackbar after fullscreen starts said "Tap OK to remove the ads for
free", but OK now opens the purchase sheet. The new text says that OK
shows how to remove the ads. The string has a new key, so the old
translations no longer show the wrong promise.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUF78xeGqXrWLtQovh2RqU
- Set Features.unlocked before the check for Play. A Play Services fix
  runs initializeManagers again, and a pass holder then kept Lite's
  editing scope until the next launch.
- Render the page again when the edit mode ends whenever the tools say
  that Pro is on. A restore during the first render left the page with
  paragraph scope, and leaving the edit mode did not fix it.
- Drop billing answers that arrive after PlayBilling.end, so an old
  BillingManager does not change the ads or the preferences.
- Answer the waiting calls when Play disconnects before the setup
  finishes. Otherwise no later call starts a connection.
- Sell only the base plan "yearly", so a later prepaid plan in the same
  product does not take its place.
- Fold offersPass into offersSupport, and remove a check that could
  never matter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUF78xeGqXrWLtQovh2RqU
- A tap on the pass in the gate sheet now also reports
  pro_gate_tapped, as OpenDocument.ios does for its gate.
- A restore at launch shows "Your purchase is restored" and reports
  pass_restored. Only a purchase from the sheet says thank you and
  reports pass_unlocked. PlayBilling knows the difference, because a
  purchase arrives through the purchases listener.
- After a pass ends during a session, the fullscreen snackbar does not
  offer ad removal until the next launch, when the ads return.
- The screen-tour caption names the new "Support the app" row.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUF78xeGqXrWLtQovh2RqU
@andiwand
andiwand merged commit 5e48777 into main Oct 25, 2026
7 checks passed
@andiwand
andiwand deleted the yearly-pass branch October 25, 2026 16:59
andiwand added a commit that referenced this pull request Oct 31, 2026
The yearly pass in Lite is new in this release, so the version is a
minor one. #740 added no changelog entry, so the cut adds one.

Translations come for the 18 strings of the pass, the Pro gate and
"Support the app". The Lite listing no longer says that the ads can be
removed for free, because that option is gone.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant