Skip to content

feat(content-gate): network product and access control#303

Open
miguelpeixe wants to merge 22 commits intotrunkfrom
feat/network-product-id
Open

feat(content-gate): network product and access control#303
miguelpeixe wants to merge 22 commits intotrunkfrom
feat/network-product-id

Conversation

@miguelpeixe
Copy link
Member

@miguelpeixe miguelpeixe commented Mar 10, 2026

All Submissions:

Changes proposed in this Pull Request:

Add product-level Network IDs to integrate with content gating. This enables cross-site subscription access and purchase restrictions.

  • Network ID on products: Adds a "Network ID" metabox to WooCommerce product edit screens, allowing admins to link subscription products across sites in a network.
  • Event sync: Syncs product Network IDs via a new newspack_network_product_updated data event, with incoming event handler, event log item, and CLI backfiller.
  • Access rules: Hooks into the newspack_access_rules_has_active_subscription filter. When a user has an active subscription on another network site for a product with a matching Network ID, access is granted.
  • Purchase restrictions: Prevents users from purchasing a subscription product when they already have an active subscription to a product with the same Network ID on another network site.

Closes NPPD-1162.

How to test the changes in this Pull Request:

  1. Set up a network with at least two sites (hub+node)
  2. On both sites, create a WooCommerce subscription product and assign the same Network ID (e.g., premium-access) via the "Newspack Network" metabox on the product edit screen.
  3. On both sites, create a content gate with an access rule requiring that subscription product.
  4. As a reader, purchase the subscription on Site A.
  5. Wait for the subscription data to sync (nodes pull every 2 minutes), or manually trigger a backfill.
  6. On Site B, verify:
    • The reader can bypass the content gate (access granted via network subscription).
    • The reader cannot purchase the same subscription product — it should show as non-purchasable with a message referencing Site A.
  7. As a logged-out user with the same billing email, attempt to checkout with the subscription on Site B — verify the checkout validation error appears.
  8. On the Hub, check the event log for "Product updated" entries with the correct product name and Network ID.

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?

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds product-level “Network ID” support to Newspack Network to enable cross-site Content Gate access checks and to prevent duplicate subscription purchases across network sites.

Changes:

  • Adds a WooCommerce Product “Network ID” metabox and stores it in product post meta.
  • Syncs product Network IDs across the network via a new newspack_network_product_updated data event (incoming handler, event log item, and CLI backfiller).
  • Introduces Content Gate integrations for network-aware access decisions and subscription purchase limiting.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
includes/woocommerce/class-product-admin.php Adds admin UI + persistence for product Network ID metadata.
includes/woocommerce/class-events.php Registers and emits the new product-updated data event.
includes/incoming-events/class-product-updated.php Processes incoming product-updated events and stores synced product data.
includes/hub/stores/event-log-items/class-product-updated.php Adds Hub event log summaries for product updates.
includes/content-gate/class-access.php Grants Content Gate access based on matching Network IDs across sites.
includes/content-gate/class-limit-purchase.php Blocks purchase/checkout when an equivalent network subscription already exists.
includes/cli/backfillers/class-product-updated.php Backfills product-updated events for products with Network IDs.
includes/class-initializer.php Boots the new Product admin and Content Gate integrations.
includes/class-accepted-actions.php Allows newspack_network_product_updated events to be accepted/pulled.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

@miguelpeixe miguelpeixe marked this pull request as ready for review March 10, 2026 19:05
@miguelpeixe miguelpeixe requested a review from a team as a code owner March 10, 2026 19:05
@miguelpeixe miguelpeixe self-assigned this Mar 10, 2026
@miguelpeixe miguelpeixe requested a review from dkoo March 10, 2026 19:05
Copy link
Contributor

@dkoo dkoo left a comment

Choose a reason for hiding this comment

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

I ran into issues matching up Network IDs when the products in question were variations of a variable subscription. Since the Network ID field attaches to the parent product instead of each individual variation (as I think it should), we should be crawling up the $product->get_parent_id() chain when comparing across sites. And/or, the network_id for a parent product needs to be populated for all of its variations/child products when syncing subscription data across sites.

Non-blocking feedback: I like the introduction of a Network ID string to allow publishers to manually map products across network sites. I wonder if we should also attempt to map by matching product slugs if the ID is empty, to reduce the overhead required for publishers? This probably won't be true of all networks, but many might have products with the same name across all network sites.

@miguelpeixe
Copy link
Member Author

I ran into issues matching up Network IDs when the products in question were variations of a variable subscription.

Good catch! Updated in a34d529. Testing it will require another product change event to populate the variant IDs.

Non-blocking feedback: I like the introduction of a Network ID string to allow publishers to manually map products across network sites. I wonder if we should also attempt to map by matching product slugs if the ID is empty, to reduce the overhead required for publishers? This probably won't be true of all networks, but many might have products with the same name across all network sites.

This is the same strategy we have in place for membership plans:

image

I decided to keep it because:

  1. We don't want to deal with slug collision. It's not uncommon for sites to have a draft product holding the slug and the published product be premium-2, which will unmatch on another site that doesn't have the same issue.
  2. It's not always that the same-slug products are intended to sync in the network. We'd have to create an opt-out strategy or manually tweak the slug to prevent sync.

@miguelpeixe miguelpeixe requested a review from dkoo March 11, 2026 16:40
Copy link
Contributor

@dkoo dkoo left a comment

Choose a reason for hiding this comment

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

Thanks for the fix for variations, this is testing well now! Some more comments inline about the usage of get_post_meta vs $product->get_meta that I missed in the earlier review.

if ( ! $product ) {
return;
}
$network_id = get_post_meta( $product->get_id(), Product_Admin::NETWORK_ID_META_KEY, true );
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this be $product->get_meta( Product_Admin::NETWORK_ID_META_KEY, true );?

Copy link
Member Author

Choose a reason for hiding this comment

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

product is still a post type that supports update_post_meta() and get_post_meta(). I'd rather use the WP approach as it should be more stable and standardized. Is there a benefit in using the Woo layer here?

We're using update_post_meta() to set it:

update_post_meta( $post_id, self::NETWORK_ID_META_KEY, $network_id );

* @return string The Network ID, or empty string if not set.
*/
public static function get_network_id( $product_id ) {
$network_id = get_post_meta( $product_id, self::NETWORK_ID_META_KEY, true );
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above, should we be using $product->get_meta( Product_Admin::NETWORK_ID_META_KEY, true );?

if ( function_exists( 'wc_get_product' ) ) {
$product = wc_get_product( $product_id );
if ( $product && $product->get_parent_id() ) {
return get_post_meta( $product->get_parent_id(), self::NETWORK_ID_META_KEY, true );
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above, should we be using $product->get_meta( Product_Admin::NETWORK_ID_META_KEY, true );?

* @param \WP_Post $post The post object.
*/
public static function render_meta_box( $post ) {
$network_id = get_post_meta( $post->ID, self::NETWORK_ID_META_KEY, true );
Copy link
Contributor

Choose a reason for hiding this comment

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

Same as above, should we be using $product->get_meta( Product_Admin::NETWORK_ID_META_KEY, true );?

@miguelpeixe miguelpeixe requested a review from dkoo March 11, 2026 17:17
Copy link
Contributor

@dkoo dkoo left a comment

Choose a reason for hiding this comment

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

Gotcha, I wasn't sure if products are also stored via HPOS, but it looks like they're not. Should be fine, then.

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