Skip to content

🐛 Fixed paid members on other tiers receiving gated newsletter content#28315

Draft
9larsons wants to merge 1 commit into
mainfrom
slars/tier-email-visibility
Draft

🐛 Fixed paid members on other tiers receiving gated newsletter content#28315
9larsons wants to merge 1 commit into
mainfrom
slars/tier-email-visibility

Conversation

@9larsons
Copy link
Copy Markdown
Contributor

@9larsons 9larsons commented Jun 2, 2026

Fixes #27893
Fixes https://linear.app/ghost/issue/ONC-1725
Related: #25046 · Forum: paid tier without post access still has access

Problem

When a post's visibility is restricted to specific tiers, the website correctly gates the content — a paying member on a tier not included in the post sees the public preview and paywall. Email newsletters did not honor this: the sending pipeline only understood a free/paid split, so any paying member received the full members-only content in their inbox, regardless of which tier they were on.

This contradicts the documented behavior that post access settings determine how members see content "on your site, or in their inbox as an email newsletter."

Cause

The email pipeline reasoned only about status:free vs status:-free:

  • getSegments() returned a hardcoded free/paid pair and never produced tier-based segments.
  • renderBody() applied the paywall only to the status:free segment — so every paying member, on any tier, got the full body.
  • The post's tiers relation wasn't even loaded during batch sending.

Change

  • Single source of truth for access — extracted getPostAccessFilter(post) in content-gating.js; checkPostAccess (web) now delegates to it, so web and email derive "who can read this post" the same way.
  • Tier-aware segmentationgetSegments() now splits a tier-restricted post into an access segment (members on an allowed tier → full content) and its complement (everyone else → preview + paywall). Reduces to the existing behavior for paid/members posts.
  • Access-driven paywallrenderBody() paywalls any segment lacking post access, instead of keying off status:free. Per-block data-gh-segment (free/paid) card visibility remains keyed on free/paid status, independent of tier access.
  • Load the relation — batch sending now loads the post's tiers.

Behavior

Post restricted to Tier A, sent to all members:

Recipient Receives
Member on Tier A Full content
Paying member on Tier B Public preview + paywall
Free member Public preview + paywall

paid and members posts are unchanged.

Tests

  • Unit: getPostAccessFilter across visibilities; getSegments tier (2-cell and 3-cell) cases; renderBody paywalls the no-access tier segment, not the access one.
  • Integration: seeds members across tiers (including a multi-tier member) and asserts the access/no-access segments partition recipients exactly against a real DB — no double-sends, no drops.
  • Verified manually end-to-end via a real newsletter send.

Known follow-up (not in this PR)

The email preheader/excerpt still regenerates from gated HTML only for the status:free segment, so a wrong-tier paid member sees members-only text in the preview text (the body is correctly gated). Same free/paid-vs-tier assumption, in #getEmailPreheader(); tracked for a separate change.

fixes #27893
ref #25046

- the website gated tier-restricted posts correctly, but newsletters only
  understood a free/paid split, so any paying member received the full
  members-only content regardless of which tier the post was restricted to
- extracted getPostAccessFilter() so web (checkPostAccess) and email derive
  "who can read this post" from a single source and can't drift apart
- getSegments() now splits a tier-restricted post into an access segment and
  its complement; renderBody() paywalls any segment lacking post access rather
  than keying off status:free; batch sending loads the post's tiers relation
- paid/members posts reduce to the existing behaviour
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 2, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 898b761a-41cc-4c90-8f7a-24b0ad11f0be

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slars/tier-email-visibility

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

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.

Public preview emails ignore tier-specific visibility

1 participant