From 4cb4a9979894499445ef665116c49e8d3b43b78c Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 15:30:27 -0300 Subject: [PATCH 01/16] feat(donations): add donation product flag checkbox to WC product edit screen Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plugins/woocommerce/class-woocommerce-products.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/includes/plugins/woocommerce/class-woocommerce-products.php b/includes/plugins/woocommerce/class-woocommerce-products.php index 2c18e4b248..9acde1ad4e 100644 --- a/includes/plugins/woocommerce/class-woocommerce-products.php +++ b/includes/plugins/woocommerce/class-woocommerce-products.php @@ -70,6 +70,15 @@ public static function get_custom_options() { 'product_types' => [ 'simple', 'variation', 'subscription', 'subscription_variation' ], 'type' => 'boolean', ], + 'newspack_is_donation' => [ + 'id' => '_newspack_is_donation', + 'wrapper_class' => '', + 'label' => __( 'Donation product', 'newspack-plugin' ), + 'description' => __( 'Flag this product as a donation. Donation products use donation-specific checkout, reporting, and reader activation behaviors.', 'newspack-plugin' ), + 'default' => 'no', + 'product_types' => [ 'simple', 'subscription', 'grouped', 'variable', 'variation', 'subscription_variation', 'variable-subscription' ], + 'type' => 'boolean', + ], ]; /** From 48822c7ce0690c4cbcca36fceb46f684d1ca5f49 Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 17:41:35 -0300 Subject: [PATCH 02/16] feat(donations): update is_donation_product to check _newspack_is_donation meta --- includes/class-donations.php | 29 ++++++++++++++++ tests/unit-tests/donations.php | 60 ++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/includes/class-donations.php b/includes/class-donations.php index ed758d3bdd..6aee3b56c5 100644 --- a/includes/class-donations.php +++ b/includes/class-donations.php @@ -278,6 +278,12 @@ public static function get_donation_product_child_products_ids() { * @return boolean True if a donation product, false if not. */ public static function is_donation_product( $product_id ) { + // Check the meta flag first (fast path). + if ( get_post_meta( $product_id, '_newspack_is_donation', true ) === '1' ) { + return true; + } + + // Fall back to the legacy parent/child donation product check. $parent_product = self::get_parent_donation_product(); if ( ! $parent_product ) { return false; @@ -286,6 +292,29 @@ public static function is_donation_product( $product_id ) { return in_array( $product_id, $donation_product_ids, true ) || $product_id === $parent_product->get_id(); } + /** + * Get IDs of all products flagged as donations via the _newspack_is_donation meta. + * + * @return int[] Array of product IDs. + */ + public static function get_flagged_donation_product_ids() { + $flagged_products = get_posts( + [ + 'post_type' => 'product', + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'fields' => 'ids', + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + [ + 'key' => '_newspack_is_donation', + 'value' => '1', + ], + ], + ] + ); + return array_map( 'intval', $flagged_products ); + } + /** * Whether the order is a donation. * diff --git a/tests/unit-tests/donations.php b/tests/unit-tests/donations.php index ade2ce1648..995de4daf9 100644 --- a/tests/unit-tests/donations.php +++ b/tests/unit-tests/donations.php @@ -28,4 +28,64 @@ public function test_donations_settings_wc() { 'WC is the default donations platform.' ); } + + /** + * Test that is_donation_product returns false for unflagged products. + * + * @group donations + */ + public function test_is_donation_product_unflagged() { + $product_id = self::factory()->post->create( [ 'post_type' => 'product' ] ); + self::assertFalse( + Donations::is_donation_product( $product_id ), + 'Unflagged product should not be a donation product.' + ); + } + + /** + * Test that is_donation_product returns true for products with _newspack_is_donation meta. + * + * @group donations + */ + public function test_is_donation_product_flagged() { + $product_id = self::factory()->post->create( [ 'post_type' => 'product' ] ); + update_post_meta( $product_id, '_newspack_is_donation', '1' ); + self::assertTrue( + Donations::is_donation_product( $product_id ), + 'Flagged product should be a donation product.' + ); + } + + /** + * Test that is_donation_product returns false when meta is removed. + * + * @group donations + */ + public function test_is_donation_product_unflagged_after_removal() { + $product_id = self::factory()->post->create( [ 'post_type' => 'product' ] ); + update_post_meta( $product_id, '_newspack_is_donation', '1' ); + delete_post_meta( $product_id, '_newspack_is_donation' ); + self::assertFalse( + Donations::is_donation_product( $product_id ), + 'Product should not be a donation product after meta removal.' + ); + } + + /** + * Test get_flagged_donation_product_ids returns flagged product IDs. + * + * @group donations + */ + public function test_get_flagged_donation_product_ids() { + $product_1 = self::factory()->post->create( [ 'post_type' => 'product' ] ); + $product_2 = self::factory()->post->create( [ 'post_type' => 'product' ] ); + $product_3 = self::factory()->post->create( [ 'post_type' => 'product' ] ); + update_post_meta( $product_1, '_newspack_is_donation', '1' ); + update_post_meta( $product_3, '_newspack_is_donation', '1' ); + + $flagged_ids = Donations::get_flagged_donation_product_ids(); + self::assertContains( $product_1, $flagged_ids, 'Flagged product 1 should be in the list.' ); + self::assertNotContains( $product_2, $flagged_ids, 'Unflagged product 2 should not be in the list.' ); + self::assertContains( $product_3, $flagged_ids, 'Flagged product 3 should be in the list.' ); + } } From 3249838a73e1b87df498fab75e6567718fbbeac2 Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 15:39:59 -0300 Subject: [PATCH 03/16] feat(donations): include flagged products in contribution meter revenue --- includes/contribution-meter/class-contribution-meter.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/includes/contribution-meter/class-contribution-meter.php b/includes/contribution-meter/class-contribution-meter.php index 075b80ef10..61c6f8213e 100644 --- a/includes/contribution-meter/class-contribution-meter.php +++ b/includes/contribution-meter/class-contribution-meter.php @@ -260,9 +260,11 @@ public static function get_donation_revenue( $start_date, $end_date ) { return new \WP_Error( 'woocommerce_inactive', __( 'WooCommerce is not active.', 'newspack-plugin' ) ); } - // Get all donation product IDs. + // Get all donation product IDs (default + flagged). $donation_products = Donations::get_donation_product_child_products_ids(); $donation_product_ids = array_filter( array_map( 'intval', array_values( $donation_products ) ) ); + $flagged_product_ids = Donations::get_flagged_donation_product_ids(); + $donation_product_ids = array_unique( array_merge( $donation_product_ids, $flagged_product_ids ) ); if ( empty( $donation_product_ids ) ) { return new \WP_Error( 'no_donation_products', __( 'No donation products found.', 'newspack-plugin' ) ); From 6bc57761327d7a06b73546da036d95f91560e8de Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 15:40:06 -0300 Subject: [PATCH 04/16] feat(donations): update is_donation_cart to support flagged products --- includes/class-donations.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/includes/class-donations.php b/includes/class-donations.php index 6aee3b56c5..6111f907a1 100644 --- a/includes/class-donations.php +++ b/includes/class-donations.php @@ -1080,15 +1080,11 @@ public static function is_donation_cart() { if ( ! self::is_platform_wc() ) { return false; } - $donation_products_ids = array_values( self::get_donation_product_child_products_ids() ); - if ( empty( $donation_products_ids ) ) { - return false; - } if ( ! WC()->cart || ! WC()->cart->cart_contents || ! is_array( WC()->cart->cart_contents ) ) { return false; } foreach ( WC()->cart->cart_contents as $prod_in_cart ) { - if ( isset( $prod_in_cart['product_id'] ) && in_array( $prod_in_cart['product_id'], $donation_products_ids ) ) { + if ( isset( $prod_in_cart['product_id'] ) && self::is_donation_product( $prod_in_cart['product_id'] ) ) { return true; } } From e7babac29a955c1db2144ce7b31f406978d84672 Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 15:41:51 -0300 Subject: [PATCH 05/16] feat(donations): add REST endpoints for flagged donation products --- .../audience/class-audience-donations.php | 148 ++++++++++++++++++ 1 file changed, 148 insertions(+) diff --git a/includes/wizards/audience/class-audience-donations.php b/includes/wizards/audience/class-audience-donations.php index 794342283b..7032304828 100644 --- a/includes/wizards/audience/class-audience-donations.php +++ b/includes/wizards/audience/class-audience-donations.php @@ -133,6 +133,51 @@ public function register_api_endpoints() { 'permission_callback' => [ $this, 'api_permissions_check' ], ] ); + + // Get flagged donation products. + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/donation-products', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_get_donation_products' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + ] + ); + + // Flag or unflag a product as donation. + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/donation-products/(?P\d+)', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [ $this, 'api_toggle_donation_product' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'is_donation' => [ + 'required' => true, + 'sanitize_callback' => 'Newspack\newspack_string_to_bool', + ], + ], + ] + ); + + // Search WooCommerce products for the product picker. + register_rest_route( + NEWSPACK_API_NAMESPACE, + '/wizard/' . $this->slug . '/products-search', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [ $this, 'api_search_products' ], + 'permission_callback' => [ $this, 'api_permissions_check' ], + 'args' => [ + 'search' => [ + 'required' => true, + 'sanitize_callback' => 'sanitize_text_field', + ], + ], + ] + ); } /** @@ -174,6 +219,7 @@ public function fetch_all_data() { 'donation_data' => Donations::get_donation_settings(), 'donation_page' => Donations::get_donation_page_info(), 'product_validation' => $this->validate_donation_products(), + 'donation_products' => $this->get_donation_products_data(), ]; if ( 'wc' === $platform ) { $plugin_status = true; @@ -256,6 +302,108 @@ public function api_reset_donation_email( $request ) { ); } + /** + * Get all products flagged as donations. + * + * @return \WP_REST_Response + */ + public function api_get_donation_products() { + $flagged_ids = Donations::get_flagged_donation_product_ids(); + $products = []; + foreach ( $flagged_ids as $product_id ) { + $product = \wc_get_product( $product_id ); + if ( $product ) { + $products[] = [ + 'id' => $product->get_id(), + 'name' => $product->get_name(), + 'type' => $product->get_type(), + 'edit_link' => get_edit_post_link( $product->get_id(), 'raw' ), + ]; + } + } + return rest_ensure_response( $products ); + } + + /** + * Flag or unflag a product as donation. + * + * @param \WP_REST_Request $request Request object. + * @return \WP_REST_Response|\WP_Error + */ + public function api_toggle_donation_product( $request ) { + $product_id = absint( $request->get_param( 'id' ) ); + $is_donation = $request->get_param( 'is_donation' ); + $product = \wc_get_product( $product_id ); + + if ( ! $product ) { + return new \WP_Error( 'invalid_product', __( 'Product not found.', 'newspack-plugin' ), [ 'status' => 404 ] ); + } + + if ( $is_donation ) { + $product->update_meta_data( '_newspack_is_donation', '1' ); + } else { + $product->delete_meta_data( '_newspack_is_donation' ); + } + $product->save(); + + return $this->api_get_donation_products(); + } + + /** + * Search WooCommerce products by name. + * + * @param \WP_REST_Request $request Request object. + * @return \WP_REST_Response + */ + public function api_search_products( $request ) { + $search = $request->get_param( 'search' ); + $results = []; + + $products = \wc_get_products( + [ + 'limit' => 10, + 'status' => 'publish', + 's' => $search, + ] + ); + + foreach ( $products as $product ) { + $results[] = [ + 'id' => $product->get_id(), + 'name' => $product->get_name(), + 'type' => $product->get_type(), + 'is_donation' => get_post_meta( $product->get_id(), '_newspack_is_donation', true ) === '1', + ]; + } + + return rest_ensure_response( $results ); + } + + /** + * Get flagged donation products data for the wizard. + * + * @return array + */ + private function get_donation_products_data() { + if ( ! function_exists( 'wc_get_product' ) ) { + return []; + } + $flagged_ids = Donations::get_flagged_donation_product_ids(); + $products = []; + foreach ( $flagged_ids as $product_id ) { + $product = \wc_get_product( $product_id ); + if ( $product ) { + $products[] = [ + 'id' => $product->get_id(), + 'name' => $product->get_name(), + 'type' => $product->get_type(), + 'edit_link' => get_edit_post_link( $product->get_id(), 'raw' ), + ]; + } + } + return $products; + } + /** * Check whether WooCommerce is installed and active. * From 9ba0391a23491afb58446f0498b1265ac8c6732b Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 15:44:30 -0300 Subject: [PATCH 06/16] feat(donations): add wizard UI for managing flagged donation products Co-Authored-By: Claude Sonnet 4.6 --- .../views/donations/configuration/index.tsx | 95 ++++++++++++++++++- 1 file changed, 94 insertions(+), 1 deletion(-) diff --git a/src/wizards/audience/views/donations/configuration/index.tsx b/src/wizards/audience/views/donations/configuration/index.tsx index 479d4e1a97..3ece04c80a 100644 --- a/src/wizards/audience/views/donations/configuration/index.tsx +++ b/src/wizards/audience/views/donations/configuration/index.tsx @@ -2,6 +2,8 @@ * WordPress dependencies. */ import { __, sprintf } from '@wordpress/i18n'; +import { useState } from '@wordpress/element'; +import apiFetch from '@wordpress/api-fetch'; import { useDispatch } from '@wordpress/data'; import { ToggleControl, ExternalLink } from '@wordpress/components'; @@ -9,7 +11,7 @@ import { ToggleControl, ExternalLink } from '@wordpress/components'; * Internal dependencies. */ import MoneyInput from '../../../components/money-input'; -import { Button, Card, Grid, Notice, SectionHeader, SelectControl, TextControl } from '../../../../../../packages/components/src'; +import { ActionCard, Button, Card, Grid, Notice, SectionHeader, SelectControl, TextControl } from '../../../../../../packages/components/src'; import { useWizardData } from '../../../../../../packages/components/src/wizard/store/utils'; import { WIZARD_STORE_NAMESPACE } from '../../../../../../packages/components/src/wizard/store'; import WizardsTab from '../../../../wizards-tab'; @@ -243,8 +245,98 @@ export const DonationAmounts = () => { ); }; +type DonationProduct = { + id: number; + name: string; + type: string; + edit_link: string; + is_donation?: boolean; +}; + +function DonationProducts( { products }: { products: DonationProduct[] } ) { + const [ searchQuery, setSearchQuery ] = useState( '' ); + const [ searchResults, setSearchResults ] = useState< DonationProduct[] >( [] ); + const [ isSearching, setIsSearching ] = useState( false ); + const { wizardApiFetch } = useDispatch( WIZARD_STORE_NAMESPACE ); + + const handleSearch = async () => { + if ( ! searchQuery.trim() ) { + return; + } + setIsSearching( true ); + try { + const results = await apiFetch< DonationProduct[] >( { + path: `/newspack/v1/wizard/${ AUDIENCE_DONATIONS_WIZARD_SLUG }/products-search?search=${ encodeURIComponent( searchQuery ) }`, + } ); + setSearchResults( results.filter( ( r: DonationProduct ) => ! r.is_donation ) ); + } finally { + setIsSearching( false ); + } + }; + + const toggleProduct = async ( productId: number, isDonation: boolean ) => { + await wizardApiFetch( { + path: `/newspack/v1/wizard/${ AUDIENCE_DONATIONS_WIZARD_SLUG }/donation-products/${ productId }`, + method: 'POST', + data: { is_donation: isDonation }, + updateEncoder: ( data: { donation_products: DonationProduct[] } ) => ( { + wizardData: { donation_products: data }, + } ), + } ); + setSearchResults( prev => prev.filter( r => r.id !== productId ) ); + setSearchQuery( '' ); + }; + + return ( + + + { products.map( ( product: DonationProduct ) => ( + toggleProduct( product.id, false ) } + > + + + ) ) } +
+ e.key === 'Enter' && handleSearch() } + /> + +
+ { searchResults.map( ( product: DonationProduct ) => ( + toggleProduct( product.id, true ) } + /> + ) ) } +
+ ); +} + const Donation = () => { const wizardData = useWizardData( AUDIENCE_DONATIONS_WIZARD_SLUG ) as AudienceDonationsWizardData; + const { donation_products } = wizardData; const { saveWizardSettings } = useDispatch( WIZARD_STORE_NAMESPACE ); const onSaveDonationSettings = () => saveWizardSettings( { @@ -326,6 +418,7 @@ const Donation = () => { { __( 'Save Settings', 'newspack-plugin' ) } + ); }; From e75c92457ca4e78ecd811886ed378d13ed296faf Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 16:00:30 -0300 Subject: [PATCH 07/16] fix(donations): address code review feedback Co-Authored-By: Claude Sonnet 4.6 --- .../wizards/audience/class-audience-donations.php | 15 +-------------- .../views/donations/configuration/index.tsx | 4 +++- src/wizards/types/hooks.d.ts | 6 ++++++ 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/includes/wizards/audience/class-audience-donations.php b/includes/wizards/audience/class-audience-donations.php index 7032304828..bc2028f50c 100644 --- a/includes/wizards/audience/class-audience-donations.php +++ b/includes/wizards/audience/class-audience-donations.php @@ -308,20 +308,7 @@ public function api_reset_donation_email( $request ) { * @return \WP_REST_Response */ public function api_get_donation_products() { - $flagged_ids = Donations::get_flagged_donation_product_ids(); - $products = []; - foreach ( $flagged_ids as $product_id ) { - $product = \wc_get_product( $product_id ); - if ( $product ) { - $products[] = [ - 'id' => $product->get_id(), - 'name' => $product->get_name(), - 'type' => $product->get_type(), - 'edit_link' => get_edit_post_link( $product->get_id(), 'raw' ), - ]; - } - } - return rest_ensure_response( $products ); + return rest_ensure_response( $this->get_donation_products_data() ); } /** diff --git a/src/wizards/audience/views/donations/configuration/index.tsx b/src/wizards/audience/views/donations/configuration/index.tsx index 3ece04c80a..adafbe61cd 100644 --- a/src/wizards/audience/views/donations/configuration/index.tsx +++ b/src/wizards/audience/views/donations/configuration/index.tsx @@ -269,6 +269,8 @@ function DonationProducts( { products }: { products: DonationProduct[] } ) { path: `/newspack/v1/wizard/${ AUDIENCE_DONATIONS_WIZARD_SLUG }/products-search?search=${ encodeURIComponent( searchQuery ) }`, } ); setSearchResults( results.filter( ( r: DonationProduct ) => ! r.is_donation ) ); + } catch { + setSearchResults( [] ); } finally { setIsSearching( false ); } @@ -418,7 +420,7 @@ const Donation = () => { { __( 'Save Settings', 'newspack-plugin' ) } - + ); }; diff --git a/src/wizards/types/hooks.d.ts b/src/wizards/types/hooks.d.ts index 2d16fcc183..68a2fe9ba6 100644 --- a/src/wizards/types/hooks.d.ts +++ b/src/wizards/types/hooks.d.ts @@ -108,4 +108,10 @@ type AudienceDonationsWizardData = { product_validation: { [key: string]: ProductValidation; }; + donation_products?: Array<{ + id: number; + name: string; + type: string; + edit_link: string; + }>; }; From 132ab1a4c47ab7d877fa40825b20a4f2dffd4302 Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 16:12:58 -0300 Subject: [PATCH 08/16] refactor(donations): replace wizard UI with products list column and filter Remove the Additional Donation Products section from the donations wizard, its REST endpoints, and TypeScript types. Add a Donation column and dropdown filter to the WooCommerce products list instead. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-woocommerce-products.php | 88 ++++++++++++ .../audience/class-audience-donations.php | 136 +----------------- .../views/donations/configuration/index.tsx | 97 +------------ src/wizards/types/hooks.d.ts | 6 - 4 files changed, 90 insertions(+), 237 deletions(-) diff --git a/includes/plugins/woocommerce/class-woocommerce-products.php b/includes/plugins/woocommerce/class-woocommerce-products.php index 9acde1ad4e..19a1aa7010 100644 --- a/includes/plugins/woocommerce/class-woocommerce-products.php +++ b/includes/plugins/woocommerce/class-woocommerce-products.php @@ -31,6 +31,10 @@ public static function init() { \add_filter( 'woocommerce_order_item_needs_processing', [ __CLASS__, 'require_order_processing' ], 10, 2 ); \add_filter( 'woocommerce_product_description_heading', '__return_false', 10, 2 ); \add_filter( 'woocommerce_product_additional_information_heading', '__return_false', 10, 2 ); + \add_filter( 'manage_product_posts_columns', [ __CLASS__, 'add_donation_column' ] ); + \add_action( 'manage_product_posts_custom_column', [ __CLASS__, 'render_donation_column' ], 10, 2 ); + \add_action( 'restrict_manage_posts', [ __CLASS__, 'add_donation_filter' ] ); + \add_action( 'pre_get_posts', [ __CLASS__, 'filter_by_donation' ] ); } /** @@ -369,6 +373,90 @@ public static function require_order_processing( $needs_proccessing, $product ) } return self::get_custom_option_value( $product, 'newspack_autocomplete_orders' ) ? false : $needs_proccessing; } + + /** + * Add "Donation" column to the products list table. + * + * @param array $columns Existing columns. + * @return array Modified columns. + */ + public static function add_donation_column( $columns ) { + $columns['newspack_donation'] = __( 'Donation', 'newspack-plugin' ); + return $columns; + } + + /** + * Render the "Donation" column content. + * + * @param string $column Column name. + * @param int $post_id Post ID. + */ + public static function render_donation_column( $column, $post_id ) { + if ( 'newspack_donation' !== $column ) { + return; + } + if ( Donations::is_donation_product( $post_id ) ) { + echo ''; + } + } + + /** + * Add a dropdown filter for donation products on the products list screen. + * + * @param string $post_type The current post type. + */ + public static function add_donation_filter( $post_type ) { + if ( 'product' !== $post_type ) { + return; + } + $selected = isset( $_GET['newspack_donation_filter'] ) ? sanitize_text_field( $_GET['newspack_donation_filter'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ?> + + is_main_query() || 'product' !== $query->get( 'post_type' ) ) { + return; + } + if ( empty( $_GET['newspack_donation_filter'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended + return; + } + $filter = sanitize_text_field( $_GET['newspack_donation_filter'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended + + $meta_query = $query->get( 'meta_query' ) ?: []; + + if ( 'donation' === $filter ) { + $meta_query[] = [ + 'key' => '_newspack_is_donation', + 'value' => '1', + ]; + } elseif ( 'non-donation' === $filter ) { + $meta_query[] = [ + 'relation' => 'OR', + [ + 'key' => '_newspack_is_donation', + 'compare' => 'NOT EXISTS', + ], + [ + 'key' => '_newspack_is_donation', + 'value' => '1', + 'compare' => '!=', + ], + ]; + } + + $query->set( 'meta_query', $meta_query ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } } WooCommerce_Products::init(); diff --git a/includes/wizards/audience/class-audience-donations.php b/includes/wizards/audience/class-audience-donations.php index bc2028f50c..57601ae2c3 100644 --- a/includes/wizards/audience/class-audience-donations.php +++ b/includes/wizards/audience/class-audience-donations.php @@ -134,50 +134,6 @@ public function register_api_endpoints() { ] ); - // Get flagged donation products. - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/donation-products', - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_get_donation_products' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - ] - ); - - // Flag or unflag a product as donation. - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/donation-products/(?P\d+)', - [ - 'methods' => \WP_REST_Server::EDITABLE, - 'callback' => [ $this, 'api_toggle_donation_product' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'is_donation' => [ - 'required' => true, - 'sanitize_callback' => 'Newspack\newspack_string_to_bool', - ], - ], - ] - ); - - // Search WooCommerce products for the product picker. - register_rest_route( - NEWSPACK_API_NAMESPACE, - '/wizard/' . $this->slug . '/products-search', - [ - 'methods' => \WP_REST_Server::READABLE, - 'callback' => [ $this, 'api_search_products' ], - 'permission_callback' => [ $this, 'api_permissions_check' ], - 'args' => [ - 'search' => [ - 'required' => true, - 'sanitize_callback' => 'sanitize_text_field', - ], - ], - ] - ); } /** @@ -219,8 +175,7 @@ public function fetch_all_data() { 'donation_data' => Donations::get_donation_settings(), 'donation_page' => Donations::get_donation_page_info(), 'product_validation' => $this->validate_donation_products(), - 'donation_products' => $this->get_donation_products_data(), - ]; + ]; if ( 'wc' === $platform ) { $plugin_status = true; $managed_plugins = Plugin_Manager::get_managed_plugins(); @@ -302,95 +257,6 @@ public function api_reset_donation_email( $request ) { ); } - /** - * Get all products flagged as donations. - * - * @return \WP_REST_Response - */ - public function api_get_donation_products() { - return rest_ensure_response( $this->get_donation_products_data() ); - } - - /** - * Flag or unflag a product as donation. - * - * @param \WP_REST_Request $request Request object. - * @return \WP_REST_Response|\WP_Error - */ - public function api_toggle_donation_product( $request ) { - $product_id = absint( $request->get_param( 'id' ) ); - $is_donation = $request->get_param( 'is_donation' ); - $product = \wc_get_product( $product_id ); - - if ( ! $product ) { - return new \WP_Error( 'invalid_product', __( 'Product not found.', 'newspack-plugin' ), [ 'status' => 404 ] ); - } - - if ( $is_donation ) { - $product->update_meta_data( '_newspack_is_donation', '1' ); - } else { - $product->delete_meta_data( '_newspack_is_donation' ); - } - $product->save(); - - return $this->api_get_donation_products(); - } - - /** - * Search WooCommerce products by name. - * - * @param \WP_REST_Request $request Request object. - * @return \WP_REST_Response - */ - public function api_search_products( $request ) { - $search = $request->get_param( 'search' ); - $results = []; - - $products = \wc_get_products( - [ - 'limit' => 10, - 'status' => 'publish', - 's' => $search, - ] - ); - - foreach ( $products as $product ) { - $results[] = [ - 'id' => $product->get_id(), - 'name' => $product->get_name(), - 'type' => $product->get_type(), - 'is_donation' => get_post_meta( $product->get_id(), '_newspack_is_donation', true ) === '1', - ]; - } - - return rest_ensure_response( $results ); - } - - /** - * Get flagged donation products data for the wizard. - * - * @return array - */ - private function get_donation_products_data() { - if ( ! function_exists( 'wc_get_product' ) ) { - return []; - } - $flagged_ids = Donations::get_flagged_donation_product_ids(); - $products = []; - foreach ( $flagged_ids as $product_id ) { - $product = \wc_get_product( $product_id ); - if ( $product ) { - $products[] = [ - 'id' => $product->get_id(), - 'name' => $product->get_name(), - 'type' => $product->get_type(), - 'edit_link' => get_edit_post_link( $product->get_id(), 'raw' ), - ]; - } - } - return $products; - } - /** * Check whether WooCommerce is installed and active. * diff --git a/src/wizards/audience/views/donations/configuration/index.tsx b/src/wizards/audience/views/donations/configuration/index.tsx index adafbe61cd..479d4e1a97 100644 --- a/src/wizards/audience/views/donations/configuration/index.tsx +++ b/src/wizards/audience/views/donations/configuration/index.tsx @@ -2,8 +2,6 @@ * WordPress dependencies. */ import { __, sprintf } from '@wordpress/i18n'; -import { useState } from '@wordpress/element'; -import apiFetch from '@wordpress/api-fetch'; import { useDispatch } from '@wordpress/data'; import { ToggleControl, ExternalLink } from '@wordpress/components'; @@ -11,7 +9,7 @@ import { ToggleControl, ExternalLink } from '@wordpress/components'; * Internal dependencies. */ import MoneyInput from '../../../components/money-input'; -import { ActionCard, Button, Card, Grid, Notice, SectionHeader, SelectControl, TextControl } from '../../../../../../packages/components/src'; +import { Button, Card, Grid, Notice, SectionHeader, SelectControl, TextControl } from '../../../../../../packages/components/src'; import { useWizardData } from '../../../../../../packages/components/src/wizard/store/utils'; import { WIZARD_STORE_NAMESPACE } from '../../../../../../packages/components/src/wizard/store'; import WizardsTab from '../../../../wizards-tab'; @@ -245,100 +243,8 @@ export const DonationAmounts = () => { ); }; -type DonationProduct = { - id: number; - name: string; - type: string; - edit_link: string; - is_donation?: boolean; -}; - -function DonationProducts( { products }: { products: DonationProduct[] } ) { - const [ searchQuery, setSearchQuery ] = useState( '' ); - const [ searchResults, setSearchResults ] = useState< DonationProduct[] >( [] ); - const [ isSearching, setIsSearching ] = useState( false ); - const { wizardApiFetch } = useDispatch( WIZARD_STORE_NAMESPACE ); - - const handleSearch = async () => { - if ( ! searchQuery.trim() ) { - return; - } - setIsSearching( true ); - try { - const results = await apiFetch< DonationProduct[] >( { - path: `/newspack/v1/wizard/${ AUDIENCE_DONATIONS_WIZARD_SLUG }/products-search?search=${ encodeURIComponent( searchQuery ) }`, - } ); - setSearchResults( results.filter( ( r: DonationProduct ) => ! r.is_donation ) ); - } catch { - setSearchResults( [] ); - } finally { - setIsSearching( false ); - } - }; - - const toggleProduct = async ( productId: number, isDonation: boolean ) => { - await wizardApiFetch( { - path: `/newspack/v1/wizard/${ AUDIENCE_DONATIONS_WIZARD_SLUG }/donation-products/${ productId }`, - method: 'POST', - data: { is_donation: isDonation }, - updateEncoder: ( data: { donation_products: DonationProduct[] } ) => ( { - wizardData: { donation_products: data }, - } ), - } ); - setSearchResults( prev => prev.filter( r => r.id !== productId ) ); - setSearchQuery( '' ); - }; - - return ( - - - { products.map( ( product: DonationProduct ) => ( - toggleProduct( product.id, false ) } - > - - - ) ) } -
- e.key === 'Enter' && handleSearch() } - /> - -
- { searchResults.map( ( product: DonationProduct ) => ( - toggleProduct( product.id, true ) } - /> - ) ) } -
- ); -} - const Donation = () => { const wizardData = useWizardData( AUDIENCE_DONATIONS_WIZARD_SLUG ) as AudienceDonationsWizardData; - const { donation_products } = wizardData; const { saveWizardSettings } = useDispatch( WIZARD_STORE_NAMESPACE ); const onSaveDonationSettings = () => saveWizardSettings( { @@ -420,7 +326,6 @@ const Donation = () => { { __( 'Save Settings', 'newspack-plugin' ) } - ); }; diff --git a/src/wizards/types/hooks.d.ts b/src/wizards/types/hooks.d.ts index 68a2fe9ba6..2d16fcc183 100644 --- a/src/wizards/types/hooks.d.ts +++ b/src/wizards/types/hooks.d.ts @@ -108,10 +108,4 @@ type AudienceDonationsWizardData = { product_validation: { [key: string]: ProductValidation; }; - donation_products?: Array<{ - id: number; - name: string; - type: string; - edit_link: string; - }>; }; From bb0401b7635ef8b50e66c9f856c6e7d20656db26 Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 16:15:29 -0300 Subject: [PATCH 09/16] fix(donations): insert donation column before date column Co-Authored-By: Claude Opus 4.6 (1M context) --- .../plugins/woocommerce/class-woocommerce-products.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/includes/plugins/woocommerce/class-woocommerce-products.php b/includes/plugins/woocommerce/class-woocommerce-products.php index 19a1aa7010..0c1e717615 100644 --- a/includes/plugins/woocommerce/class-woocommerce-products.php +++ b/includes/plugins/woocommerce/class-woocommerce-products.php @@ -381,8 +381,14 @@ public static function require_order_processing( $needs_proccessing, $product ) * @return array Modified columns. */ public static function add_donation_column( $columns ) { - $columns['newspack_donation'] = __( 'Donation', 'newspack-plugin' ); - return $columns; + $new_columns = []; + foreach ( $columns as $key => $label ) { + if ( 'date' === $key ) { + $new_columns['newspack_donation'] = __( 'Donation', 'newspack-plugin' ); + } + $new_columns[ $key ] = $label; + } + return $new_columns; } /** From 8ccdd2ad88c8851b2f279a8063869942abf16152 Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 16:35:18 -0300 Subject: [PATCH 10/16] fix(donations): fix column insertion priority and position Use priority 20 to run after WooCommerce defines its columns, and use array_slice to insert before the last 2 columns (featured + date). Co-Authored-By: Claude Opus 4.6 (1M context) --- .../woocommerce/class-woocommerce-products.php | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/includes/plugins/woocommerce/class-woocommerce-products.php b/includes/plugins/woocommerce/class-woocommerce-products.php index 0c1e717615..a4f4b6e070 100644 --- a/includes/plugins/woocommerce/class-woocommerce-products.php +++ b/includes/plugins/woocommerce/class-woocommerce-products.php @@ -31,7 +31,7 @@ public static function init() { \add_filter( 'woocommerce_order_item_needs_processing', [ __CLASS__, 'require_order_processing' ], 10, 2 ); \add_filter( 'woocommerce_product_description_heading', '__return_false', 10, 2 ); \add_filter( 'woocommerce_product_additional_information_heading', '__return_false', 10, 2 ); - \add_filter( 'manage_product_posts_columns', [ __CLASS__, 'add_donation_column' ] ); + \add_filter( 'manage_product_posts_columns', [ __CLASS__, 'add_donation_column' ], 20 ); \add_action( 'manage_product_posts_custom_column', [ __CLASS__, 'render_donation_column' ], 10, 2 ); \add_action( 'restrict_manage_posts', [ __CLASS__, 'add_donation_filter' ] ); \add_action( 'pre_get_posts', [ __CLASS__, 'filter_by_donation' ] ); @@ -381,14 +381,14 @@ public static function require_order_processing( $needs_proccessing, $product ) * @return array Modified columns. */ public static function add_donation_column( $columns ) { - $new_columns = []; - foreach ( $columns as $key => $label ) { - if ( 'date' === $key ) { - $new_columns['newspack_donation'] = __( 'Donation', 'newspack-plugin' ); - } - $new_columns[ $key ] = $label; + if ( empty( $columns ) ) { + return $columns; } - return $new_columns; + return array_merge( + array_slice( $columns, 0, -2, true ), + [ 'newspack_donation' => __( 'Donation', 'newspack-plugin' ) ], + array_slice( $columns, -2, null, true ) + ); } /** From 6bd3cfc5021152facd26192bea0536e0eb93b0da Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 16:36:41 -0300 Subject: [PATCH 11/16] refactor(donations): remove donation column, keep only filter Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-woocommerce-products.php | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/includes/plugins/woocommerce/class-woocommerce-products.php b/includes/plugins/woocommerce/class-woocommerce-products.php index a4f4b6e070..aa71ee1293 100644 --- a/includes/plugins/woocommerce/class-woocommerce-products.php +++ b/includes/plugins/woocommerce/class-woocommerce-products.php @@ -31,8 +31,6 @@ public static function init() { \add_filter( 'woocommerce_order_item_needs_processing', [ __CLASS__, 'require_order_processing' ], 10, 2 ); \add_filter( 'woocommerce_product_description_heading', '__return_false', 10, 2 ); \add_filter( 'woocommerce_product_additional_information_heading', '__return_false', 10, 2 ); - \add_filter( 'manage_product_posts_columns', [ __CLASS__, 'add_donation_column' ], 20 ); - \add_action( 'manage_product_posts_custom_column', [ __CLASS__, 'render_donation_column' ], 10, 2 ); \add_action( 'restrict_manage_posts', [ __CLASS__, 'add_donation_filter' ] ); \add_action( 'pre_get_posts', [ __CLASS__, 'filter_by_donation' ] ); } @@ -374,38 +372,6 @@ public static function require_order_processing( $needs_proccessing, $product ) return self::get_custom_option_value( $product, 'newspack_autocomplete_orders' ) ? false : $needs_proccessing; } - /** - * Add "Donation" column to the products list table. - * - * @param array $columns Existing columns. - * @return array Modified columns. - */ - public static function add_donation_column( $columns ) { - if ( empty( $columns ) ) { - return $columns; - } - return array_merge( - array_slice( $columns, 0, -2, true ), - [ 'newspack_donation' => __( 'Donation', 'newspack-plugin' ) ], - array_slice( $columns, -2, null, true ) - ); - } - - /** - * Render the "Donation" column content. - * - * @param string $column Column name. - * @param int $post_id Post ID. - */ - public static function render_donation_column( $column, $post_id ) { - if ( 'newspack_donation' !== $column ) { - return; - } - if ( Donations::is_donation_product( $post_id ) ) { - echo ''; - } - } - /** * Add a dropdown filter for donation products on the products list screen. * From 5cbf0b5e6df2efa5b121839d0c2281a6e97792b2 Mon Sep 17 00:00:00 2001 From: leogermani Date: Mon, 6 Apr 2026 16:43:23 -0300 Subject: [PATCH 12/16] refactor(donations): remove products list filter Co-Authored-By: Claude Opus 4.6 (1M context) --- .../class-woocommerce-products.php | 59 ------------------- 1 file changed, 59 deletions(-) diff --git a/includes/plugins/woocommerce/class-woocommerce-products.php b/includes/plugins/woocommerce/class-woocommerce-products.php index aa71ee1293..f5ff59a9d2 100644 --- a/includes/plugins/woocommerce/class-woocommerce-products.php +++ b/includes/plugins/woocommerce/class-woocommerce-products.php @@ -31,8 +31,6 @@ public static function init() { \add_filter( 'woocommerce_order_item_needs_processing', [ __CLASS__, 'require_order_processing' ], 10, 2 ); \add_filter( 'woocommerce_product_description_heading', '__return_false', 10, 2 ); \add_filter( 'woocommerce_product_additional_information_heading', '__return_false', 10, 2 ); - \add_action( 'restrict_manage_posts', [ __CLASS__, 'add_donation_filter' ] ); - \add_action( 'pre_get_posts', [ __CLASS__, 'filter_by_donation' ] ); } /** @@ -372,63 +370,6 @@ public static function require_order_processing( $needs_proccessing, $product ) return self::get_custom_option_value( $product, 'newspack_autocomplete_orders' ) ? false : $needs_proccessing; } - /** - * Add a dropdown filter for donation products on the products list screen. - * - * @param string $post_type The current post type. - */ - public static function add_donation_filter( $post_type ) { - if ( 'product' !== $post_type ) { - return; - } - $selected = isset( $_GET['newspack_donation_filter'] ) ? sanitize_text_field( $_GET['newspack_donation_filter'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended - ?> - - is_main_query() || 'product' !== $query->get( 'post_type' ) ) { - return; - } - if ( empty( $_GET['newspack_donation_filter'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - return; - } - $filter = sanitize_text_field( $_GET['newspack_donation_filter'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended - - $meta_query = $query->get( 'meta_query' ) ?: []; - - if ( 'donation' === $filter ) { - $meta_query[] = [ - 'key' => '_newspack_is_donation', - 'value' => '1', - ]; - } elseif ( 'non-donation' === $filter ) { - $meta_query[] = [ - 'relation' => 'OR', - [ - 'key' => '_newspack_is_donation', - 'compare' => 'NOT EXISTS', - ], - [ - 'key' => '_newspack_is_donation', - 'value' => '1', - 'compare' => '!=', - ], - ]; - } - - $query->set( 'meta_query', $meta_query ); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - } } WooCommerce_Products::init(); From 8da219ceed62ad003cc3011bfa63452d9772d214 Mon Sep 17 00:00:00 2001 From: leogermani Date: Tue, 7 Apr 2026 15:27:53 -0300 Subject: [PATCH 13/16] fix: use woo functions to handle value --- includes/class-donations.php | 9 ++++++--- .../woocommerce/class-woocommerce-products.php | 5 +++-- .../wizards/audience/class-audience-donations.php | 3 +-- tests/unit-tests/donations.php | 11 ++++++----- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/includes/class-donations.php b/includes/class-donations.php index 6111f907a1..fa391a0548 100644 --- a/includes/class-donations.php +++ b/includes/class-donations.php @@ -279,7 +279,7 @@ public static function get_donation_product_child_products_ids() { */ public static function is_donation_product( $product_id ) { // Check the meta flag first (fast path). - if ( get_post_meta( $product_id, '_newspack_is_donation', true ) === '1' ) { + if ( function_exists( 'wc_bool_to_string' ) && get_post_meta( $product_id, WooCommerce_Products::DONATION_FLAG_META_KEY, true ) === wc_bool_to_string( true ) ) { return true; } @@ -298,6 +298,9 @@ public static function is_donation_product( $product_id ) { * @return int[] Array of product IDs. */ public static function get_flagged_donation_product_ids() { + if ( ! function_exists( 'wc_bool_to_string' ) ) { + return []; + } $flagged_products = get_posts( [ 'post_type' => 'product', @@ -306,8 +309,8 @@ public static function get_flagged_donation_product_ids() { 'fields' => 'ids', 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query [ - 'key' => '_newspack_is_donation', - 'value' => '1', + 'key' => WooCommerce_Products::DONATION_FLAG_META_KEY, + 'value' => wc_bool_to_string( true ), ], ], ] diff --git a/includes/plugins/woocommerce/class-woocommerce-products.php b/includes/plugins/woocommerce/class-woocommerce-products.php index f5ff59a9d2..08bdcad042 100644 --- a/includes/plugins/woocommerce/class-woocommerce-products.php +++ b/includes/plugins/woocommerce/class-woocommerce-products.php @@ -15,6 +15,8 @@ * Connection with WooCommerce's features. */ class WooCommerce_Products { + + const DONATION_FLAG_META_KEY = '_newspack_is_donation'; /** * Initialize. * @@ -71,7 +73,7 @@ public static function get_custom_options() { 'type' => 'boolean', ], 'newspack_is_donation' => [ - 'id' => '_newspack_is_donation', + 'id' => self::DONATION_FLAG_META_KEY, 'wrapper_class' => '', 'label' => __( 'Donation product', 'newspack-plugin' ), 'description' => __( 'Flag this product as a donation. Donation products use donation-specific checkout, reporting, and reader activation behaviors.', 'newspack-plugin' ), @@ -369,7 +371,6 @@ public static function require_order_processing( $needs_proccessing, $product ) } return self::get_custom_option_value( $product, 'newspack_autocomplete_orders' ) ? false : $needs_proccessing; } - } WooCommerce_Products::init(); diff --git a/includes/wizards/audience/class-audience-donations.php b/includes/wizards/audience/class-audience-donations.php index 57601ae2c3..794342283b 100644 --- a/includes/wizards/audience/class-audience-donations.php +++ b/includes/wizards/audience/class-audience-donations.php @@ -133,7 +133,6 @@ public function register_api_endpoints() { 'permission_callback' => [ $this, 'api_permissions_check' ], ] ); - } /** @@ -175,7 +174,7 @@ public function fetch_all_data() { 'donation_data' => Donations::get_donation_settings(), 'donation_page' => Donations::get_donation_page_info(), 'product_validation' => $this->validate_donation_products(), - ]; + ]; if ( 'wc' === $platform ) { $plugin_status = true; $managed_plugins = Plugin_Manager::get_managed_plugins(); diff --git a/tests/unit-tests/donations.php b/tests/unit-tests/donations.php index 995de4daf9..38bdd32d0b 100644 --- a/tests/unit-tests/donations.php +++ b/tests/unit-tests/donations.php @@ -6,6 +6,7 @@ */ use Newspack\Donations; +use Newspack\WooCommerce_Products; require_once __DIR__ . '/../mocks/wc-mocks.php'; @@ -49,7 +50,7 @@ public function test_is_donation_product_unflagged() { */ public function test_is_donation_product_flagged() { $product_id = self::factory()->post->create( [ 'post_type' => 'product' ] ); - update_post_meta( $product_id, '_newspack_is_donation', '1' ); + update_post_meta( $product_id, WooCommerce_Products::DONATION_FLAG_META_KEY, wc_bool_to_string( true ) ); self::assertTrue( Donations::is_donation_product( $product_id ), 'Flagged product should be a donation product.' @@ -63,8 +64,8 @@ public function test_is_donation_product_flagged() { */ public function test_is_donation_product_unflagged_after_removal() { $product_id = self::factory()->post->create( [ 'post_type' => 'product' ] ); - update_post_meta( $product_id, '_newspack_is_donation', '1' ); - delete_post_meta( $product_id, '_newspack_is_donation' ); + update_post_meta( $product_id, WooCommerce_Products::DONATION_FLAG_META_KEY, wc_bool_to_string( true ) ); + delete_post_meta( $product_id, WooCommerce_Products::DONATION_FLAG_META_KEY ); self::assertFalse( Donations::is_donation_product( $product_id ), 'Product should not be a donation product after meta removal.' @@ -80,8 +81,8 @@ public function test_get_flagged_donation_product_ids() { $product_1 = self::factory()->post->create( [ 'post_type' => 'product' ] ); $product_2 = self::factory()->post->create( [ 'post_type' => 'product' ] ); $product_3 = self::factory()->post->create( [ 'post_type' => 'product' ] ); - update_post_meta( $product_1, '_newspack_is_donation', '1' ); - update_post_meta( $product_3, '_newspack_is_donation', '1' ); + update_post_meta( $product_1, WooCommerce_Products::DONATION_FLAG_META_KEY, wc_bool_to_string( true ) ); + update_post_meta( $product_3, WooCommerce_Products::DONATION_FLAG_META_KEY, wc_bool_to_string( true ) ); $flagged_ids = Donations::get_flagged_donation_product_ids(); self::assertContains( $product_1, $flagged_ids, 'Flagged product 1 should be in the list.' ); From 6fd301bf6eb5a41cfb90f30943ee3fece9b3b9fb Mon Sep 17 00:00:00 2001 From: leogermani Date: Tue, 7 Apr 2026 15:49:37 -0300 Subject: [PATCH 14/16] chore: add memoization --- includes/class-donations.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/includes/class-donations.php b/includes/class-donations.php index fa391a0548..673217d274 100644 --- a/includes/class-donations.php +++ b/includes/class-donations.php @@ -298,6 +298,10 @@ public static function is_donation_product( $product_id ) { * @return int[] Array of product IDs. */ public static function get_flagged_donation_product_ids() { + static $memo = null; + if ( null !== $memo ) { + return $memo; + } if ( ! function_exists( 'wc_bool_to_string' ) ) { return []; } @@ -315,7 +319,8 @@ public static function get_flagged_donation_product_ids() { ], ] ); - return array_map( 'intval', $flagged_products ); + $memo = array_map( 'intval', $flagged_products ); + return $memo; } /** From b9f90938dfefb9bd73cc5729c4ce6140afcceaee Mon Sep 17 00:00:00 2001 From: leogermani Date: Tue, 7 Apr 2026 15:54:46 -0300 Subject: [PATCH 15/16] fix: get all donation products --- includes/class-donations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-donations.php b/includes/class-donations.php index 673217d274..1124fcf274 100644 --- a/includes/class-donations.php +++ b/includes/class-donations.php @@ -308,7 +308,7 @@ public static function get_flagged_donation_product_ids() { $flagged_products = get_posts( [ 'post_type' => 'product', - 'post_status' => 'publish', + 'post_status' => 'any', 'posts_per_page' => -1, 'fields' => 'ids', 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query From 92b634435d0131818e45186e2158fce7e1dc13fa Mon Sep 17 00:00:00 2001 From: leogermani Date: Tue, 7 Apr 2026 15:59:28 -0300 Subject: [PATCH 16/16] fix: consider all donations to get product from order --- includes/class-donations.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-donations.php b/includes/class-donations.php index 1124fcf274..8e3a29c245 100644 --- a/includes/class-donations.php +++ b/includes/class-donations.php @@ -346,7 +346,7 @@ public static function is_donation_order( $order ) { * @return int|false The donation product ID or false. */ public static function get_order_donation_product_id( $order_id ) { - $donation_products = self::get_donation_product_child_products_ids(); + $donation_products = array_merge( self::get_donation_product_child_products_ids(), self::get_flagged_donation_product_ids() ); if ( empty( array_filter( $donation_products ) ) ) { return; }