Skip to content

feat(access-control): filter available lists by content restrictions#4589

Merged
dkoo merged 80 commits intotrunkfrom
feat/access-control-premium-newsletters
Apr 1, 2026
Merged

feat(access-control): filter available lists by content restrictions#4589
dkoo merged 80 commits intotrunkfrom
feat/access-control-premium-newsletters

Conversation

@dkoo
Copy link
Copy Markdown
Contributor

@dkoo dkoo commented Mar 20, 2026

All Submissions:

Changes proposed in this Pull Request:

Requires Automattic/newspack-newsletters#2053. Apply access control restrictions to available newsletter subscription lists, and auto-signup users depending on the newspack_premium_newsletters_auto_signup option value.

Also removes Registered Access options for premium newsletter gates, as these make less sense for restricting newsletter lists.

To avoid rapid roundtrip requests to the ESP and the potential race conditions that might cause, implements debouncing logic for access checks via an access check queue. When a data event occurs that might trigger a change in a user's access to premium newsletters, their user ID is added to a queue to check their access 10 minutes in the future. Future checks are scheduled via ActionScheduler if available, using WP Cron as a backup if not.

If a scheduled check already exists due to a prior action and isn't scheduled to happen within one minute, the user ID is added to the queue for that check.

If there's no scheduled check or the next check is happening imminently, a new check is scheduled for 10 minutes after the imminent check.

When the scheduled check occurs, the callback reads the queue of user IDs and performs access checks for all at once. Each user is added to or removed from premium newsletter lists based on this access check.

In this way, we avoid trying to update users' lists in real time and only perform the updates once, well after all transitional states have passed. This only applies to the auto-signup/removal mechanism. Access to lists is reflected in real time on the site, so that users can still see and sign up for premium lists manually in the gap period if desired.

Data events that trigger an access check:

  • Successful payment for a subscription: fires for new, switch, and renewal payments.
  • Failed payment for a subscription: only for renewal payments.
  • Subscription status changes
  • Reader verification
  • Updates to reader data in the DB

This PR doesn't attempt to reconcile the scenario where a user may have previously unsubscribed from a premium list they have access to; in this PR, the user will be readded automatically if they have access at the time of the check. I'll try to implement a fix for this in a future PR as this one is getting pretty big already.

Closes NPPD-1369.

How to test the changes in this Pull Request:

For easier testing, you may want to temporarily reduce the Premium_Newsletters::DEFAULT_DELAY and Premium_Newsletters::FUTURE_EVENT_THRESHOLD constants for faster turnaround. I tested successfully with these set to 15 seconds and 10 seconds, respectively.

Testing access checks with Active Subscription requirements:

  1. Choose a couple of newsletter lists to be restricted. Make sure the restricted lists are shown in Reader Registration and Newsletter Subscription blocks. Also enable these lists for the post-signup newsletter modal in Audience > Configuration.
  2. In Newsletters > Premium make sure Auto-signup is enabled in Advanced Settings. Set up and activate at least one premium newsletter gate targeting these lists at Newsletters > Premium. Try setting up a couple with different lists and access requirements! For example:
Screenshot 2026-03-27 at 11 25 14 AM
  1. As an anonymous reader, confirm that you can't see the restricted lists in Reader Registration and Newsletter Subscription blocks, even though they're configured there.
  2. Register a reader account via the Sign In modal flow and confirm that you still don't see the restricted lists in the post-registration signup modal. Also confirm that the restricted lists are not shown in My Account > Newsletters.
  3. Purchase a subscription product that's required by a premium newsletter gate. After completing the purchase, confirm that you DO see the lists restricted by that gate in the post-checkout signup modal and in My Account > Newsletters.
  4. Wait until the Premium_Newsletters::DEFAULT_DELAY elapses (or manually trigger the pending ActionScheduler event).
  5. In your connected ESP, confirm that the user was added to the restricted lists automatically.
  6. As an admin, update the subscription's status to a non-active status.
  7. Wait until the delay elapses again. Confirm that the user was removed from the restricted lists automatically.
  8. Reactivate the subscription and wait until they're re-added to the restricted lists.
  9. As the reader, log into My Account and update your default payment method to a number that will be declined (hint: use the Stripe test number that will "decline after attaching"). Make sure the active subscription is using this card number.
  10. Trigger an early renewal for the subscription that will fail.
  11. After the access check delay elapses, confirm that the user was automatically removed from the restricted lists.
  12. As the reader, visit My Account and pay for the failed subscription order.
  13. After the access check delay elapses, confirm that the user was automatically re-added to the restricted lists.

