From 298fd7c958450cebae8803b334394eea63de501b Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Fri, 17 Nov 2023 09:32:57 -0300 Subject: [PATCH 01/15] feat(data-events): track content gate interactions (#2740) --- assets/blocks/reader-registration/index.php | 9 + assets/memberships-gate/gate.js | 106 +++++++ .../{overlay.scss => gate.scss} | 0 assets/memberships-gate/overlay.js | 50 ---- includes/class-donations.php | 2 +- includes/class-newspack.php | 1 + includes/data-events/README.md | 46 ++- includes/data-events/class-memberships.php | 274 ++++++++++++++++++ .../data-events/connectors/ga4/class-ga4.php | 25 +- .../wc-memberships/class-memberships.php | 17 +- .../class-reader-activation.php | 9 + webpack.config.js | 7 +- 12 files changed, 460 insertions(+), 86 deletions(-) create mode 100644 assets/memberships-gate/gate.js rename assets/memberships-gate/{overlay.scss => gate.scss} (100%) delete mode 100644 assets/memberships-gate/overlay.js create mode 100644 includes/data-events/class-memberships.php diff --git a/assets/blocks/reader-registration/index.php b/assets/blocks/reader-registration/index.php index 6bea4208c4..c516bc2cfe 100644 --- a/assets/blocks/reader-registration/index.php +++ b/assets/blocks/reader-registration/index.php @@ -430,10 +430,19 @@ function process_form() { $popup_id = isset( $_REQUEST['newspack_popup_id'] ) ? (int) $_REQUEST['newspack_popup_id'] : false; $metadata['newspack_popup_id'] = $popup_id; + if ( $popup_id ) { $metadata['registration_method'] = 'registration-block-popup'; } + /** + * Filters the metadata to be saved for a reader registered through the Reader Registration Block. + * + * @param array $metadata Metadata. + * @param string $email Email address of the reader. + */ + $metadata = apply_filters( 'newspack_register_reader_form_metadata', $metadata, $email ); + $user_id = Reader_Activation::register_reader( $email, '', true, $metadata ); /** diff --git a/assets/memberships-gate/gate.js b/assets/memberships-gate/gate.js new file mode 100644 index 0000000000..5ef0c624d5 --- /dev/null +++ b/assets/memberships-gate/gate.js @@ -0,0 +1,106 @@ +/** + * Internal dependencies + */ +import './gate.scss'; + +/** + * Specify a function to execute when the DOM is fully loaded. + * + * @see https://github.com/WordPress/gutenberg/blob/trunk/packages/dom-ready/ + * + * @param {Function} callback A function to execute after the DOM is ready. + * @return {void} + */ +function domReady( callback ) { + if ( typeof document === 'undefined' ) { + return; + } + if ( + document.readyState === 'complete' || // DOMContentLoaded + Images/Styles/etc loaded, so we call directly. + document.readyState === 'interactive' // DOMContentLoaded fires at this point, so we call directly. + ) { + return void callback(); + } + // DOMContentLoaded has not fired yet, delay callback until then. + document.addEventListener( 'DOMContentLoaded', callback ); +} + +/** + * Adds 'memberships_content_gate' hidden input to every form inside the gate. + * + * @param {HTMLElement} gate The gate element. + */ +function addFormInputs( gate ) { + const forms = gate.querySelectorAll( 'form' ); + forms.forEach( form => { + const input = document.createElement( 'input' ); + input.type = 'hidden'; + input.name = 'memberships_content_gate'; + input.value = '1'; + form.appendChild( input ); + } ); +} + +/** + * Push gate 'seen' event to Google Analytics. + */ +function pushSeenEvent() { + const eventName = 'np_gate_interaction'; + const payload = { + action: 'seen', + }; + if ( 'function' === typeof window.gtag && payload ) { + window.gtag( 'event', eventName, payload ); + } +} + +/** + * Initializes the overlay gate. + * + * @param {HTMLElement} gate The gate element. + */ +function initOverlay( gate ) { + let entry = document.querySelector( '.entry-content' ); + if ( ! entry ) { + entry = document.querySelector( '#content' ); + } + gate.style.removeProperty( 'display' ); + let seen = false; + const handleScroll = () => { + const delta = ( entry?.getBoundingClientRect().top || 0 ) - window.innerHeight / 2; + let visible = false; + if ( delta < 0 ) { + visible = true; + if ( ! seen ) { + pushSeenEvent(); + } + seen = true; + } + gate.setAttribute( 'data-visible', visible ); + }; + document.addEventListener( 'scroll', handleScroll ); + handleScroll(); +} + +domReady( function () { + const gate = document.querySelector( '.newspack-memberships__gate' ); + if ( ! gate ) { + return; + } + addFormInputs( gate ); + + if ( gate.classList.contains( 'newspack-memberships__overlay-gate' ) ) { + initOverlay( gate ); + } else { + // Seen event for inline gate. + const detectSeen = () => { + const delta = ( gate?.getBoundingClientRect().top || 0 ) - window.innerHeight / 2; + if ( delta < 0 ) { + pushSeenEvent(); + document.removeEventListener( 'scroll', detectSeen ); + } + }; + document.addEventListener( 'scroll', detectSeen ); + detectSeen(); + } +} ); diff --git a/assets/memberships-gate/overlay.scss b/assets/memberships-gate/gate.scss similarity index 100% rename from assets/memberships-gate/overlay.scss rename to assets/memberships-gate/gate.scss diff --git a/assets/memberships-gate/overlay.js b/assets/memberships-gate/overlay.js deleted file mode 100644 index 6ee0c7f61b..0000000000 --- a/assets/memberships-gate/overlay.js +++ /dev/null @@ -1,50 +0,0 @@ -/** - * Internal dependencies - */ -import './overlay.scss'; - -/** - * Specify a function to execute when the DOM is fully loaded. - * - * @see https://github.com/WordPress/gutenberg/blob/trunk/packages/dom-ready/ - * - * @param {Function} callback A function to execute after the DOM is ready. - * @return {void} - */ -function domReady( callback ) { - if ( typeof document === 'undefined' ) { - return; - } - if ( - document.readyState === 'complete' || // DOMContentLoaded + Images/Styles/etc loaded, so we call directly. - document.readyState === 'interactive' // DOMContentLoaded fires at this point, so we call directly. - ) { - return void callback(); - } - // DOMContentLoaded has not fired yet, delay callback until then. - document.addEventListener( 'DOMContentLoaded', callback ); -} - -domReady( function () { - const overlay = document.querySelector( '.newspack-memberships__overlay-gate' ); - if ( ! overlay ) { - return; - } - let entry = document.querySelector( '.entry-content' ); - if ( ! entry ) { - entry = document.querySelector( '#content' ); - } - if ( overlay ) { - overlay.style.removeProperty( 'display' ); - const handleScroll = () => { - const delta = ( entry?.getBoundingClientRect().top || 0 ) - window.innerHeight / 2; - let visible = false; - if ( delta < 0 ) { - visible = true; - } - overlay.setAttribute( 'data-visible', visible ); - }; - document.addEventListener( 'scroll', handleScroll ); - handleScroll(); - } -} ); diff --git a/includes/class-donations.php b/includes/class-donations.php index 75d1dbc77b..515cc09a57 100644 --- a/includes/class-donations.php +++ b/includes/class-donations.php @@ -260,7 +260,7 @@ private static function get_donation_product_child_products_ids() { /** * Check whether the given product ID is a donation product. - * + * * @param int $product_id Product ID to check. * @return boolean True if a donation product, false if not. */ diff --git a/includes/class-newspack.php b/includes/class-newspack.php index 6a5c159529..ac3464af7c 100644 --- a/includes/class-newspack.php +++ b/includes/class-newspack.php @@ -83,6 +83,7 @@ private function includes() { include_once NEWSPACK_ABSPATH . 'includes/data-events/class-api.php'; include_once NEWSPACK_ABSPATH . 'includes/data-events/listeners.php'; include_once NEWSPACK_ABSPATH . 'includes/data-events/class-popups.php'; + include_once NEWSPACK_ABSPATH . 'includes/data-events/class-memberships.php'; include_once NEWSPACK_ABSPATH . 'includes/data-events/connectors/ga4/class-ga4.php'; include_once NEWSPACK_ABSPATH . 'includes/data-events/connectors/class-mailchimp.php'; include_once NEWSPACK_ABSPATH . 'includes/data-events/connectors/class-activecampaign.php'; diff --git a/includes/data-events/README.md b/includes/data-events/README.md index 374b0d7de9..c4af3d4cb2 100644 --- a/includes/data-events/README.md +++ b/includes/data-events/README.md @@ -108,7 +108,6 @@ the order is already marked as failed so this hook will not trigger. | `popup_id` | `string` | If the donation was triggered by a popup, the popup ID | | `platform_data` | `array` | | - ### `donation_new` When there's a new donation, either through Stripe or Newspack (WooCommerce) platforms. @@ -173,7 +172,7 @@ When a WooCommerce Subscription status changes. ### `product_subscription_active` -When a non-donation subscription is activated. +When a non-donation subscription is activated. | Name | Type | | ----------------- | -------- | @@ -186,7 +185,7 @@ When a non-donation subscription is activated. ### `product_subscription_inactive` -When a non-donation subscription is changed to any non-active status. +When a non-donation subscription is changed to any non-active status. | Name | Type | | ----------------- | -------- | @@ -199,22 +198,39 @@ When a non-donation subscription is changed to any non-active status. | `status_before` | `string` | | `status_after` | `string` | +## Membership Actions + +### `gate_interaction` + +When a reader interacts with the content gate. + +| Name | Type | Obs | +| -------------- | -------- | ------------------------------------------------------------------------------------------------- | +| `gate_post_id` | `int` | | +| `action` | `string` | Either `seen`, `form_submission_received`, `form_submission_success` or `form_submission_failure` | +| `action_type` | `string` | Either `paid_membership` or `registration`. Not applicable when `action` is `seen` | +| `referer` | `string` | | +| `order_id` | `int` | Only applicable when `action_type` is `paid_membership` | +| `product_id` | `int` | Only applicable when `action_type` is `paid_membership` | +| `amount` | `float` | Only applicable when `action_type` is `paid_membership` | +| `currency` | `string` | Only applicable when `action_type` is `paid_membership` | + ## Newspack Popups Actions ### `prompt_interaction` When a user interacts with a Newspack Popup's campaign prompt. -| Name | Type | Obs | -| ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | -| `prompt_id` | `int` | | -| `prompt_title` | `string` | | -| `prompt_frequency` | `string` | | -| `prompt_placement` | `string` | | +| Name | Type | Obs | +| ------------------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| `prompt_id` | `int` | | +| `prompt_title` | `string` | | +| `prompt_frequency` | `string` | | +| `prompt_placement` | `string` | | | `prompt_blocks` | `array` | Array containing the blocks that are inside the prompt. Only 3 blocks are tracked: `donation`, `registration` and `newsletters_subscription` | -| `action` | `string` | `form_submission_received`, `form_submission_success` or `form_submission_failure` | -| `action_type` | `string` | `donation`, `registration` or `newsletters_subscription` | -| `interaction_data` | `array` | Depending on the action type, it will contain different information about the interaction. | +| `action` | `string` | `form_submission_received`, `form_submission_success` or `form_submission_failure` | +| `action_type` | `string` | `donation`, `registration` or `newsletters_subscription` | +| `interaction_data` | `array` | Depending on the action type, it will contain different information about the interaction. | #### Possible values for `interaction_data` @@ -226,8 +242,8 @@ If `action_type` is `registration`: If `action_type` is `newsletters_subscription`: -| Name | Type | -| --------------------- | -------- | +| Name | Type | +| --------------------------------- | -------- | | `newsletters_subscription_method` | `string` | If `action_type` is `donation`: @@ -241,8 +257,6 @@ If `action_type` is `donation`: | `donation_platform` | `string` | | | `donation_error` | `string` | Only for failed donations via Stripe | - - ## Registering a new action To dispatch an event, an action must first be registered with the following: diff --git a/includes/data-events/class-memberships.php b/includes/data-events/class-memberships.php new file mode 100644 index 0000000000..1282b5430a --- /dev/null +++ b/includes/data-events/class-memberships.php @@ -0,0 +1,274 @@ +add_meta_data( '_memberships_content_gate', true ); + } + } + + /** + * Add content gate metadata on reader registration. + * + * @param array $metadata The metadata. + * @param int|false $user_id The user ID or false if not created. + * + * @return array + */ + public static function register_reader_metadata( $metadata, $user_id ) { + if ( isset( $_REQUEST[ self::METADATA_NAME ] ) && ! empty( $_REQUEST[ self::METADATA_NAME ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $metadata[ self::METADATA_NAME ] = true; + $metadata['registration_method'] = 'registration-block-content-gate'; + } + return $metadata; + } + + /** + * Register listeners. + */ + public static function register_listeners() { + /** + * Gate interaction: Registration membership + */ + Data_Events::register_listener( + 'newspack_reader_registration_form_processed', + 'gate_interaction', + [ __CLASS__, 'registration_submission' ] + ); + Data_Events::register_listener( + 'newspack_reader_registration_form_processed', + 'gate_interaction', + [ __CLASS__, 'registration_submission_with_status' ] + ); + + /** + * Gate interaction: Paid membership + */ + Data_Events::register_listener( + 'woocommerce_checkout_order_processed', + 'gate_interaction', + [ __CLASS__, 'woocommerce_checkout_order_processed' ] + ); + Data_Events::register_listener( + 'woocommerce_order_status_failed', + 'gate_interaction', + [ __CLASS__, 'woocommerce_order_status_failed' ] + ); + Data_Events::register_listener( + 'woocommerce_order_status_completed', + 'gate_interaction', + [ __CLASS__, 'woocommerce_order_status_completed' ] + ); + } + + /** + * Get common metadata to be sent with all gate interaction events. + */ + private static function get_gate_metadata() { + return [ + 'gate_post_id' => NewspackMemberships::get_gate_post_id(), + ]; + } + + /** + * A listener for the registration block form submission + * + * Will trigger the event with "form_submission" as action in all cases. + * + * @param string $email Email address of the reader. + * @param int|false|\WP_Error $user_id The created user ID in case of registration, false if not created or a WP_Error object. + * @param array $metadata Array with metadata about the user being registered. + * @return ?array + */ + public static function registration_submission( $email, $user_id, $metadata ) { + if ( ! isset( $metadata[ self::METADATA_NAME ] ) ) { + return; + } + $data = array_merge( + self::get_gate_metadata(), + [ + 'action' => self::FORM_SUBMISSION, + 'action_type' => 'registration', + 'referer' => $metadata['referer'], + ] + ); + $data['interaction_data']['registration_method'] = $metadata['registration_method']; + return $data; + } + + /** + * A listener for the registration block form submission + * + * Will trigger the event with "form_submission" as action in all cases. + * + * @param string $email Email address of the reader. + * @param int|false|\WP_Error $user_id The created user ID in case of registration, false if not created or a WP_Error object. + * @param array $metadata Array with metadata about the user being registered. + * @return ?array + */ + public static function registration_submission_with_status( $email, $user_id, $metadata ) { + if ( ! isset( $metadata[ self::METADATA_NAME ] ) ) { + return; + } + $action = self::FORM_SUBMISSION_SUCCESS; + if ( ! $user_id || \is_wp_error( $user_id ) ) { + $action = self::FORM_SUBMISSION_FAILURE; + } + $data = array_merge( + self::get_gate_metadata(), + [ + 'action' => $action, + 'action_type' => 'registration', + 'referer' => $metadata['referer'], + ] + ); + $data['interaction_data']['registration_method'] = $metadata['registration_method']; + return $data; + } + + /** + * Get order data. + * + * @param int $order_id The order ID. + * @param \WC_Order $order The order object. + * + * @return ?array + */ + private static function get_order_data( $order_id, $order ) { + $is_from_gate = $order->get_meta( '_memberships_content_gate' ); + if ( ! $is_from_gate ) { + return; + } + $item = array_shift( $order->get_items() ); + $data = array_merge( + self::get_gate_metadata(), + [ + 'action_type' => 'paid_membership', + 'order_id' => $order_id, + 'product_id' => $item->get_product_id(), + 'amount' => (float) $order->get_total(), + 'currency' => $order->get_currency(), + 'referer' => $order->get_meta( '_newspack_referer' ), + ] + ); + return $data; + } + + /** + * A listener for when a WooCommerce order is processed. + * + * @param int $order_id The order ID. + * + * @return ?array + */ + public static function woocommerce_checkout_order_processed( $order_id ) { + $order = \wc_get_order( $order_id ); + $data = self::get_order_data( $order_id, $order ); + if ( empty( $data ) ) { + return; + } + $data['action'] = self::FORM_SUBMISSION; + return $data; + } + + /** + * A listener for when a WooCommerce order has failed. + * + * @param int $order_id The order ID. + * @param \WC_Order $order The order object. + * + * @return ?array + */ + public static function woocommerce_order_status_failed( $order_id, $order ) { + $data = self::get_order_data( $order_id, $order ); + if ( empty( $data ) ) { + return; + } + $data['action'] = self::FORM_SUBMISSION_FAILURE; + return $data; + } + + /** + * A listener for when a WooCommerce order is completed. + * + * @param int $order_id The order ID. + * @param \WC_Order $order The order object. + * + * @return ?array + */ + public static function woocommerce_order_status_completed( $order_id, $order ) { + $data = self::get_order_data( $order_id, $order ); + if ( empty( $data ) ) { + return; + } + $data['action'] = self::FORM_SUBMISSION_SUCCESS; + return $data; + } +} +Memberships::init(); diff --git a/includes/data-events/connectors/ga4/class-ga4.php b/includes/data-events/connectors/ga4/class-ga4.php index 3859c5850a..07c8de17a0 100644 --- a/includes/data-events/connectors/ga4/class-ga4.php +++ b/includes/data-events/connectors/ga4/class-ga4.php @@ -38,6 +38,7 @@ class GA4 { 'donation_subscription_cancelled', 'newsletter_subscribed', 'prompt_interaction', + 'gate_interaction', ]; /** @@ -370,8 +371,8 @@ public static function handle_newsletter_subscribed( $params, $data ) { /** * Handler for the prompt_interaction event. * - * @param int $params The GA4 event parameters. - * @param array $data Data associated with the Data Events api event. + * @param array $params The GA4 event parameters. + * @param array $data Data associated with the Data Events api event. * * @return array $params The final version of the GA4 event params that will be sent to GA. */ @@ -386,6 +387,26 @@ public static function handle_prompt_interaction( $params, $data ) { return array_merge( $params, $transformed_data ); } + /** + * Handler for the gate_interaction event. + * + * @param array $params The GA4 event parameters. + * @param array $data Data associated with the Data Events api event. + * + * @return array $params The final version of the GA4 event params that will be sent to GA. + */ + public static function handle_gate_interaction( $params, $data ) { + $params['gate_post_id'] = $data['gate_post_id'] ?? ''; + $params['action'] = $data['action'] ?? ''; + $params['action_type'] = $data['action_type'] ?? ''; + $params['referer'] = $data['referer'] ?? ''; + $params['order_id'] = $data['order_id'] ?? ''; + $params['product_id'] = $data['product_id'] ?? ''; + $params['amount'] = $data['amount'] ?? ''; + $params['currency'] = $data['currency'] ?? ''; + return $params; + } + /** * Gets the santized popup params from a popup ID * diff --git a/includes/plugins/wc-memberships/class-memberships.php b/includes/plugins/wc-memberships/class-memberships.php index 52e45f7239..d897b2ccee 100644 --- a/includes/plugins/wc-memberships/class-memberships.php +++ b/includes/plugins/wc-memberships/class-memberships.php @@ -28,7 +28,7 @@ class Memberships { /** * Membership statuses that should grant access to restricted content. * See: https://woocommerce.com/document/woocommerce-memberships-user-memberships/#section-4 - * + * * @var array */ public static $active_statuses = [ 'active', 'complimentary', 'free-trial', 'pending' ]; @@ -195,25 +195,20 @@ public static function enqueue_scripts() { if ( ! is_singular() || ! self::is_post_restricted() ) { return; } - $gate_post_id = self::get_gate_post_id(); - $style = \get_post_meta( $gate_post_id, 'style', true ); - if ( 'overlay' !== $style ) { - return; - } - $handle = 'newspack-memberships-gate-overlay'; + $handle = 'newspack-memberships-gate'; \wp_enqueue_script( $handle, - Newspack::plugin_url() . '/dist/memberships-gate-overlay.js', + Newspack::plugin_url() . '/dist/memberships-gate.js', [], - filemtime( dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/memberships-gate-overlay.js' ), + filemtime( dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/memberships-gate.js' ), true ); \wp_script_add_data( $handle, 'async', true ); \wp_enqueue_style( $handle, - Newspack::plugin_url() . '/dist/memberships-gate-overlay.css', + Newspack::plugin_url() . '/dist/memberships-gate.css', [], - filemtime( dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/memberships-gate-overlay.css' ) + filemtime( dirname( NEWSPACK_PLUGIN_FILE ) . '/dist/memberships-gate.css' ) ); } diff --git a/includes/reader-activation/class-reader-activation.php b/includes/reader-activation/class-reader-activation.php index 468b0b87b7..a534df514e 100644 --- a/includes/reader-activation/class-reader-activation.php +++ b/includes/reader-activation/class-reader-activation.php @@ -1630,6 +1630,15 @@ public static function register_reader( $email, $display_name = '', $authenticat } } + /** + * Filters the metadata to pass along to the action hook. + * + * @param array $metadata Metadata. + * @param int|false $user_id The created user id or false if the user already exists. + * @param false|\WP_User $existing_user The existing user object. + */ + $metadata = apply_filters( 'newspack_register_reader_metadata', $metadata, $user_id, $existing_user ); + // Note the user's login method for later use. if ( isset( $metadata['registration_method'] ) ) { \update_user_meta( $user_id, self::REGISTRATION_METHOD, $metadata['registration_method'] ); diff --git a/webpack.config.js b/webpack.config.js index e046a9cac8..0f7dfa8f64 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -70,12 +70,7 @@ const webpackConfig = getBaseWebpackConfig( 'my-account': path.join( __dirname, 'includes', 'reader-revenue', 'my-account', 'index.js' ), admin: path.join( __dirname, 'assets', 'admin', 'index.js' ), 'memberships-gate-editor': path.join( __dirname, 'assets', 'memberships-gate', 'editor.js' ), - 'memberships-gate-overlay': path.join( - __dirname, - 'assets', - 'memberships-gate', - 'overlay.js' - ), + 'memberships-gate': path.join( __dirname, 'assets', 'memberships-gate', 'gate.js' ), 'memberships-gate-metering': path.join( __dirname, 'assets', From 2199d691c2fada36c455ba892047b145bb9f3728 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Nov 2023 09:36:12 +0000 Subject: [PATCH 02/15] chore(deps-dev): bump @wordpress/browserslist-config Bumps [@wordpress/browserslist-config](https://github.com/WordPress/gutenberg/tree/HEAD/packages/browserslist-config) from 5.28.0 to 5.29.0. - [Release notes](https://github.com/WordPress/gutenberg/releases) - [Changelog](https://github.com/WordPress/gutenberg/blob/trunk/packages/browserslist-config/CHANGELOG.md) - [Commits](https://github.com/WordPress/gutenberg/commits/@wordpress/browserslist-config@5.29.0/packages/browserslist-config) --- updated-dependencies: - dependency-name: "@wordpress/browserslist-config" dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index e815772375..0b11ba8dad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,7 +28,7 @@ "devDependencies": { "@rushstack/eslint-patch": "^1.5.1", "@testing-library/react": "^12.1.4", - "@wordpress/browserslist-config": "^5.28.0", + "@wordpress/browserslist-config": "^5.29.0", "eslint": "^7.32.0", "lint-staged": "^13.2.3", "newspack-scripts": "^5.1.0", @@ -6109,9 +6109,9 @@ "dev": true }, "node_modules/@wordpress/browserslist-config": { - "version": "5.28.0", - "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-5.28.0.tgz", - "integrity": "sha512-6O4mgi4mZAizyPpcKjXoXwcF7onL+BJckH3M1JnnXoa0aBb42TB3wQMTYDcGc1Kg1sRD4HWaDl53inWdmiyk7g==", + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-5.29.0.tgz", + "integrity": "sha512-sVDgPWcI3wdKd3cIvgf/NLO7HtEkwyV4bWuSztzoemNMGQW17BhH2Blx46HnJFXm9ooYw4SpAOyioHTkuT+JFg==", "dev": true, "engines": { "node": ">=14" @@ -35081,9 +35081,9 @@ } }, "@wordpress/browserslist-config": { - "version": "5.28.0", - "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-5.28.0.tgz", - "integrity": "sha512-6O4mgi4mZAizyPpcKjXoXwcF7onL+BJckH3M1JnnXoa0aBb42TB3wQMTYDcGc1Kg1sRD4HWaDl53inWdmiyk7g==", + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-5.29.0.tgz", + "integrity": "sha512-sVDgPWcI3wdKd3cIvgf/NLO7HtEkwyV4bWuSztzoemNMGQW17BhH2Blx46HnJFXm9ooYw4SpAOyioHTkuT+JFg==", "dev": true }, "@wordpress/components": { diff --git a/package.json b/package.json index a2d8aefc95..0920a7f6b7 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@rushstack/eslint-patch": "^1.5.1", "@testing-library/react": "^12.1.4", - "@wordpress/browserslist-config": "^5.28.0", + "@wordpress/browserslist-config": "^5.29.0", "eslint": "^7.32.0", "lint-staged": "^13.2.3", "newspack-scripts": "^5.1.0", From 1bfc6f0cbdbb8a64fcf252a85c427a30e203b097 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Mon, 20 Nov 2023 10:37:29 -0300 Subject: [PATCH 03/15] fix(metering): restrict comments on gated content (#2751) --- assets/memberships-gate/metering.js | 21 +++++-------------- .../wc-memberships/class-memberships.php | 11 ++++++++++ .../plugins/wc-memberships/class-metering.php | 1 + 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/assets/memberships-gate/metering.js b/assets/memberships-gate/metering.js index 8f455f8bc3..437aafed95 100644 --- a/assets/memberships-gate/metering.js +++ b/assets/memberships-gate/metering.js @@ -57,23 +57,12 @@ function lockContent() { prompts.forEach( prompt => { prompt.parentNode.removeChild( prompt ); } ); - const visibleParagraphs = settings.visible_paragraphs; - const articleElements = document.querySelectorAll( '.entry-content > *' ); - const moreIndex = content.innerHTML.indexOf( '' ); + // Replace content. + content.innerHTML = settings.excerpt; + // Remove comments. + document.getElementById( 'comments' ).remove(); + // Append inline gate, if any. const inlineGate = document.querySelector( '.newspack-memberships__inline-gate' ); - if ( moreIndex > -1 && settings.use_more_tag ) { - content.innerHTML = content.innerHTML.substring( 0, moreIndex ); - } else { - let paragraphIndex = 0; - articleElements.forEach( element => { - if ( element.tagName === 'P' ) { - paragraphIndex++; - } - if ( paragraphIndex > visibleParagraphs ) { - content.removeChild( element ); - } - } ); - } if ( inlineGate ) { content.appendChild( inlineGate ); } diff --git a/includes/plugins/wc-memberships/class-memberships.php b/includes/plugins/wc-memberships/class-memberships.php index d897b2ccee..2155535e2f 100644 --- a/includes/plugins/wc-memberships/class-memberships.php +++ b/includes/plugins/wc-memberships/class-memberships.php @@ -593,6 +593,17 @@ public static function wc_memberships_excerpt( $excerpt, $post, $message_code ) if ( get_queried_object_id() !== $post->ID ) { return $excerpt; } + return self::get_restricted_post_excerpt( $post ); + } + + /** + * Get the post excerpt to be displayed in the gate. + * + * @param WP_Post $post Post object. + * + * @return string + */ + public static function get_restricted_post_excerpt( $post ) { $gate_post_id = self::get_gate_post_id(); $content = $post->post_content; diff --git a/includes/plugins/wc-memberships/class-metering.php b/includes/plugins/wc-memberships/class-metering.php index 44c99db78b..f83e5ddc77 100644 --- a/includes/plugins/wc-memberships/class-metering.php +++ b/includes/plugins/wc-memberships/class-metering.php @@ -112,6 +112,7 @@ public static function enqueue_scripts() { 'gate_id' => $gate_post_id, 'post_id' => get_the_ID(), 'article_view' => self::$article_view, + 'excerpt' => Memberships::get_restricted_post_excerpt( get_post() ), ] ); } From 7229e1da173f6cc423f39692b53bdfcb17127d13 Mon Sep 17 00:00:00 2001 From: Adam Borowski Date: Mon, 20 Nov 2023 08:55:50 +0100 Subject: [PATCH 04/15] chore: remove legacy code in setup --- includes/cli/class-setup.php | 46 ------------------------------------ 1 file changed, 46 deletions(-) diff --git a/includes/cli/class-setup.php b/includes/cli/class-setup.php index c54cde40c4..1094ee6690 100644 --- a/includes/cli/class-setup.php +++ b/includes/cli/class-setup.php @@ -23,10 +23,6 @@ class Setup { * * This is a command used to set up a testing site from scratch, installing required plugins and running the onboard wizard automatically * - * ## OPTIONS - * [--skip-ras] - * : If set, will not add the EXPERIMENTAL RAS flag to wp-config - * * @param array $args Positional arguments. * @param array $assoc_args Assoc arguments. * @return void @@ -47,12 +43,6 @@ public function __invoke( $args, $assoc_args ) { $this->initial_content(); - $this->campaigns_config(); - - if ( ! WP_CLI\Utils\get_flag_value( $assoc_args, 'skip-ras', false ) ) { - $this->ras_beta(); - } - WP_CLI::success( 'Done!' ); } @@ -129,40 +119,4 @@ private function initial_content() { $response = rest_do_request( $request ); WP_CLI::success( 'Initial content completed.' ); } - - /** - * Creates the newspack-popups-config file - * - * @return void - */ - private function campaigns_config() { - global $table_prefix; - $filename = WP_CONTENT_DIR . '/newspack-popups-config.php'; - if ( ! file_exists( $filename ) ) { - $content = " Date: Tue, 21 Nov 2023 10:15:31 -0300 Subject: [PATCH 05/15] feat(authentication): rate limit magic links and OTP generation (#2765) Co-authored-by: Derrick Koo --- assets/reader-activation/auth.js | 8 ++++++-- includes/class-magic-link.php | 12 ++++++++++-- .../reader-activation/class-reader-activation.php | 2 +- tests/unit-tests/magic-link.php | 14 ++++++++++++++ 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/assets/reader-activation/auth.js b/assets/reader-activation/auth.js index 0401022720..312f7c53d8 100644 --- a/assets/reader-activation/auth.js +++ b/assets/reader-activation/auth.js @@ -384,7 +384,6 @@ window.newspackRAS.push( function ( readerActivation ) { if ( ! body.has( 'npe' ) || ! body.get( 'npe' ) ) { return form.endFlow( newspack_reader_auth_labels.invalid_email, 400 ); } - readerActivation.setReaderEmail( body.get( 'npe' ) ); if ( 'otp' === action ) { readerActivation .authenticateOTP( body.get( 'otp_code' ) ) @@ -425,9 +424,14 @@ window.newspackRAS.push( function ( readerActivation ) { if ( currentHash ) { redirect = ''; } + if ( status === 200 ) { + readerActivation.setReaderEmail( body.get( 'npe' ) ); + } const otpHash = readerActivation.getOTPHash(); if ( otpHash && [ 'register', 'link' ].includes( action ) ) { - setFormAction( 'otp' ); + if ( status === 200 ) { + setFormAction( 'otp' ); + } /** If action is link, suppress message and status so the OTP handles it. */ if ( status === 200 && action === 'link' ) { status = null; diff --git a/includes/class-magic-link.php b/includes/class-magic-link.php index b71c1a8e7f..e43d0f1c9e 100644 --- a/includes/class-magic-link.php +++ b/includes/class-magic-link.php @@ -28,6 +28,8 @@ final class Magic_Link { const AUTH_ACTION_RESULT = 'np_auth_link_result'; const COOKIE = 'np_auth_link'; + const RATE_INTERVAL = 60; // Interval in seconds to rate limit token generation. + const OTP_LENGTH = 6; const OTP_MAX_ATTEMPTS = 5; const OTP_AUTH_ACTION = 'np_otp_auth'; @@ -346,11 +348,15 @@ public static function generate_token( $user ) { if ( ! empty( $tokens ) ) { /** Limit maximum tokens to 5. */ $tokens = array_slice( $tokens, -4, 4 ); - /** Clear expired tokens. */ foreach ( $tokens as $index => $token_data ) { + /** Clear expired tokens. */ if ( $token_data['time'] < $expire ) { unset( $tokens[ $index ] ); } + /** Rate limit token generation. */ + if ( $token_data['time'] + self::RATE_INTERVAL > $now ) { + return new \WP_Error( 'rate_limit_exceeded', __( 'Please wait a minute before requesting another authorization code.', 'newspack' ) ); + } } $tokens = array_values( $tokens ); } @@ -538,7 +544,9 @@ public static function validate_otp( $user_id, $hash, $code ) { $errors->add( 'invalid_otp', __( 'OTP is not enabled.', 'newspack' ) ); } else { $tokens = \get_user_meta( $user->ID, self::TOKENS_META, true ); - if ( empty( $tokens ) || empty( $hash ) || empty( $code ) ) { + if ( empty( $tokens ) || empty( $hash ) ) { + $errors->add( 'invalid_hash', __( 'Invalid hash.', 'newspack' ) ); + } elseif ( empty( $code ) ) { $errors->add( 'invalid_otp', __( 'Invalid OTP.', 'newspack' ) ); } } diff --git a/includes/reader-activation/class-reader-activation.php b/includes/reader-activation/class-reader-activation.php index a534df514e..baf6da30c7 100644 --- a/includes/reader-activation/class-reader-activation.php +++ b/includes/reader-activation/class-reader-activation.php @@ -1460,7 +1460,7 @@ public static function process_auth_form() { case 'link': $sent = Magic_Link::send_email( $user ); if ( true !== $sent ) { - return self::send_auth_form_response( new \WP_Error( 'unauthorized', __( 'We encountered an error sending an authentication link. Please try again.', 'newspack-plugin' ) ) ); + return self::send_auth_form_response( new \WP_Error( 'unauthorized', \is_wp_error( $sent ) ? $sent->get_error_message() : __( 'We encountered an error sending an authentication link. Please try again.', 'newspack-plugin' ) ) ); } return self::send_auth_form_response( $payload, __( 'Please check your inbox for an authentication link.', 'newspack-plugin' ), $redirect ); case 'register': diff --git a/tests/unit-tests/magic-link.php b/tests/unit-tests/magic-link.php index 45afc8a25a..955f18c6e3 100644 --- a/tests/unit-tests/magic-link.php +++ b/tests/unit-tests/magic-link.php @@ -57,6 +57,8 @@ public function set_up() { ] ); } + // Remove tokens. + delete_user_meta( self::$user_id, Magic_Link::TOKENS_META ); // Create sample admin. if ( empty( self::$admin_id ) ) { @@ -69,6 +71,8 @@ public function set_up() { ] ); } + // Remove tokens. + delete_user_meta( self::$admin_id, Magic_Link::TOKENS_META ); } /** @@ -99,6 +103,16 @@ public function test_generate_token() { $this->assertTokenIsValid( $token_data ); } + /** + * Test rate limiting of token generation. + */ + public function test_rate_limit() { + $token_data = Magic_Link::generate_token( get_user_by( 'id', self::$user_id ) ); + $new_token = Magic_Link::generate_token( get_user_by( 'id', self::$user_id ) ); + $this->assertTrue( is_wp_error( $new_token ) ); + $this->assertEquals( 'rate_limit_exceeded', $new_token->get_error_code() ); + } + /** * Test simple token validation. */ From ffc0174fe93c07c0e8c673cc4713de42a885e34c Mon Sep 17 00:00:00 2001 From: leogermani Date: Tue, 21 Nov 2023 12:26:54 -0300 Subject: [PATCH 06/15] Fix/donation events renewals (#2756) * feat: add renewal and subscription id to events * fix: do not fire prompt interaction on renewals * feat: add renewal and subscription_id to ga4 event * docs: update events docs * docs: fix data type --- includes/data-events/README.md | 72 +++++++++------- includes/data-events/class-popups.php | 6 +- includes/data-events/connectors/ga4/README.md | 5 +- .../data-events/connectors/ga4/class-ga4.php | 17 ++-- includes/data-events/listeners.php | 85 ++++++++++++------- 5 files changed, 109 insertions(+), 76 deletions(-) diff --git a/includes/data-events/README.md b/includes/data-events/README.md index c4af3d4cb2..d9b6c28cf8 100644 --- a/includes/data-events/README.md +++ b/includes/data-events/README.md @@ -78,17 +78,19 @@ When a reader updates their lists subscription from Newspack Newsletters. For when there's a new donation processed through WooCommerce. -| Name | Type | Obs | -| --------------- | -------- | ------------------------------------------------------ | -| `user_id` | `int` | | -| `email` | `string` | | -| `amount` | `float` | | -| `currency` | `string` | | -| `recurrence` | `string` | | -| `platform` | `string` | Always `wc` in this case | -| `referer` | `string` | | -| `popup_id` | `string` | If the donation was triggered by a popup, the popup ID | -| `platform_data` | `array` | | +| Name | Type | Obs | +| ---------------- | -------- | ------------------------------------------------------ | +| `user_id` | `int` | | +| `email` | `string` | | +| `amount` | `float` | | +| `currency` | `string` | | +| `recurrence` | `string` | | +| `platform` | `string` | Always `wc` in this case | +| `referer` | `string` | | +| `popup_id` | `string` | If the donation was triggered by a popup, the popup ID | +| `is_renewal` | `bool` | If this is a subscription renewal (recurring payment) | +| `subscription_id`| `int` | The related subscription id (if any) | +| `platform_data` | `array` | | ### `woocommerce_order_failed` @@ -96,33 +98,37 @@ For when there's a new donation payment failed through WooCommerce. Known issue: If the user tries to pay again after a failed payment, and the payment fails for a second time, the order is already marked as failed so this hook will not trigger. -| Name | Type | Obs | -| --------------- | -------- | ------------------------------------------------------ | -| `user_id` | `int` | | -| `email` | `string` | | -| `amount` | `float` | | -| `currency` | `string` | | -| `recurrence` | `string` | | -| `platform` | `string` | Always `wc` in this case | -| `referer` | `string` | | -| `popup_id` | `string` | If the donation was triggered by a popup, the popup ID | -| `platform_data` | `array` | | +| Name | Type | Obs | +| ---------------- | -------- | ------------------------------------------------------ | +| `user_id` | `int` | | +| `email` | `string` | | +| `amount` | `float` | | +| `currency` | `string` | | +| `recurrence` | `string` | | +| `platform` | `string` | Always `wc` in this case | +| `referer` | `string` | | +| `is_renewal` | `bool` | If this is a subscription renewal (recurring payment) | +| `subscription_id`| `int` | The related subscription id (if any) | +| `popup_id` | `string` | If the donation was triggered by a popup, the popup ID | +| `platform_data` | `array` | | ### `donation_new` When there's a new donation, either through Stripe or Newspack (WooCommerce) platforms. -| Name | Type | Obs | -| --------------- | -------- | ------------------------------------------------------ | -| `user_id` | `int` | | -| `email` | `string` | | -| `amount` | `float` | | -| `currency` | `string` | | -| `recurrence` | `string` | | -| `platform` | `string` | | -| `referer` | `string` | | -| `popup_id` | `string` | If the donation was triggered by a popup, the popup ID | -| `platform_data` | `array` | | +| Name | Type | Obs | +| ---------------- | -------- | ------------------------------------------------------ | +| `user_id` | `int` | | +| `email` | `string` | | +| `amount` | `float` | | +| `currency` | `string` | | +| `recurrence` | `string` | | +| `platform` | `string` | | +| `referer` | `string` | | +| `popup_id` | `string` | If the donation was triggered by a popup, the popup ID | +| `is_renewal` | `bool` | If this is a subscription renewal (recurring payment) | +| `subscription_id`| `int` | The related subscription id (if any) | +| `platform_data` | `array` | | ### `donation_subscription_new` diff --git a/includes/data-events/class-popups.php b/includes/data-events/class-popups.php index 7497c23337..4d72135f6c 100644 --- a/includes/data-events/class-popups.php +++ b/includes/data-events/class-popups.php @@ -222,7 +222,7 @@ public static function newsletter_submission_with_status( $email, $result, $meta */ public static function donation_submission_success( $timestamp, $data ) { $popup_id = $data['popup_id'] ?? false; - if ( ! $popup_id ) { + if ( ! $popup_id || $data['is_renewal'] ) { return; } $popup_data = Newspack_Popups_Data_Api::get_popup_metadata( $popup_id ); @@ -310,7 +310,7 @@ public static function donation_submission_stripe_error( $config, $stripe_data, */ public static function donation_submission_woocommerce( $timestamp, $data ) { $popup_id = $data['popup_id'] ?? false; - if ( ! $popup_id ) { + if ( ! $popup_id || $data['is_renewal'] ) { return; } $popup_data = Newspack_Popups_Data_Api::get_popup_metadata( $popup_id ); @@ -340,7 +340,7 @@ public static function donation_submission_woocommerce( $timestamp, $data ) { */ public static function donation_submission_woocommerce_error( $timestamp, $data ) { $popup_id = $data['popup_id'] ?? false; - if ( ! $popup_id ) { + if ( ! $popup_id || $data['is_renewal'] ) { return; } $popup_data = Newspack_Popups_Data_Api::get_popup_metadata( $popup_id ); diff --git a/includes/data-events/connectors/ga4/README.md b/includes/data-events/connectors/ga4/README.md index 82b9225d1a..0350dde700 100644 --- a/includes/data-events/connectors/ga4/README.md +++ b/includes/data-events/connectors/ga4/README.md @@ -57,10 +57,11 @@ Additional parameters: * `recurrence` * `platform` * `referer` +* `is_renewal`: If this is a subscription renewal (recurring payment). +* `subscription_id`: The related subscription id (if any). * `popup_id`: If the action was triggered from inside a popup, the popup id. * `range`: The range of the donation amount: `under-20`, `20-50`, `51-100`, `101-200`, `201-500` or `over-500`. - ### donation_subscription_cancelled When a subscription is cancelled. @@ -75,8 +76,6 @@ Additional parameters: ### newsletter_subscribed - - Additional parameters: * `newsletters_subscription_method` diff --git a/includes/data-events/connectors/ga4/class-ga4.php b/includes/data-events/connectors/ga4/class-ga4.php index 07c8de17a0..e4af422c22 100644 --- a/includes/data-events/connectors/ga4/class-ga4.php +++ b/includes/data-events/connectors/ga4/class-ga4.php @@ -288,13 +288,15 @@ public static function handle_reader_registered( $params, $data ) { * @return array $params The final version of the GA4 event params that will be sent to GA. */ public static function handle_donation_new( $params, $data ) { - $params['amount'] = $data['amount']; - $params['currency'] = $data['currency']; - $params['recurrence'] = $data['recurrence']; - $params['platform'] = $data['platform']; - $params['referer'] = $data['referer'] ?? ''; - $params['popup_id'] = $data['popup_id'] ?? ''; - $params['range'] = self::get_donation_amount_range( $data['amount'] ); + $params['amount'] = $data['amount']; + $params['currency'] = $data['currency']; + $params['recurrence'] = $data['recurrence']; + $params['platform'] = $data['platform']; + $params['referer'] = $data['referer'] ?? ''; + $params['popup_id'] = $data['popup_id'] ?? ''; + $params['is_renewal'] = $data['is_renewal'] ? 'yes' : 'no'; + $params['subscription_id'] = $data['subscription_id'] ?? ''; + $params['range'] = self::get_donation_amount_range( $data['amount'] ); return $params; } @@ -486,6 +488,7 @@ private static function send_event( Event $event, $client_id, $timestamp, $user_ ); self::log( sprintf( 'Event sent to %s - %s - Client ID: %s', $property['measurement_id'], $event->get_name(), $client_id ) ); + self::log( sprintf( 'Event payload: %s', wp_json_encode( $payload ) ) ); } } diff --git a/includes/data-events/listeners.php b/includes/data-events/listeners.php index cb2759952c..850338406c 100644 --- a/includes/data-events/listeners.php +++ b/includes/data-events/listeners.php @@ -138,17 +138,26 @@ function( $order_id, $product_id ) { if ( ! $order ) { return; } - $recurrence = get_post_meta( $product_id, '_subscription_period', true ); + $recurrence = get_post_meta( $product_id, '_subscription_period', true ); + $is_renewal = function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order ); + $subscription_id = null; + if ( function_exists( 'wcs_get_subscriptions_for_renewal_order' ) ) { + $subscriptions = array_values( wcs_get_subscriptions_for_renewal_order( $order ) ); + $subscription_id = is_array( $subscriptions ) && ! empty( $subscriptions ) && is_a( $subscriptions[0], 'WC_Subscription' ) ? $subscriptions[0]->get_id() : null; + } + return [ - 'user_id' => $order->get_customer_id(), - 'email' => $order->get_billing_email(), - 'amount' => (float) $order->get_total(), - 'currency' => $order->get_currency(), - 'recurrence' => empty( $recurrence ) ? 'once' : $recurrence, - 'platform' => 'wc', - 'referer' => $order->get_meta( '_newspack_referer' ), - 'popup_id' => $order->get_meta( '_newspack_popup_id' ), - 'platform_data' => [ + 'user_id' => $order->get_customer_id(), + 'email' => $order->get_billing_email(), + 'amount' => (float) $order->get_total(), + 'currency' => $order->get_currency(), + 'recurrence' => empty( $recurrence ) ? 'once' : $recurrence, + 'platform' => 'wc', + 'referer' => $order->get_meta( '_newspack_referer' ), + 'popup_id' => $order->get_meta( '_newspack_popup_id' ), + 'is_renewal' => $is_renewal, + 'subscription_id' => $subscription_id, + 'platform_data' => [ 'order_id' => $order_id, 'product_id' => $product_id, ], @@ -170,18 +179,26 @@ function( $order_id, $order ) { if ( ! $product_id ) { return; } - $recurrence = get_post_meta( $product_id, '_subscription_period', true ); + $recurrence = get_post_meta( $product_id, '_subscription_period', true ); + $is_renewal = function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order ); + $subscription_id = null; + if ( function_exists( 'wcs_get_subscriptions_for_renewal_order' ) ) { + $subscriptions = array_values( wcs_get_subscriptions_for_renewal_order( $order ) ); + $subscription_id = is_array( $subscriptions ) && ! empty( $subscriptions ) && is_a( $subscriptions[0], 'WC_Subscription' ) ? $subscriptions[0]->get_id() : null; + } return [ - 'user_id' => $order->get_customer_id(), - 'email' => $order->get_billing_email(), - 'amount' => (float) $order->get_total(), - 'currency' => $order->get_currency(), - 'recurrence' => empty( $recurrence ) ? 'once' : $recurrence, - 'platform' => Donations::get_platform_slug(), - 'referer' => $order->get_meta( '_newspack_referer' ), - 'popup_id' => $order->get_meta( '_newspack_popup_id' ), - 'platform_data' => [ + 'user_id' => $order->get_customer_id(), + 'email' => $order->get_billing_email(), + 'amount' => (float) $order->get_total(), + 'currency' => $order->get_currency(), + 'recurrence' => empty( $recurrence ) ? 'once' : $recurrence, + 'platform' => Donations::get_platform_slug(), + 'referer' => $order->get_meta( '_newspack_referer' ), + 'popup_id' => $order->get_meta( '_newspack_popup_id' ), + 'is_renewal' => $is_renewal, + 'subscription_id' => $subscription_id, + 'platform_data' => [ 'order_id' => $order_id, 'product_id' => $product_id, 'client_id' => $order->get_meta( NEWSPACK_CLIENT_ID_COOKIE_NAME ), @@ -230,18 +247,26 @@ function( $order_id, $order ) { if ( ! $product_id ) { return; } - $recurrence = get_post_meta( $product_id, '_subscription_period', true ); + $recurrence = get_post_meta( $product_id, '_subscription_period', true ); + $is_renewal = function_exists( 'wcs_order_contains_renewal' ) && wcs_order_contains_renewal( $order ); + $subscription_id = null; + if ( function_exists( 'wcs_get_subscriptions_for_renewal_order' ) ) { + $subscriptions = array_values( wcs_get_subscriptions_for_renewal_order( $order ) ); + $subscription_id = is_array( $subscriptions ) && ! empty( $subscriptions ) && is_a( $subscriptions[0], 'WC_Subscription' ) ? $subscriptions[0]->get_id() : null; + } return [ - 'user_id' => $order->get_customer_id(), - 'email' => $order->get_billing_email(), - 'amount' => (float) $order->get_total(), - 'currency' => $order->get_currency(), - 'recurrence' => empty( $recurrence ) ? 'once' : $recurrence, - 'platform' => Donations::get_platform_slug(), - 'referer' => $order->get_meta( '_newspack_referer' ), - 'popup_id' => $order->get_meta( '_newspack_popup_id' ), - 'platform_data' => [ + 'user_id' => $order->get_customer_id(), + 'email' => $order->get_billing_email(), + 'amount' => (float) $order->get_total(), + 'currency' => $order->get_currency(), + 'recurrence' => empty( $recurrence ) ? 'once' : $recurrence, + 'platform' => Donations::get_platform_slug(), + 'referer' => $order->get_meta( '_newspack_referer' ), + 'popup_id' => $order->get_meta( '_newspack_popup_id' ), + 'is_renewal' => $is_renewal, + 'subscription_id' => $subscription_id, + 'platform_data' => [ 'order_id' => $order_id, 'product_id' => $product_id, 'client_id' => $order->get_meta( NEWSPACK_CLIENT_ID_COOKIE_NAME ), From 975ab96032694ff5f5c25b6f0f72f25f3cdb9ca9 Mon Sep 17 00:00:00 2001 From: Miguel Peixe Date: Tue, 21 Nov 2023 12:57:32 -0300 Subject: [PATCH 07/15] fix(data-events): no longer use ActionScheduler for dispatches (#2755) --- includes/data-events/class-data-events.php | 70 ++++++------------- includes/data-events/class-webhooks.php | 26 +++++-- .../data-events/connectors/ga4/class-ga4.php | 4 ++ 3 files changed, 47 insertions(+), 53 deletions(-) diff --git a/includes/data-events/class-data-events.php b/includes/data-events/class-data-events.php index a2bb1e8d1f..534a056103 100644 --- a/includes/data-events/class-data-events.php +++ b/includes/data-events/class-data-events.php @@ -43,26 +43,8 @@ final class Data_Events { public static function init() { \add_action( 'wp_ajax_' . self::ACTION, [ __CLASS__, 'maybe_handle' ] ); \add_action( 'wp_ajax_nopriv_' . self::ACTION, [ __CLASS__, 'maybe_handle' ] ); - \add_action( 'newspack_data_events_as_dispatch', [ __CLASS__, 'handle' ], 10, 4 ); } - /** - * Whether to use Action Scheduler if available. - * - * @return bool - */ - public static function use_action_scheduler() { - $use_action_scheduler = false; - if ( function_exists( 'as_enqueue_async_action' ) ) { - $use_action_scheduler = true; - } - /** - * Filters whether to use the Action Scheduler if available. - * - * @param bool $use_action_scheduler - */ - return \apply_filters( 'newspack_data_events_use_action_scheduler', $use_action_scheduler ); - } /** * Maybe handle an event. @@ -330,39 +312,29 @@ public static function dispatch( $action_name, $data, $use_client_id = true ) { return $body; } - if ( self::use_action_scheduler() ) { - Logger::log( - sprintf( 'Dispatching action "%s" via Action Scheduler.', $action_name ), - self::LOGGER_HEADER - ); - - \as_enqueue_async_action( 'newspack_data_events_as_dispatch', array_values( $body ), 'newspack-data-events' ); - - } else { - Logger::log( - sprintf( 'Dispatching action "%s" via HTTP request.', $action_name ), - self::LOGGER_HEADER - ); + Logger::log( + sprintf( 'Dispatching action "%s".', $action_name ), + self::LOGGER_HEADER + ); - $url = \add_query_arg( - [ - 'action' => self::ACTION, - 'nonce' => \wp_create_nonce( self::ACTION ), - ], - \admin_url( 'admin-ajax.php' ) - ); + $url = \add_query_arg( + [ + 'action' => self::ACTION, + 'nonce' => \wp_create_nonce( self::ACTION ), + ], + \admin_url( 'admin-ajax.php' ) + ); - return \wp_remote_post( - $url, - [ - 'timeout' => 0.01, - 'blocking' => false, - 'body' => $body, - 'cookies' => $_COOKIE, // phpcs:ignore - 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), - ] - ); - } + return \wp_remote_post( + $url, + [ + 'timeout' => 0.01, + 'blocking' => false, + 'body' => $body, + 'cookies' => $_COOKIE, // phpcs:ignore + 'sslverify' => apply_filters( 'https_local_ssl_verify', false ), + ] + ); } } Data_Events::init(); diff --git a/includes/data-events/class-webhooks.php b/includes/data-events/class-webhooks.php index 7044672f55..3b0fc9c882 100644 --- a/includes/data-events/class-webhooks.php +++ b/includes/data-events/class-webhooks.php @@ -175,12 +175,30 @@ public static function clear_finished() { } } + /** + * Whether to use Action Scheduler if available. + * + * @return bool + */ + private static function use_action_scheduler() { + $use_action_scheduler = false; + if ( function_exists( 'as_enqueue_async_action' ) ) { + $use_action_scheduler = true; + } + /** + * Filters whether to use the Action Scheduler if available. + * + * @param bool $use_action_scheduler + */ + return \apply_filters( 'newspack_data_events_use_action_scheduler', $use_action_scheduler ); + } + /** * Send webhook requests that should've been sent but are still pending at * 'future' status. */ public static function send_late_requests() { - if ( Data_Events::use_action_scheduler() ) { + if ( self::use_action_scheduler() ) { return; } $requests = \get_posts( @@ -602,7 +620,7 @@ public static function transition_post_status( $new_status, $old_status, $post ) self::REQUEST_POST_TYPE === $post->post_type && 'publish' === $new_status && 'publish' !== $old_status && - ! Data_Events::use_action_scheduler() + ! self::use_action_scheduler() ) { self::process_request( $post->ID ); } @@ -635,7 +653,7 @@ private static function schedule_request( $request_id, $delay = 1 ) { $date = date( 'Y-m-d H:i:s', $time ); // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date $date_gmt = gmdate( 'Y-m-d H:i:s', strtotime( $date ) ); \update_post_meta( $request_id, 'scheduled', $time ); - if ( Data_Events::use_action_scheduler() ) { + if ( self::use_action_scheduler() ) { Logger::log( "Scheduling request {$request_id} for {$date_gmt} via Action Scheduler.", self::LOGGER_HEADER ); \as_schedule_single_action( $time, 'newspack_webhooks_as_process_request', [ $request_id ], 'newspack-data-events' ); } else { @@ -661,7 +679,7 @@ public static function finish_request( $request_id ) { Logger::log( "Finishing request {$request_id}.", self::LOGGER_HEADER ); \update_post_meta( $request_id, 'status', 'finished' ); // If using ActionScheduler, manually set to publish. - if ( Data_Events::use_action_scheduler() ) { + if ( self::use_action_scheduler() ) { \wp_publish_post( $request_id ); } } diff --git a/includes/data-events/connectors/ga4/class-ga4.php b/includes/data-events/connectors/ga4/class-ga4.php index e4af422c22..073fefa88d 100644 --- a/includes/data-events/connectors/ga4/class-ga4.php +++ b/includes/data-events/connectors/ga4/class-ga4.php @@ -224,6 +224,10 @@ public static function filter_donation_new_event_body( $body, $event_name ) { $order_id = $body['data']['interaction_data']['donation_order_id'] ?? false; } + if ( ! function_exists( 'wc_get_order' ) ) { + return $body; + } + $order = wc_get_order( $order_id ); if ( $order ) { $ga_client_id = $order->get_meta( '_newspack_ga_client_id' ); From cb5b5273a52546a5478d258859d893491f1e1311 Mon Sep 17 00:00:00 2001 From: Adam Boro Date: Tue, 21 Nov 2023 08:57:29 +0100 Subject: [PATCH 08/15] feat(campaigns): mark duplicate segments --- assets/wizards/popups/views/segments/segments-list.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/assets/wizards/popups/views/segments/segments-list.js b/assets/wizards/popups/views/segments/segments-list.js index e1064829af..a679697617 100644 --- a/assets/wizards/popups/views/segments/segments-list.js +++ b/assets/wizards/popups/views/segments/segments-list.js @@ -174,6 +174,9 @@ const SegmentActionCard = ( { description={ segmentDescription( segment ) } toggleChecked={ ! segment.configuration.is_disabled } toggleOnChange={ () => toggleSegmentStatus( segment ) } + badge={ + segment.is_criteria_duplicated ? __( 'Duplicate', 'newspack-plugin' ) : undefined + } actionText={ <>