From 2f86a9b30c39d7d4902f8dc58515af21d7eb4bca Mon Sep 17 00:00:00 2001 From: Adam Cassis Date: Mon, 1 Sep 2025 12:16:31 +0200 Subject: [PATCH 1/3] feat(donations): validate products --- .../class-woocommerce-connection.php | 1 + .../class-woocommerce-product-validator.php | 263 ++++++++++++++++++ .../audience/class-audience-donations.php | 72 +++++ .../views/donations/configuration/index.tsx | 36 +++ src/wizards/types/hooks.d.ts | 5 + 5 files changed, 377 insertions(+) create mode 100644 includes/plugins/woocommerce/class-woocommerce-product-validator.php diff --git a/includes/plugins/woocommerce/class-woocommerce-connection.php b/includes/plugins/woocommerce/class-woocommerce-connection.php index 875acd241a..53a628280b 100644 --- a/includes/plugins/woocommerce/class-woocommerce-connection.php +++ b/includes/plugins/woocommerce/class-woocommerce-connection.php @@ -36,6 +36,7 @@ public static function init() { include_once __DIR__ . '/class-woocommerce-cover-fees.php'; include_once __DIR__ . '/class-woocommerce-order-utm.php'; include_once __DIR__ . '/class-woocommerce-products.php'; + include_once __DIR__ . '/class-woocommerce-product-validator.php'; include_once __DIR__ . '/class-woocommerce-duplicate-orders.php'; include_once __DIR__ . '/class-woocommerce-update-payment-notice.php'; include_once __DIR__ . '/class-woocommerce-custom-currency-symbol.php'; diff --git a/includes/plugins/woocommerce/class-woocommerce-product-validator.php b/includes/plugins/woocommerce/class-woocommerce-product-validator.php new file mode 100644 index 0000000000..3c92a38c6c --- /dev/null +++ b/includes/plugins/woocommerce/class-woocommerce-product-validator.php @@ -0,0 +1,263 @@ +get_id(); + $product_name = $product->get_name(); + $issues = []; + $warnings = []; + + // Basic WooCommerce checks. + $wc_checks = self::validate_woocommerce_conditions( $product ); + $issues = array_merge( $issues, $wc_checks['issues'] ); + $warnings = array_merge( $warnings, $wc_checks['warnings'] ); + + // WooCommerce Memberships checks. + if ( function_exists( 'wc_memberships' ) ) { + $membership_checks = self::validate_membership_restrictions( $product, $user_id ); + $issues = array_merge( $issues, $membership_checks['issues'] ); + $warnings = array_merge( $warnings, $membership_checks['warnings'] ); + } + + // Check if product can be added to cart. + $cart_validation = self::validate_cart_addition( $product ); + if ( ! $cart_validation['can_add'] ) { + $issues[] = $cart_validation['reason']; + } + + return [ + 'product_id' => $product_id, + 'product_name' => $product_name, + 'is_valid' => empty( $issues ), + 'is_purchasable' => $product->is_purchasable() && empty( $issues ), + 'issues' => $issues, + 'warnings' => $warnings, + 'checks' => [ + 'is_published' => 'publish' === $product->get_status(), + 'is_in_stock' => $product->is_in_stock(), + 'is_purchasable' => $product->is_purchasable(), + 'has_price' => '' !== $product->get_price(), + 'membership_check' => $membership_checks ?? null, + ], + ]; + } + + /** + * Validate basic WooCommerce conditions. + * + * @param WC_Product $product Product object. + * + * @return array Issues and warnings. + */ + private static function validate_woocommerce_conditions( $product ) { + $issues = []; + $warnings = []; + + // Check if product is published. + if ( 'publish' !== $product->get_status() ) { + $issues[] = sprintf( + /* translators: %s: product status */ + __( 'Product is not published (status: %s).', 'newspack-plugin' ), + $product->get_status() + ); + } + + // Check if product has a price. + $price = $product->get_price(); + if ( '' === $price || null === $price ) { + $issues[] = __( 'Product has no price set.', 'newspack-plugin' ); + } + + // Check stock status. + if ( ! $product->is_in_stock() ) { + $issues[] = __( 'Product is out of stock.', 'newspack-plugin' ); + } + + // Check if managing stock and has sufficient stock. + if ( $product->managing_stock() && ! $product->backorders_allowed() ) { + $stock_quantity = $product->get_stock_quantity(); + if ( $stock_quantity <= 0 ) { + $issues[] = sprintf( + /* translators: %d: stock quantity */ + __( 'Product has no stock available (%d in stock).', 'newspack-plugin' ), + $stock_quantity + ); + } elseif ( $stock_quantity < 5 ) { + $warnings[] = sprintf( + /* translators: %d: stock quantity */ + __( 'Low stock warning: only %d items remaining.', 'newspack-plugin' ), + $stock_quantity + ); + } + } + + // Check if product is purchasable. + if ( ! $product->is_purchasable() ) { + $issues[] = __( 'Product is not purchasable (WooCommerce setting).', 'newspack-plugin' ); + } + + // Check catalog visibility. + if ( 'hidden' === $product->get_catalog_visibility() ) { + $warnings[] = __( 'Product is hidden from catalog.', 'newspack-plugin' ); + } + + return [ + 'issues' => $issues, + 'warnings' => $warnings, + ]; + } + + /** + * Validate WooCommerce Memberships restrictions. + * + * @param WC_Product $product Product object. + * @param int $user_id User ID to check against. + * + * @return array Issues and warnings. + */ + private static function validate_membership_restrictions( $product, $user_id ) { + $issues = []; + $warnings = []; + $product_id = $product->get_id(); + $parent_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product_id; + + // Check viewing restrictions. + if ( function_exists( 'wc_memberships_is_product_viewing_restricted' ) ) { + $is_viewing_restricted = wc_memberships_is_product_viewing_restricted( $product_id ); + if ( ! $is_viewing_restricted && $parent_id !== $product_id ) { + $is_viewing_restricted = wc_memberships_is_product_viewing_restricted( $parent_id ); + } + + if ( $is_viewing_restricted ) { + $can_view = false; + if ( $user_id ) { + $can_view = current_user_can( 'wc_memberships_view_restricted_product', $product_id ) || + current_user_can( 'wc_memberships_view_delayed_product', $product_id ); + } + + if ( ! $can_view ) { + $issues[] = __( 'Product viewing is restricted by membership rules.', 'newspack-plugin' ); + } + } + } + + // Check purchasing restrictions. + if ( function_exists( 'wc_memberships_is_product_purchasing_restricted' ) ) { + $is_purchasing_restricted = wc_memberships_is_product_purchasing_restricted( $product_id ); + if ( ! $is_purchasing_restricted && $parent_id !== $product_id ) { + $is_purchasing_restricted = wc_memberships_is_product_purchasing_restricted( $parent_id ); + } + + if ( $is_purchasing_restricted ) { + $can_purchase = false; + if ( $user_id ) { + $can_purchase = current_user_can( 'wc_memberships_purchase_restricted_product', $product_id ) || + current_user_can( 'wc_memberships_purchase_delayed_product', $product_id ); + } + + if ( ! $can_purchase ) { + $issues[] = __( 'Product purchasing is restricted by membership rules.', 'newspack-plugin' ); + + // Get the membership plans that restrict this product. + if ( function_exists( 'wc_memberships' ) && method_exists( wc_memberships(), 'get_rules_instance' ) ) { + $rules = wc_memberships()->get_rules_instance()->get_product_restriction_rules( $parent_id ); + $plan_names = []; + + foreach ( $rules as $rule ) { + if ( 'purchase' === $rule->get_access_type() ) { + $plan = wc_memberships_get_membership_plan( $rule->get_membership_plan_id() ); + if ( $plan ) { + $plan_names[] = $plan->get_name(); + } + } + } + + if ( ! empty( $plan_names ) ) { + $warnings[] = sprintf( + /* translators: %s: list of membership plan names */ + __( 'Restricted by membership plans: %s', 'newspack-plugin' ), + implode( ', ', array_unique( $plan_names ) ) + ); + } + } + } + } + } + + // Check if product is forced public. + if ( function_exists( 'wc_memberships' ) && method_exists( wc_memberships(), 'get_restrictions_instance' ) ) { + $is_public = wc_memberships()->get_restrictions_instance()->is_product_public( $parent_id ); + if ( $is_public ) { + $warnings[] = __( 'Product is forced public (overrides membership restrictions).', 'newspack-plugin' ); + } + } + + return [ + 'issues' => $issues, + 'warnings' => $warnings, + ]; + } + + /** + * Validate if product can be added to cart. + * + * @param WC_Product $product Product object. + * + * @return array Validation result. + */ + private static function validate_cart_addition( $product ) { + // Simulate adding to cart to check for any restrictions. + $can_add = true; + $reason = ''; + + // Check if product passes validation (this triggers various hooks). + $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product->get_id(), 1 ); + + if ( ! $passed_validation ) { + $can_add = false; + $reason = __( 'Product fails add to cart validation (may be restricted).', 'newspack-plugin' ); + } + + return [ + 'can_add' => $can_add, + 'reason' => $reason, + ]; + } +} diff --git a/includes/wizards/audience/class-audience-donations.php b/includes/wizards/audience/class-audience-donations.php index 9c6efb6f3e..3436973527 100644 --- a/includes/wizards/audience/class-audience-donations.php +++ b/includes/wizards/audience/class-audience-donations.php @@ -258,6 +258,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; @@ -359,4 +360,75 @@ protected function check_required_plugins_installed() { return true; } + + /** + * Validate donation products for purchasability and restrictions. + * + * @return array Validation results for donation products. + */ + protected function validate_donation_products() { + $validation_results = [ + 'has_errors' => false, + 'has_warnings' => false, + 'products' => [], + ]; + + // Check if WooCommerce is active. + if ( ! function_exists( 'wc_get_product' ) || ! class_exists( '\Newspack\WooCommerce_Product_Validator' ) ) { + return $validation_results; + } + + // Get the actual donation product IDs using the internal method. + try { + $reflection = new \ReflectionClass( 'Newspack\Donations' ); + $method = $reflection->getMethod( 'get_donation_product_child_products_ids' ); + $method->setAccessible( true ); + $donation_product_ids = $method->invoke( null ); + } catch ( Exception $e ) { + return $validation_results; + } + + // Check if we have donation products configured. + if ( empty( array_filter( $donation_product_ids ) ) ) { + return $validation_results; + } + + // Validate each donation product. + foreach ( $donation_product_ids as $frequency => $product_id ) { + if ( empty( $product_id ) ) { + continue; + } + + $validation = WooCommerce_Product_Validator::validate_product_purchasability( $product_id ); + + if ( is_wp_error( $validation ) ) { + $validation_results['products'][ $frequency ] = [ + 'product_id' => $product_id, + 'frequency' => $frequency, + 'is_valid' => false, + 'issues' => [ $validation->get_error_message() ], + 'warnings' => [], + ]; + $validation_results['has_errors'] = true; + } else { + $validation_results['products'][ $frequency ] = [ + 'product_id' => $product_id, + 'product_name' => $validation['product_name'], + 'frequency' => $frequency, + 'is_valid' => $validation['is_valid'], + 'issues' => $validation['issues'], + 'warnings' => $validation['warnings'], + ]; + + if ( ! $validation['is_valid'] ) { + $validation_results['has_errors'] = true; + } + if ( ! empty( $validation['warnings'] ) ) { + $validation_results['has_warnings'] = true; + } + } + } + + return $validation_results; + } } diff --git a/src/wizards/audience/views/donations/configuration/index.tsx b/src/wizards/audience/views/donations/configuration/index.tsx index 8ceae6e38c..2a4916e6fd 100644 --- a/src/wizards/audience/views/donations/configuration/index.tsx +++ b/src/wizards/audience/views/donations/configuration/index.tsx @@ -253,8 +253,44 @@ const Donation = () => { auxData: { saveDonationProduct: true }, } ); + // Check for product validation errors. + const hasProductValidationErrors = wizardData.product_validation?.has_errors; + const hasProductValidationWarnings = wizardData.product_validation?.has_warnings; + const productValidation = wizardData.product_validation; + return ( + { /* Display product validation warnings */ } + { productValidation && ( hasProductValidationWarnings || hasProductValidationErrors ) && ( + +
    + { Object.values( productValidation.products ).map( ( product: any ) => { + if ( product.warnings && product.warnings.length > 0 ) { + return ( +
  • + + { product.product_name || + sprintf( + // translators: %d: Product ID. + __( 'Product ID %d', 'newspack-plugin' ), + product.product_id + ) } + { product.frequency && ` (${ product.frequency })` }: + +
      + { product.warnings.map( ( warning: string, index: number ) => ( +
    • { warning }
    • + ) ) } +
    +
  • + ); + } + return null; + } ) } +
+
+ ) } + { wizardData.donation_page && ( <> diff --git a/src/wizards/types/hooks.d.ts b/src/wizards/types/hooks.d.ts index 59586661dc..bd4299e5d2 100644 --- a/src/wizards/types/hooks.d.ts +++ b/src/wizards/types/hooks.d.ts @@ -97,4 +97,9 @@ type AudienceDonationsWizardData = { [ key: string ]: AudienceFieldConfig; }; order_notes_field: AudienceFieldConfig; + product_validation: { + has_errors: boolean; + has_warnings: boolean; + products: {}; + }; }; From 3ed53f9889ec220b274ffb391f30280001fe5a97 Mon Sep 17 00:00:00 2001 From: Adam Cassis Date: Mon, 1 Sep 2025 12:42:41 +0200 Subject: [PATCH 2/3] refactor: simplify --- includes/class-donations.php | 2 +- .../class-woocommerce-product-validator.php | 192 ++++-------------- .../audience/class-audience-donations.php | 37 +--- .../views/donations/configuration/index.tsx | 17 +- src/wizards/types/hooks.d.ts | 6 +- 5 files changed, 58 insertions(+), 196 deletions(-) diff --git a/includes/class-donations.php b/includes/class-donations.php index 1dd0cbd5cb..27c5894dba 100644 --- a/includes/class-donations.php +++ b/includes/class-donations.php @@ -241,7 +241,7 @@ private static function get_parent_donation_product() { /** * Get the child products of the main donation product. */ - private static function get_donation_product_child_products_ids() { + public static function get_donation_product_child_products_ids() { $child_products_ids = [ 'once' => false, 'month' => false, diff --git a/includes/plugins/woocommerce/class-woocommerce-product-validator.php b/includes/plugins/woocommerce/class-woocommerce-product-validator.php index 3c92a38c6c..dbbfcb06f5 100644 --- a/includes/plugins/woocommerce/class-woocommerce-product-validator.php +++ b/includes/plugins/woocommerce/class-woocommerce-product-validator.php @@ -7,9 +7,6 @@ namespace Newspack; -use WP_Error; -use WC_Product; - defined( 'ABSPATH' ) || exit; /** @@ -20,75 +17,47 @@ class WooCommerce_Product_Validator { * Validate if a product can be purchased by checking various conditions. * This checks standard WooCommerce conditions and also WooCommerce Memberships restrictions. * - * @param int|WC_Product $product Product ID or product object. - * @param int|null $user_id User ID to check against (null for current user). + * @param int|\WC_Product $product Product ID or product object. * - * @return array|WP_Error Array with validation results or WP_Error on failure. + * @return array|\WP_Error Array with validation results or WP_Error on failure. */ - public static function validate_product_purchasability( $product, $user_id = null ) { + public static function validate_product_purchasability( $product ) { + if ( ! function_exists( 'wc_get_product' ) ) { + return new \WP_Error( 'woocommerce_inactive', __( 'WooCommerce inactive.', 'newspack-plugin' ) ); + } // Get the product object. - if ( ! is_a( $product, 'WC_Product' ) ) { + if ( ! is_a( $product, '\WC_Product' ) ) { $product = wc_get_product( $product ); } if ( ! $product ) { - return new WP_Error( 'invalid_product', __( 'Invalid product.', 'newspack-plugin' ) ); - } - - if ( null === $user_id ) { - $user_id = get_current_user_id(); + return new \WP_Error( 'invalid_product', __( 'Invalid product.', 'newspack-plugin' ) ); } - $product_id = $product->get_id(); - $product_name = $product->get_name(); - $issues = []; - $warnings = []; - // Basic WooCommerce checks. - $wc_checks = self::validate_woocommerce_conditions( $product ); - $issues = array_merge( $issues, $wc_checks['issues'] ); - $warnings = array_merge( $warnings, $wc_checks['warnings'] ); + $issues = self::validate_woocommerce_conditions( $product ); // WooCommerce Memberships checks. if ( function_exists( 'wc_memberships' ) ) { - $membership_checks = self::validate_membership_restrictions( $product, $user_id ); - $issues = array_merge( $issues, $membership_checks['issues'] ); - $warnings = array_merge( $warnings, $membership_checks['warnings'] ); - } - - // Check if product can be added to cart. - $cart_validation = self::validate_cart_addition( $product ); - if ( ! $cart_validation['can_add'] ) { - $issues[] = $cart_validation['reason']; + $issues = array_merge( $issues, self::validate_membership_restrictions( $product ) ); } return [ - 'product_id' => $product_id, - 'product_name' => $product_name, - 'is_valid' => empty( $issues ), - 'is_purchasable' => $product->is_purchasable() && empty( $issues ), - 'issues' => $issues, - 'warnings' => $warnings, - 'checks' => [ - 'is_published' => 'publish' === $product->get_status(), - 'is_in_stock' => $product->is_in_stock(), - 'is_purchasable' => $product->is_purchasable(), - 'has_price' => '' !== $product->get_price(), - 'membership_check' => $membership_checks ?? null, - ], + 'product_id' => $product->get_id(), + 'product_name' => $product->get_name(), + 'issues' => $issues, ]; } /** * Validate basic WooCommerce conditions. * - * @param WC_Product $product Product object. + * @param \WC_Product $product Product object. * * @return array Issues and warnings. */ private static function validate_woocommerce_conditions( $product ) { - $issues = []; - $warnings = []; + $issues = []; // Check if product is published. if ( 'publish' !== $product->get_status() ) { @@ -110,51 +79,23 @@ private static function validate_woocommerce_conditions( $product ) { $issues[] = __( 'Product is out of stock.', 'newspack-plugin' ); } - // Check if managing stock and has sufficient stock. - if ( $product->managing_stock() && ! $product->backorders_allowed() ) { - $stock_quantity = $product->get_stock_quantity(); - if ( $stock_quantity <= 0 ) { - $issues[] = sprintf( - /* translators: %d: stock quantity */ - __( 'Product has no stock available (%d in stock).', 'newspack-plugin' ), - $stock_quantity - ); - } elseif ( $stock_quantity < 5 ) { - $warnings[] = sprintf( - /* translators: %d: stock quantity */ - __( 'Low stock warning: only %d items remaining.', 'newspack-plugin' ), - $stock_quantity - ); - } - } - // Check if product is purchasable. if ( ! $product->is_purchasable() ) { $issues[] = __( 'Product is not purchasable (WooCommerce setting).', 'newspack-plugin' ); } - // Check catalog visibility. - if ( 'hidden' === $product->get_catalog_visibility() ) { - $warnings[] = __( 'Product is hidden from catalog.', 'newspack-plugin' ); - } - - return [ - 'issues' => $issues, - 'warnings' => $warnings, - ]; + return $issues; } /** * Validate WooCommerce Memberships restrictions. * - * @param WC_Product $product Product object. - * @param int $user_id User ID to check against. + * @param \WC_Product $product Product object. * - * @return array Issues and warnings. + * @return array Issues. */ - private static function validate_membership_restrictions( $product, $user_id ) { + private static function validate_membership_restrictions( $product ) { $issues = []; - $warnings = []; $product_id = $product->get_id(); $parent_id = $product->is_type( 'variation' ) ? $product->get_parent_id() : $product_id; @@ -166,15 +107,7 @@ private static function validate_membership_restrictions( $product, $user_id ) { } if ( $is_viewing_restricted ) { - $can_view = false; - if ( $user_id ) { - $can_view = current_user_can( 'wc_memberships_view_restricted_product', $product_id ) || - current_user_can( 'wc_memberships_view_delayed_product', $product_id ); - } - - if ( ! $can_view ) { - $issues[] = __( 'Product viewing is restricted by membership rules.', 'newspack-plugin' ); - } + $issues[] = __( 'Product viewing is restricted by membership rules.', 'newspack-plugin' ); } } @@ -186,78 +119,33 @@ private static function validate_membership_restrictions( $product, $user_id ) { } if ( $is_purchasing_restricted ) { - $can_purchase = false; - if ( $user_id ) { - $can_purchase = current_user_can( 'wc_memberships_purchase_restricted_product', $product_id ) || - current_user_can( 'wc_memberships_purchase_delayed_product', $product_id ); - } - - if ( ! $can_purchase ) { - $issues[] = __( 'Product purchasing is restricted by membership rules.', 'newspack-plugin' ); - - // Get the membership plans that restrict this product. - if ( function_exists( 'wc_memberships' ) && method_exists( wc_memberships(), 'get_rules_instance' ) ) { - $rules = wc_memberships()->get_rules_instance()->get_product_restriction_rules( $parent_id ); - $plan_names = []; - - foreach ( $rules as $rule ) { - if ( 'purchase' === $rule->get_access_type() ) { - $plan = wc_memberships_get_membership_plan( $rule->get_membership_plan_id() ); - if ( $plan ) { - $plan_names[] = $plan->get_name(); - } + $issues[] = __( 'Product purchasing is restricted by membership rules.', 'newspack-plugin' ); + + // Get the membership plans that restrict this product. + if ( function_exists( 'wc_memberships' ) && method_exists( wc_memberships(), 'get_rules_instance' ) ) { + $rules = wc_memberships()->get_rules_instance()->get_product_restriction_rules( $parent_id ); + $plan_names = []; + + foreach ( $rules as $rule ) { + if ( 'purchase' === $rule->get_access_type() ) { + $plan = wc_memberships_get_membership_plan( $rule->get_membership_plan_id() ); + if ( $plan ) { + $plan_names[] = $plan->get_name(); } } + } - if ( ! empty( $plan_names ) ) { - $warnings[] = sprintf( - /* translators: %s: list of membership plan names */ - __( 'Restricted by membership plans: %s', 'newspack-plugin' ), - implode( ', ', array_unique( $plan_names ) ) - ); - } + if ( ! empty( $plan_names ) ) { + $issues[] = sprintf( + /* translators: %s: list of membership plan names */ + __( 'Restricted by membership plans: %s', 'newspack-plugin' ), + implode( ', ', array_unique( $plan_names ) ) + ); } } } } - // Check if product is forced public. - if ( function_exists( 'wc_memberships' ) && method_exists( wc_memberships(), 'get_restrictions_instance' ) ) { - $is_public = wc_memberships()->get_restrictions_instance()->is_product_public( $parent_id ); - if ( $is_public ) { - $warnings[] = __( 'Product is forced public (overrides membership restrictions).', 'newspack-plugin' ); - } - } - - return [ - 'issues' => $issues, - 'warnings' => $warnings, - ]; - } - - /** - * Validate if product can be added to cart. - * - * @param WC_Product $product Product object. - * - * @return array Validation result. - */ - private static function validate_cart_addition( $product ) { - // Simulate adding to cart to check for any restrictions. - $can_add = true; - $reason = ''; - - // Check if product passes validation (this triggers various hooks). - $passed_validation = apply_filters( 'woocommerce_add_to_cart_validation', true, $product->get_id(), 1 ); - - if ( ! $passed_validation ) { - $can_add = false; - $reason = __( 'Product fails add to cart validation (may be restricted).', 'newspack-plugin' ); - } - - return [ - 'can_add' => $can_add, - 'reason' => $reason, - ]; + return $issues; } } diff --git a/includes/wizards/audience/class-audience-donations.php b/includes/wizards/audience/class-audience-donations.php index 3436973527..9ee9f78c6e 100644 --- a/includes/wizards/audience/class-audience-donations.php +++ b/includes/wizards/audience/class-audience-donations.php @@ -367,27 +367,14 @@ protected function check_required_plugins_installed() { * @return array Validation results for donation products. */ protected function validate_donation_products() { - $validation_results = [ - 'has_errors' => false, - 'has_warnings' => false, - 'products' => [], - ]; + $validation_results = []; // Check if WooCommerce is active. - if ( ! function_exists( 'wc_get_product' ) || ! class_exists( '\Newspack\WooCommerce_Product_Validator' ) ) { + if ( ! class_exists( '\Newspack\WooCommerce_Product_Validator' ) ) { return $validation_results; } - // Get the actual donation product IDs using the internal method. - try { - $reflection = new \ReflectionClass( 'Newspack\Donations' ); - $method = $reflection->getMethod( 'get_donation_product_child_products_ids' ); - $method->setAccessible( true ); - $donation_product_ids = $method->invoke( null ); - } catch ( Exception $e ) { - return $validation_results; - } - + $donation_product_ids = Donations::get_donation_product_child_products_ids( null ); // Check if we have donation products configured. if ( empty( array_filter( $donation_product_ids ) ) ) { return $validation_results; @@ -400,32 +387,20 @@ protected function validate_donation_products() { } $validation = WooCommerce_Product_Validator::validate_product_purchasability( $product_id ); - + if ( is_wp_error( $validation ) ) { - $validation_results['products'][ $frequency ] = [ + $validation_results[ $frequency ] = [ 'product_id' => $product_id, 'frequency' => $frequency, - 'is_valid' => false, 'issues' => [ $validation->get_error_message() ], - 'warnings' => [], ]; - $validation_results['has_errors'] = true; } else { - $validation_results['products'][ $frequency ] = [ + $validation_results[ $frequency ] = [ 'product_id' => $product_id, 'product_name' => $validation['product_name'], 'frequency' => $frequency, - 'is_valid' => $validation['is_valid'], 'issues' => $validation['issues'], - 'warnings' => $validation['warnings'], ]; - - if ( ! $validation['is_valid'] ) { - $validation_results['has_errors'] = true; - } - if ( ! empty( $validation['warnings'] ) ) { - $validation_results['has_warnings'] = true; - } } } diff --git a/src/wizards/audience/views/donations/configuration/index.tsx b/src/wizards/audience/views/donations/configuration/index.tsx index 2a4916e6fd..e9a4aa640d 100644 --- a/src/wizards/audience/views/donations/configuration/index.tsx +++ b/src/wizards/audience/views/donations/configuration/index.tsx @@ -254,18 +254,17 @@ const Donation = () => { } ); // Check for product validation errors. - const hasProductValidationErrors = wizardData.product_validation?.has_errors; - const hasProductValidationWarnings = wizardData.product_validation?.has_warnings; - const productValidation = wizardData.product_validation; + const validationResults = Object.values( wizardData.product_validation || {} ); + const hasInvalidProducts = validationResults.every( product => product.issues.length !== 0 ); return ( - { /* Display product validation warnings */ } - { productValidation && ( hasProductValidationWarnings || hasProductValidationErrors ) && ( + { /* Display product validation issues */ } + { hasInvalidProducts ? (
    - { Object.values( productValidation.products ).map( ( product: any ) => { - if ( product.warnings && product.warnings.length > 0 ) { + { validationResults.map( ( product: any ) => { + if ( product.issues && product.issues.length > 0 ) { return (
  • @@ -278,7 +277,7 @@ const Donation = () => { { product.frequency && ` (${ product.frequency })` }:
      - { product.warnings.map( ( warning: string, index: number ) => ( + { product.issues.map( ( warning: string, index: number ) => (
    • { warning }
    • ) ) }
    @@ -289,7 +288,7 @@ const Donation = () => { } ) }
- ) } + ) : null } { wizardData.donation_page && ( <> diff --git a/src/wizards/types/hooks.d.ts b/src/wizards/types/hooks.d.ts index bd4299e5d2..8a3bda8fd2 100644 --- a/src/wizards/types/hooks.d.ts +++ b/src/wizards/types/hooks.d.ts @@ -98,8 +98,8 @@ type AudienceDonationsWizardData = { }; order_notes_field: AudienceFieldConfig; product_validation: { - has_errors: boolean; - has_warnings: boolean; - products: {}; + [ key: string ]: { + issues: any[]; + }; }; }; From a4348d0a64449f6e78a670673031b3bdfee050c0 Mon Sep 17 00:00:00 2001 From: dkoo Date: Fri, 10 Oct 2025 10:26:57 -0600 Subject: [PATCH 3/3] fix: show validation issues in Audience configuration view --- .../class-woocommerce-product-validator.php | 2 +- .../views/donations/configuration/index.tsx | 17 ++++++++++++----- src/wizards/types/hooks.d.ts | 11 ++++++++--- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/includes/plugins/woocommerce/class-woocommerce-product-validator.php b/includes/plugins/woocommerce/class-woocommerce-product-validator.php index dbbfcb06f5..3e6d134a4e 100644 --- a/includes/plugins/woocommerce/class-woocommerce-product-validator.php +++ b/includes/plugins/woocommerce/class-woocommerce-product-validator.php @@ -107,7 +107,7 @@ private static function validate_membership_restrictions( $product ) { } if ( $is_viewing_restricted ) { - $issues[] = __( 'Product viewing is restricted by membership rules.', 'newspack-plugin' ); + $issues[] = __( 'Product is restricted by membership rules.', 'newspack-plugin' ); } } diff --git a/src/wizards/audience/views/donations/configuration/index.tsx b/src/wizards/audience/views/donations/configuration/index.tsx index e9a4aa640d..184d354c34 100644 --- a/src/wizards/audience/views/donations/configuration/index.tsx +++ b/src/wizards/audience/views/donations/configuration/index.tsx @@ -3,7 +3,7 @@ */ import { __, sprintf } from '@wordpress/i18n'; import { useDispatch } from '@wordpress/data'; -import { ToggleControl } from '@wordpress/components'; +import { ToggleControl, ExternalLink } from '@wordpress/components'; /** * Internal dependencies. @@ -255,15 +255,19 @@ const Donation = () => { // Check for product validation errors. const validationResults = Object.values( wizardData.product_validation || {} ); - const hasInvalidProducts = validationResults.every( product => product.issues.length !== 0 ); + const hasInvalidProducts = validationResults.some( product => product.issues.length > 0 ); return ( { /* Display product validation issues */ } { hasInvalidProducts ? ( - +
    - { validationResults.map( ( product: any ) => { + { validationResults.map( ( product: ProductValidation ) => { if ( product.issues && product.issues.length > 0 ) { return (
  • @@ -275,7 +279,10 @@ const Donation = () => { product.product_id ) } { product.frequency && ` (${ product.frequency })` }: - + { ' ' } + + { __( 'edit', 'newspack-plugin' ) } +
      { product.issues.map( ( warning: string, index: number ) => (
    • { warning }
    • diff --git a/src/wizards/types/hooks.d.ts b/src/wizards/types/hooks.d.ts index 8a3bda8fd2..c31e796dbb 100644 --- a/src/wizards/types/hooks.d.ts +++ b/src/wizards/types/hooks.d.ts @@ -70,6 +70,13 @@ type AudienceFieldConfig = { type: string; validate: string[]; }; + +type ProductValidation = { + product_id: number; + product_name?: string; + frequency: string; + issues: string[]; +}; type AudienceDonationsWizardData = { donation_data: | { errors: { [ key: string ]: string[] } } @@ -98,8 +105,6 @@ type AudienceDonationsWizardData = { }; order_notes_field: AudienceFieldConfig; product_validation: { - [ key: string ]: { - issues: any[]; - }; + [ key: string ]: ProductValidation; }; };