Testing access checks with email domain whitelist

  1. Enable an email domain whitelist for a premium newsletter gate.
  2. As a reader, register an account with that email domain. Verify via My Account.
  3. After the delay elapses, confirm that the reader was automatically added to the restricted lists.
  4. In My Account > Newsletters, confirm that the restricted lists are shown.

#### Testing access with reader data updates

We decided to remove this for now. Leaving the testing steps here for posterity.

~Note: this wasn't always working reliably for me, so it might need some more work. 😕 ~

  1. Enable a reader data key/value pair for a premium newsletter gate. e.g. foo=bar
  2. As a reader, register an account. Verify via My Account.
  3. Via wp shell, update the reader data for the user: Newspack\Reader_Data::update_item( $user_id, 'foo', 'bar' )
  4. [Note: this wasn't always working reliably for me, so it might need some more work. 😕] After the delay elapses, confirm that the reader was automatically added to the restricted lists.
  5. In My Account > Newsletters, confirm that the restricted lists are shown.

Testing access with institutional access

Note: I didn't implement any auto-signup or removal features for institutional access, since it feels kind of invasive to do so on something like an IP check.

  1. Set up an institution with IP whitelist and add this institution to a premium newsletter gate.
  2. As a reader, visit the /institutional-access endpoint to trigger the IP check.
  3. Browse around the site and confirm that the restricted lists are visible wherever shown.

Other information:

  • Have you added an explanation of what your changes do and why you'd like us to include them?
  • Have you written new tests for your changes, as applicable?
  • Have you successfully ran tests with your changes locally?

dkoo and others added 24 commits March 17, 2026 16:51
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Comment on lines +1729 to +1746
$email = $request['email_address'];

// If the email address is associated with a different user, use that user's ID for evaluating content restrictions for the signup form.
// TODO: Maybe check this against the result of self::set_current_reader()?
$user = get_user_by( 'email', $email );
if ( $user && $user->ID !== get_current_user_id() ) {
self::$current_reader_user_id = $user->ID;
add_filter( 'newspack_content_restriction_control_user_id', [ self::class, 'get_user_id_for_content_restriction' ] );
}
ob_start();
self::render_newsletters_signup_modal( $request['email_address'] );
self::render_newsletters_signup_modal( $email );
$html = trim( ob_get_clean() );

// Reset the current reader user ID so it doesn't affect other operations in this session.
if ( $user && $user->ID !== get_current_user_id() ) {
self::$current_reader_user_id = 0;
remove_filter( 'newspack_content_restriction_control_user_id', [ self::class, 'get_user_id_for_content_restriction' ] );
}
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.

This looks a bit convoluted... Is it not possible to set an X-WP-Nonce header when fetching this data from refreshNewslettersSignupModal() and rely on the current user?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The challenge is that when this REST API request is fired, it happens right before the reader account is registered and authenticated, so the current user isn't available yet in this execution.

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.

I'm confused... How does it set $current_reader_user_id then?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think we can continue discussing a solution for this, but it's best tackled in a separate PR since it might touch other flows outside of premium newsletters.

Copy link
Copy Markdown
Contributor Author

@dkoo dkoo Apr 1, 2026

Choose a reason for hiding this comment

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

Forgot to respond to this, sorry!

I'm confused... How does it set $current_reader_user_id then?

The api_render_newsletters_signup_form callback can run in an unauthenticated OR an authenticated session:

  • If an anonymous reader registers for a new account or completes a Woo purchase, it will fire in an unauthenticated session as the reader account is created on the server
  • If the reader is already logged in and completes a Woo purchase, it will fire in the authenticated session

In the first scenario, get_current_user_id() will return 0, so we can compare that to the $user we fetch via the passed email address. In this case, we assume that the passed email address is the brand-new user account that was just created and cache that user ID for the sole purpose of rendering the newsletter signup modal.

In the second scenario, get_current_user_id() will return the correct user ID, so no further action is needed.

@github-actions github-actions bot added the [Status] Needs Changes or Feedback The issue or pull request needs action from the original creator label Mar 31, 2026
@dkoo dkoo requested a review from miguelpeixe March 31, 2026 18:16
Copy link
Copy Markdown
Member

@miguelpeixe miguelpeixe left a comment

Choose a reason for hiding this comment

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

Tests well! Thank you for the revisions.

I'm working on improving the session hydration handling for the post-checkout premium newsletter offering in #4618.

I'm still able to see restricted newsletters in the My Account -> Newsletters page, but let's tackle it in a separate PR.

@github-actions github-actions bot added [Status] Approved The pull request has been reviewed and is ready to merge and removed [Status] Needs Review The issue or pull request needs to be reviewed [Status] Needs Changes or Feedback The issue or pull request needs action from the original creator labels Apr 1, 2026
@miguelpeixe
Copy link
Copy Markdown
Member

miguelpeixe commented Apr 1, 2026

Hmm... I can actually see restricted newsletters in the newsletter sign-up modal and on the My Account page, regardless of my access.

I had them unchecked in the "Present newsletter signup after checkout and registration" while first testing, and toggled them on after purchasing the subscription. That's why I missed it.

@dkoo
Copy link
Copy Markdown
Contributor Author

dkoo commented Apr 1, 2026

@miguelpeixe is your Newsletters plugin running the required branch?

@miguelpeixe
Copy link
Copy Markdown
Member

Not this time around 🤦
Sorry about that

@dkoo dkoo merged commit 959127f into trunk Apr 1, 2026
11 checks passed
@dkoo dkoo deleted the feat/access-control-premium-newsletters branch April 1, 2026 17:01
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 1, 2026

Hey @dkoo, good job getting this PR merged! 🎉

Now, the needs-changelog label has been added to it.

Please check if this PR needs to be included in the "Upcoming Changes" and "Release Notes" doc. If it doesn't, simply remove the label.

If it does, please add an entry to our shared document, with screenshots and testing instructions if applicable, then remove the label.

Thank you! ❤️

miguelpeixe added a commit that referenced this pull request Apr 1, 2026
matticbot pushed a commit that referenced this pull request Apr 2, 2026
# [6.37.0-alpha.1](v6.36.1...v6.37.0-alpha.1) (2026-04-02)

### Bug Fixes

* **card-settings-group:** change default actionType from chevron to none ([#4610](#4610)) ([00505ed](00505ed))
* **post-date:** preserve classic theme markup and fix archive titles ([#4602](#4602)) ([c5fb825](c5fb825))
* remove removal of block visibility ([#4595](#4595)) ([9396379](9396379))

### Features

* **access-control:** filter available lists by content restrictions ([#4589](#4589)) ([959127f](959127f)), closes [#4581](#4581) [#4583](#4583) [#4590](#4590)
* **access-control:** premium newsletters UI ([#4577](#4577)) ([6f8c891](6f8c891)), closes [#4581](#4581) [#4583](#4583) [#4590](#4590)
* **author-profile-social:** add support for colors, block spacing, brand style ([#4509](#4509)) ([21cf4c9](21cf4c9))
* campaigns wizard light UI refresh ([#4588](#4588)) ([6078c4b](6078c4b))
* **color-picker:** simplify component to use basecontrol ([#4581](#4581)) ([ff677ea](ff677ea))
* **components:** add CardFeature component ([#4583](#4583)) ([5aabb18](5aabb18))
* **content-gate:** institution management ui ([#4582](#4582)) ([ae88750](ae88750))
* **content-gate:** institutional access redirect and loading UX ([#4593](#4593)) ([548d236](548d236))
* **content-gate:** institutions ([#4574](#4574)) ([49b0c05](49b0c05))
* **content-gate:** personalized institutional access verification page ([#4596](#4596)) ([0eed591](0eed591))
* **image-upload:** simplify component to use basecontrol; remove info prop ([#4580](#4580)) ([d51eb54](d51eb54))
* **integrations:** add ActionScheduler group handling ([#4559](#4559)) ([411732a](411732a))
* **integrations:** promoted fields for content gate and campaign segmentation ([#4601](#4601)) ([f943df2](f943df2))
* **newspack-ui:** add stack layout and color utility classes ([#4600](#4600)) ([1934067](1934067))
* **post-date:** centralize date features from theme into plugin ([#4579](#4579)) ([19f15eb](19f15eb))
* **sync:** prevent stale data on retry, improve logging and error handling ([#4562](#4562)) ([5467f34](5467f34))
* **tags:** add private tags feature ([#4507](#4507)) ([06d7711](06d7711))
* **yoast:** add primary category utility and settings toggle ([#4563](#4563)) ([4b396c3](4b396c3))
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 2, 2026

🎉 This PR is included in version 6.37.0-alpha.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Status] Approved The pull request has been reviewed and is ready to merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants