From 6d797787aa045f645dcf02853db1f554629173ff Mon Sep 17 00:00:00 2001 From: David Stone Date: Sat, 24 Jan 2026 22:37:39 -0700 Subject: [PATCH 01/32] Move tracking opt-in to general settings and onboarding wizard --- inc/class-settings.php | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/inc/class-settings.php b/inc/class-settings.php index f6e111cf..be022253 100644 --- a/inc/class-settings.php +++ b/inc/class-settings.php @@ -708,6 +708,22 @@ public function default_sections(): void { 120 ); + $this->add_field( + 'general', + 'enable_error_reporting', + [ + 'title' => __('Help Improve Ultimate Multisite', 'ultimate-multisite'), + 'desc' => sprintf( + /* translators: %s is a link to the privacy policy */ + __('Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. Learn more.', 'ultimate-multisite'), + 'https://ultimatemultisite.com/privacy-policy/' + ), + 'type' => 'toggle', + 'default' => 0, + ], + 130 + ); + /* * Login & Registration * This section holds the Login & Registration settings of the Ultimate Multisite Plugin. @@ -1730,21 +1746,6 @@ public function default_sections(): void { ] ); - $this->add_field( - 'other', - 'enable_error_reporting', - [ - 'title' => __('Help Improve Ultimate Multisite', 'ultimate-multisite'), - 'desc' => sprintf( - /* translators: %s is a link to the privacy policy */ - __('Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. Learn more.', 'ultimate-multisite'), - 'https://ultimatemultisite.com/privacy-policy/' - ), - 'type' => 'toggle', - 'default' => 0, - ] - ); - $this->add_field( 'other', 'advanced_header', From c101dc4d70084303e48f5ada25a4c56a203dd16a Mon Sep 17 00:00:00 2001 From: David Stone Date: Sat, 24 Jan 2026 22:44:02 -0700 Subject: [PATCH 02/32] better error logging and handling of user accounts unable to connect to addon store --- inc/class-addon-repository.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/inc/class-addon-repository.php b/inc/class-addon-repository.php index 8bcb0550..ca02fd5d 100644 --- a/inc/class-addon-repository.php +++ b/inc/class-addon-repository.php @@ -1,7 +1,12 @@ get_error_message()), (int) $request->get_error_code()); + wu_log_add('api-calls', $request->get_error_message(), LogLevel::ERROR); + $this->delete_tokens(); } if (200 === absint($code) && 'OK' === $message) { $user = json_decode($body, true); From 0f94b89318317d961c8e6a5abe7f787c8143b22b Mon Sep 17 00:00:00 2001 From: David Stone Date: Sat, 24 Jan 2026 22:46:39 -0700 Subject: [PATCH 03/32] add recommended html attr --- inc/ui/class-login-form-element.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/inc/ui/class-login-form-element.php b/inc/ui/class-login-form-element.php index 1374379b..137b5e29 100644 --- a/inc/ui/class-login-form-element.php +++ b/inc/ui/class-login-form-element.php @@ -771,12 +771,18 @@ public function output($atts, $content = null): void { 'title' => $atts['label_username'], 'placeholder' => $atts['placeholder_username'], 'tooltip' => '', + 'html_attr' => [ + 'autocomplete' => 'username', + ], ], 'pwd' => [ 'type' => 'password', 'title' => $atts['label_password'], 'placeholder' => $atts['placeholder_password'], 'tooltip' => '', + 'html_attr' => [ + 'autocomplete' => 'current-password', + ], ], ]; From a1df46f71f836139cdf32428ef37214b9cacc5f9 Mon Sep 17 00:00:00 2001 From: David Stone Date: Sat, 24 Jan 2026 22:47:01 -0700 Subject: [PATCH 04/32] allow kses to be extended --- inc/functions/helper.php | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/inc/functions/helper.php b/inc/functions/helper.php index ae1729c0..409686d7 100644 --- a/inc/functions/helper.php +++ b/inc/functions/helper.php @@ -105,7 +105,7 @@ function wu_slugify($term) { */ function wu_path($dir): string { - return WP_ULTIMO_PLUGIN_DIR . $dir; // @phpstan-ignore-line + return WP_ULTIMO_PLUGIN_DIR . $dir; } /** @@ -117,7 +117,7 @@ function wu_path($dir): string { */ function wu_url($dir) { - return apply_filters('wp_ultimo_url', WP_ULTIMO_PLUGIN_URL . $dir); // @phpstan-ignore-line + return apply_filters('wp_ultimo_url', WP_ULTIMO_PLUGIN_URL . $dir); } /** @@ -294,8 +294,7 @@ function wu_ignore_errors($func, $log = false) { // phpcs:ignore Generic.CodeAna try { call_user_func($func); - } catch (\Throwable $exception) { - + } catch (\Throwable $exception) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch // Ignore it or log it. } } @@ -496,7 +495,7 @@ function wu_kses_allowed_html(): array { 'template' => true, ]; - return [ + $allowed_tags = [ 'svg' => $svg_attributes + [ 'width' => true, 'height' => true, @@ -613,4 +612,15 @@ function wu_kses_allowed_html(): array { 'height' => true, ], ] + array_merge_recursive($allowed_html, array_fill_keys(array_keys($allowed_html) + ['div', 'span', 'p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'strong', 'em', 'b', 'i', 'ul', 'ol', 'li', 'a', 'img', 'input', 'textarea'], $vue_and_data_attributes)); + + /** + * Filters the allowed HTML tags and attributes. + * + * Allows addons to extend the allowed HTML elements for wp_kses sanitization. + * + * @since 2.5.0 + * + * @param array $allowed_tags The allowed HTML tags and attributes. + */ + return apply_filters('wu_kses_allowed_html', $allowed_tags); } From d0bc5952f455dedc87e18d0211168847b66140d2 Mon Sep 17 00:00:00 2001 From: David Stone Date: Sat, 24 Jan 2026 22:47:34 -0700 Subject: [PATCH 05/32] add settings api --- inc/apis/class-settings-endpoint.php | 439 +++++++++++++++++++++++++++ inc/class-wp-ultimo.php | 5 + 2 files changed, 444 insertions(+) create mode 100644 inc/apis/class-settings-endpoint.php diff --git a/inc/apis/class-settings-endpoint.php b/inc/apis/class-settings-endpoint.php new file mode 100644 index 00000000..2ab6476c --- /dev/null +++ b/inc/apis/class-settings-endpoint.php @@ -0,0 +1,439 @@ +get_namespace(); + + // GET /settings - Retrieve all settings + register_rest_route( + $namespace, + '/settings', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [$this, 'get_settings'], + 'permission_callback' => \Closure::fromCallable([$api, 'check_authorization']), + ] + ); + + // GET /settings/{setting_key} - Retrieve a specific setting + register_rest_route( + $namespace, + '/settings/(?P[a-zA-Z0-9_-]+)', + [ + 'methods' => \WP_REST_Server::READABLE, + 'callback' => [$this, 'get_setting'], + 'permission_callback' => \Closure::fromCallable([$api, 'check_authorization']), + 'args' => [ + 'setting_key' => [ + 'description' => __('The setting key to retrieve.', 'ultimate-multisite'), + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_key', + ], + ], + ] + ); + + // POST /settings - Update multiple settings + register_rest_route( + $namespace, + '/settings', + [ + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => [$this, 'update_settings'], + 'permission_callback' => \Closure::fromCallable([$api, 'check_authorization']), + 'args' => $this->get_update_args(), + ] + ); + + // PUT/PATCH /settings/{setting_key} - Update a specific setting + register_rest_route( + $namespace, + '/settings/(?P[a-zA-Z0-9_-]+)', + [ + 'methods' => \WP_REST_Server::EDITABLE, + 'callback' => [$this, 'update_setting'], + 'permission_callback' => \Closure::fromCallable([$api, 'check_authorization']), + 'args' => [ + 'setting_key' => [ + 'description' => __('The setting key to update.', 'ultimate-multisite'), + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => 'sanitize_key', + ], + 'value' => [ + 'description' => __('The new value for the setting.', 'ultimate-multisite'), + 'required' => true, + ], + ], + ] + ); + } + + /** + * Get all settings. + * + * @since 2.4.0 + * + * @param \WP_REST_Request $request WP Request Object. + * @return \WP_REST_Response + */ + public function get_settings($request) { + + $this->maybe_log_api_call($request); + + $settings = wu_get_all_settings(); + + // Remove sensitive settings from the response + $settings = $this->filter_sensitive_settings($settings); + + return rest_ensure_response( + [ + 'success' => true, + 'settings' => $settings, + ] + ); + } + + /** + * Get a specific setting. + * + * @since 2.4.0 + * + * @param \WP_REST_Request $request WP Request Object. + * @return \WP_REST_Response|\WP_Error + */ + public function get_setting($request) { + + $this->maybe_log_api_call($request); + + $setting_key = $request->get_param('setting_key'); + + // Check if this is a sensitive setting + if ($this->is_sensitive_setting($setting_key)) { + return new \WP_Error( + 'setting_protected', + __('This setting is protected and cannot be retrieved via the API.', 'ultimate-multisite'), + ['status' => 403] + ); + } + + $value = wu_get_setting($setting_key, null); + + if (null === $value) { + // Check if setting exists (even with null/false value) vs doesn't exist + $all_settings = wu_get_all_settings(); + + if (! array_key_exists($setting_key, $all_settings)) { + return new \WP_Error( + 'setting_not_found', + sprintf( + /* translators: %s is the setting key */ + __('Setting "%s" not found.', 'ultimate-multisite'), + $setting_key + ), + ['status' => 404] + ); + } + } + + return rest_ensure_response( + [ + 'success' => true, + 'setting_key' => $setting_key, + 'value' => $value, + ] + ); + } + + /** + * Update multiple settings. + * + * @since 2.4.0 + * + * @param \WP_REST_Request $request WP Request Object. + * @return \WP_REST_Response|\WP_Error + */ + public function update_settings($request) { + + $this->maybe_log_api_call($request); + + $params = $request->get_json_params(); + + if (empty($params) || ! is_array($params)) { + $params = $request->get_body_params(); + } + + $settings_to_update = wu_get_isset($params, 'settings', $params); + + if (empty($settings_to_update) || ! is_array($settings_to_update)) { + return new \WP_Error( + 'invalid_settings', + __('No valid settings provided. Please provide a "settings" object with key-value pairs.', 'ultimate-multisite'), + ['status' => 400] + ); + } + + // Validate and filter out sensitive settings + $errors = []; + $filtered_settings = []; + + foreach ($settings_to_update as $key => $value) { + if ($this->is_sensitive_setting($key)) { + $errors[] = sprintf( + /* translators: %s is the setting key */ + __('Setting "%s" is protected and cannot be modified via the API.', 'ultimate-multisite'), + $key + ); + continue; + } + + // Validate setting key format + $sanitized_key = sanitize_key($key); + if ($sanitized_key !== $key) { + $errors[] = sprintf( + /* translators: %s is the setting key */ + __('Invalid setting key format: "%s".', 'ultimate-multisite'), + $key + ); + continue; + } + + $filtered_settings[ $key ] = $value; + } + + if (empty($filtered_settings)) { + return new \WP_Error( + 'no_valid_settings', + __('No valid settings to update after filtering.', 'ultimate-multisite'), + [ + 'status' => 400, + 'errors' => $errors, + ] + ); + } + + // Save each setting + $updated = []; + $failed = []; + + foreach ($filtered_settings as $key => $value) { + $result = wu_save_setting($key, $value); + + if ($result) { + $updated[] = $key; + } else { + $failed[] = $key; + } + } + + $response_data = [ + 'success' => ! empty($updated), + 'updated' => $updated, + ]; + + if (! empty($failed)) { + $response_data['failed'] = $failed; + } + + if (! empty($errors)) { + $response_data['warnings'] = $errors; + } + + return rest_ensure_response($response_data); + } + + /** + * Update a specific setting. + * + * @since 2.4.0 + * + * @param \WP_REST_Request $request WP Request Object. + * @return \WP_REST_Response|\WP_Error + */ + public function update_setting($request) { + + $this->maybe_log_api_call($request); + + $setting_key = $request->get_param('setting_key'); + + // Check if this is a sensitive setting + if ($this->is_sensitive_setting($setting_key)) { + return new \WP_Error( + 'setting_protected', + __('This setting is protected and cannot be modified via the API.', 'ultimate-multisite'), + ['status' => 403] + ); + } + + $params = $request->get_json_params(); + + if (empty($params)) { + $params = $request->get_body_params(); + } + + $value = wu_get_isset($params, 'value'); + + if (! isset($params['value'])) { + return new \WP_Error( + 'missing_value', + __('The "value" parameter is required.', 'ultimate-multisite'), + ['status' => 400] + ); + } + + $result = wu_save_setting($setting_key, $value); + + if (! $result) { + return new \WP_Error( + 'update_failed', + sprintf( + /* translators: %s is the setting key */ + __('Failed to update setting "%s".', 'ultimate-multisite'), + $setting_key + ), + ['status' => 500] + ); + } + + return rest_ensure_response( + [ + 'success' => true, + 'setting_key' => $setting_key, + 'value' => wu_get_setting($setting_key), + ] + ); + } + + /** + * Get the arguments schema for the update endpoint. + * + * @since 2.4.0 + * @return array + */ + protected function get_update_args(): array { + + return [ + 'settings' => [ + 'description' => __('An object containing setting key-value pairs to update.', 'ultimate-multisite'), + 'type' => 'object', + 'required' => false, + ], + ]; + } + + /** + * Check if a setting is sensitive and should not be exposed via API. + * + * @since 2.4.0 + * + * @param string $setting_key The setting key to check. + * @return bool + */ + protected function is_sensitive_setting(string $setting_key): bool { + + $sensitive_settings = [ + 'api_key', + 'api_secret', + 'stripe_api_sk_live', + 'stripe_api_sk_test', + 'paypal_client_secret_live', + 'paypal_client_secret_sandbox', + ]; + + /** + * Filter the list of sensitive settings that should not be exposed via API. + * + * @since 2.4.0 + * + * @param array $sensitive_settings List of sensitive setting keys. + * @param string $setting_key The setting key being checked. + */ + $sensitive_settings = apply_filters('wu_api_sensitive_settings', $sensitive_settings, $setting_key); + + return in_array($setting_key, $sensitive_settings, true); + } + + /** + * Filter out sensitive settings from a settings array. + * + * @since 2.4.0 + * + * @param array $settings The settings array to filter. + * @return array + */ + protected function filter_sensitive_settings(array $settings): array { + + foreach ($settings as $key => $value) { + if ($this->is_sensitive_setting($key)) { + unset($settings[ $key ]); + } + } + + return $settings; + } + + /** + * Log API call if logging is enabled. + * + * @since 2.4.0 + * + * @param \WP_REST_Request $request The request object. + * @return void + */ + protected function maybe_log_api_call($request): void { + + if (\WP_Ultimo\API::get_instance()->should_log_api_calls()) { + $payload = [ + 'route' => $request->get_route(), + 'method' => $request->get_method(), + 'url_params' => $request->get_url_params(), + 'body_params' => $request->get_body(), + ]; + + wu_log_add('api-calls', wp_json_encode($payload, JSON_PRETTY_PRINT)); + } + } +} diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php index 1ff69b9a..029ea638 100644 --- a/inc/class-wp-ultimo.php +++ b/inc/class-wp-ultimo.php @@ -540,6 +540,11 @@ protected function load_extra_components(): void { */ \WP_Ultimo\API\Register_Endpoint::get_instance(); + /* + * Loads API settings endpoint. + */ + \WP_Ultimo\API\Settings_Endpoint::get_instance(); + /* * Loads Documentation */ From 58d27e59916b7f5b18f874e7b4522971c4badc7d Mon Sep 17 00:00:00 2001 From: David Stone Date: Sat, 24 Jan 2026 22:50:15 -0700 Subject: [PATCH 06/32] Fix problems with choosing country and state --- readme.txt | 7 +++++++ views/checkout/fields/field-select.php | 6 ++++++ views/checkout/fields/field-text.php | 10 +++++++++- 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/readme.txt b/readme.txt index 1f72355c..86c53f96 100644 --- a/readme.txt +++ b/readme.txt @@ -240,16 +240,23 @@ We recommend running this in a staging environment before updating your producti == Changelog == +Version [2.4.10] - Released on 2026-XX-XX +- New: Settings API +- Fix: Problems with choosing country and state + + Version [2.4.10] - Released on 2026-01-23 - New: Configurable minimum password strength setting with Medium, Strong, and Super Strong options. - New: Super Strong password requirements include 12+ characters, uppercase, lowercase, numbers, and special characters - compatible with WPMU DEV Defender Pro rules. - New: Real-time password requirement hints during checkout with translatable strings. - New: Themed password field styling with visibility toggle and color fallbacks for page builders (Elementor, Kadence, Beaver Builder). - New: Opt-in anonymous usage tracking to help improve the plugin. +- New: Better error page for customers and admins. - New: Rating reminder notice after 30 days of installation. - New: WooCommerce Subscriptions compatibility layer for site duplication. - Improved: JSON response handling for pending site creation in non-FastCGI environments. + Version [2.4.9] - Released on 2025-12-23 - New: Inline login prompt at checkout for existing users - returning customers can sign in directly without leaving the checkout flow. - New: GitHub Actions workflow for PR builds with WordPress Playground testing - enables one-click browser-based testing of pull requests. diff --git a/views/checkout/fields/field-select.php b/views/checkout/fields/field-select.php index 2f0d0ca1..2008bb17 100644 --- a/views/checkout/fields/field-select.php +++ b/views/checkout/fields/field-select.php @@ -26,10 +26,16 @@ ?> + html_attr['v-bind:name']); + ?> print_html_attributes(); ?>> + html_attr['v-bind:name']); + ?> + + name="id); ?>" type="type); ?>" placeholder="placeholder); ?>" value="value); ?>" print_html_attributes(); ?>> suffix) : ?> From 9998c0022f42a3670f1135752392d33d976b7d00 Mon Sep 17 00:00:00 2001 From: David Stone Date: Sat, 24 Jan 2026 23:23:27 -0700 Subject: [PATCH 07/32] Fix login redirect --- inc/checkout/class-checkout-pages.php | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/inc/checkout/class-checkout-pages.php b/inc/checkout/class-checkout-pages.php index 66c1e47c..b316e0ed 100644 --- a/inc/checkout/class-checkout-pages.php +++ b/inc/checkout/class-checkout-pages.php @@ -37,19 +37,28 @@ public function init(): void { add_filter('lostpassword_redirect', [$this, 'filter_lost_password_redirect']); + $use_custom_login = wu_get_setting('enable_custom_login_page', false); + + /* + * Login URL filters need to run on ALL sites (including subsites) + * so that password reset and login links redirect to the main site's + * custom login page instead of wp-login.php (which may be obfuscated). + * + * @see https://github.com/Ultimate-Multisite/ultimate-multisite/issues/291 + */ + if ($use_custom_login) { + add_filter('login_url', [$this, 'filter_login_url'], 10, 3); + + add_filter('lostpassword_url', [$this, 'filter_login_url'], 10, 3); + } + if (is_main_site()) { add_action('before_signup_header', [$this, 'redirect_to_registration_page']); - $use_custom_login = wu_get_setting('enable_custom_login_page', false); - if ( ! $use_custom_login) { return; } - add_filter('login_url', [$this, 'filter_login_url'], 10, 3); - - add_filter('lostpassword_url', [$this, 'filter_login_url'], 10, 3); - add_filter('retrieve_password_message', [$this, 'replace_reset_password_link'], 10, 4); add_filter('network_site_url', [$this, 'maybe_change_wp_login_on_urls']); From a5b944de80bfd14cfc3892ca9b1ad7a5354d4625 Mon Sep 17 00:00:00 2001 From: David Stone Date: Wed, 28 Jan 2026 22:30:22 -0700 Subject: [PATCH 08/32] Add support for pay what you want prices --- .../class-product-edit-admin-page.php | 134 ++++++++-- inc/checkout/class-cart.php | 178 +++++++++++--- inc/checkout/class-checkout.php | 24 +- inc/models/class-product.php | 230 ++++++++++++++++-- 4 files changed, 476 insertions(+), 90 deletions(-) diff --git a/inc/admin-pages/class-product-edit-admin-page.php b/inc/admin-pages/class-product-edit-admin-page.php index dd6b0637..19882ce5 100644 --- a/inc/admin-pages/class-product-edit-admin-page.php +++ b/inc/admin-pages/class-product-edit-admin-page.php @@ -274,30 +274,32 @@ public function register_widgets(): void { 'data-wu-app' => 'product_pricing', 'data-state' => wp_json_encode( [ - 'is_recurring' => $this->get_object()->is_recurring(), - 'pricing_type' => $this->get_object()->get_pricing_type(), - 'has_trial' => $this->get_object()->get_trial_duration() > 0, - 'has_setup_fee' => $this->get_object()->has_setup_fee(), - 'setup_fee' => $this->get_object()->get_setup_fee(), - 'amount' => $this->get_object()->get_formatted_amount(), - 'duration' => $this->get_object()->get_duration(), - 'duration_unit' => $this->get_object()->get_duration_unit(), + 'is_recurring' => $this->get_object()->is_recurring(), + 'pricing_type' => $this->get_object()->get_pricing_type(), + 'has_trial' => $this->get_object()->get_trial_duration() > 0, + 'has_setup_fee' => $this->get_object()->has_setup_fee(), + 'setup_fee' => $this->get_object()->get_setup_fee(), + 'amount' => $this->get_object()->get_formatted_amount(), + 'duration' => $this->get_object()->get_duration(), + 'duration_unit' => $this->get_object()->get_duration_unit(), + 'pwyw_recurring_mode' => $this->get_object()->get_pwyw_recurring_mode() ?: 'customer_choice', ] ), ], 'fields' => [ // Fields for price - 'pricing_type' => [ + 'pricing_type' => [ 'type' => 'select', 'title' => __('Pricing Type', 'ultimate-multisite'), 'placeholder' => __('Select Pricing Type', 'ultimate-multisite'), - 'desc' => __('Products can be free, paid, or require further contact for pricing.', 'ultimate-multisite'), + 'desc' => __('Products can be free, paid, pay what you want, or require further contact for pricing.', 'ultimate-multisite'), 'value' => $this->get_object()->get_pricing_type(), 'tooltip' => '', 'options' => [ - 'paid' => __('Paid', 'ultimate-multisite'), - 'free' => __('Free', 'ultimate-multisite'), - 'contact_us' => __('Contact Us', 'ultimate-multisite'), + 'paid' => __('Paid', 'ultimate-multisite'), + 'free' => __('Free', 'ultimate-multisite'), + 'pay_what_you_want' => __('Pay What You Want', 'ultimate-multisite'), + 'contact_us' => __('Contact Us', 'ultimate-multisite'), ], 'wrapper_html_attr' => [ 'v-cloak' => '1', @@ -306,7 +308,7 @@ public function register_widgets(): void { 'v-model' => 'pricing_type', ], ], - 'contact_us_label' => [ + 'contact_us_label' => [ 'type' => 'text', 'title' => __('Button Label', 'ultimate-multisite'), 'placeholder' => __('E.g. Contact us', 'ultimate-multisite'), @@ -317,7 +319,7 @@ public function register_widgets(): void { 'v-cloak' => '1', ], ], - 'contact_us_link' => [ + 'contact_us_link' => [ 'type' => 'url', 'title' => __('Button Link', 'ultimate-multisite'), 'placeholder' => __('E.g. https://contactus.page.com', 'ultimate-multisite'), @@ -328,7 +330,89 @@ public function register_widgets(): void { 'v-cloak' => '1', ], ], - 'recurring' => [ + 'pwyw_minimum_amount' => [ + 'type' => 'text', + 'title' => __('Minimum Price', 'ultimate-multisite'), + 'placeholder' => wu_format_currency('0'), + 'desc' => __('The minimum amount customers can pay. Leave at 0 for truly "pay what you want".', 'ultimate-multisite'), + 'value' => $this->get_object()->get_pwyw_minimum_amount(), + 'money' => true, + 'wrapper_html_attr' => [ + 'v-show' => "pricing_type == 'pay_what_you_want'", + 'v-cloak' => '1', + ], + ], + 'pwyw_suggested_amount' => [ + 'type' => 'text', + 'title' => __('Suggested Price', 'ultimate-multisite'), + 'placeholder' => wu_format_currency('0'), + 'desc' => __('A suggested price shown as the default value in the price input.', 'ultimate-multisite'), + 'value' => $this->get_object()->get_pwyw_suggested_amount(), + 'money' => true, + 'wrapper_html_attr' => [ + 'v-show' => "pricing_type == 'pay_what_you_want'", + 'v-cloak' => '1', + ], + ], + 'pwyw_recurring_mode' => [ + 'type' => 'select', + 'title' => __('Recurring Mode', 'ultimate-multisite'), + 'desc' => __('Control whether customers can choose between one-time and recurring payments.', 'ultimate-multisite'), + 'value' => $this->get_object()->get_pwyw_recurring_mode(), + 'options' => [ + 'customer_choice' => __('Customer Chooses (One-time or Recurring)', 'ultimate-multisite'), + 'force_recurring' => __('Force Recurring Only', 'ultimate-multisite'), + 'force_one_time' => __('Force One-time Only', 'ultimate-multisite'), + ], + 'wrapper_html_attr' => [ + 'v-show' => "pricing_type == 'pay_what_you_want'", + 'v-cloak' => '1', + ], + 'html_attr' => [ + 'v-model' => 'pwyw_recurring_mode', + ], + ], + 'pwyw_duration_group' => [ + 'type' => 'group', + 'title' => __('Billing Period', 'ultimate-multisite'), + 'desc' => __('The billing period for recurring PWYW payments. Only applies when recurring is enabled.', 'ultimate-multisite'), + 'wrapper_html_attr' => [ + 'v-show' => "pricing_type == 'pay_what_you_want' && pwyw_recurring_mode != 'force_one_time'", + 'v-cloak' => '1', + ], + 'fields' => [ + 'duration' => [ + 'type' => 'number', + 'value' => $this->get_object()->get_duration(), + 'placeholder' => 1, + 'wrapper_classes' => 'wu-w-1/2', + 'min' => 1, + 'html_attr' => [ + 'v-model' => 'duration', + 'v-bind:name' => '""', + 'steps' => 1, + 'v-on:keypress' => 'numeric_only', + ], + ], + 'duration_unit' => [ + 'type' => 'select', + 'value' => $this->get_object()->get_duration_unit(), + 'placeholder' => '', + 'wrapper_classes' => 'wu-w-1/2 wu-ml-2', + 'html_attr' => [ + 'v-model' => 'duration_unit', + 'v-bind:name' => '""', + ], + 'options' => [ + 'day' => __('Day(s)', 'ultimate-multisite'), + 'week' => __('Week(s)', 'ultimate-multisite'), + 'month' => __('Month(s)', 'ultimate-multisite'), + 'year' => __('Year(s)', 'ultimate-multisite'), + ], + ], + ], + ], + 'recurring' => [ 'type' => 'toggle', 'title' => __('Is Recurring?', 'ultimate-multisite'), 'desc' => __('Check this if this product has a recurring charge.', 'ultimate-multisite'), @@ -341,13 +425,13 @@ public function register_widgets(): void { 'v-model' => 'is_recurring', ], ], - 'amount' => [ + 'amount' => [ 'type' => 'hidden', 'html_attr' => [ 'v-model' => 'amount', ], ], - '_amount' => [ + '_amount' => [ 'type' => 'text', 'title' => __('Price', 'ultimate-multisite'), 'placeholder' => __('Price', 'ultimate-multisite'), @@ -363,7 +447,7 @@ public function register_widgets(): void { 'v-model' => 'amount', ], ], - 'amount_group' => [ + 'amount_group' => [ 'type' => 'group', 'title' => __('Price', 'ultimate-multisite'), // translators: placeholder %1$s is the amount, %2$s is the duration (such as 1, 2, 3), and %3$s is the unit (such as month, year, week) @@ -413,7 +497,7 @@ public function register_widgets(): void { ], ], ], - 'billing_cycles' => [ + 'billing_cycles' => [ 'type' => 'number', 'title' => __('Billing Cycles', 'ultimate-multisite'), 'placeholder' => __('E.g. 1', 'ultimate-multisite'), @@ -425,7 +509,7 @@ public function register_widgets(): void { 'v-cloak' => '1', ], ], - 'has_trial' => [ + 'has_trial' => [ 'type' => 'toggle', 'title' => __('Offer Trial', 'ultimate-multisite'), 'desc' => __('Check if you want to add a trial period to this product.', 'ultimate-multisite'), @@ -438,7 +522,7 @@ public function register_widgets(): void { 'v-model' => 'has_trial', ], ], - 'trial_group' => [ + 'trial_group' => [ 'type' => 'group', 'title' => __('Trial', 'ultimate-multisite'), 'tooltip' => '', @@ -467,7 +551,7 @@ public function register_widgets(): void { ], ], ], - 'has_setup_fee' => [ + 'has_setup_fee' => [ 'type' => 'toggle', 'title' => __('Add Setup Fee?', 'ultimate-multisite'), 'desc' => __('Check if you want to add a setup fee.', 'ultimate-multisite'), @@ -480,13 +564,13 @@ public function register_widgets(): void { 'v-model' => 'has_setup_fee', ], ], - 'setup_fee' => [ + 'setup_fee' => [ 'type' => 'hidden', 'html_attr' => [ 'v-model' => 'setup_fee', ], ], - '_setup_fee' => [ + '_setup_fee' => [ 'type' => 'text', 'money' => true, 'title' => __('Setup Fee', 'ultimate-multisite'), diff --git a/inc/checkout/class-cart.php b/inc/checkout/class-cart.php index 053c5471..f9dd93d9 100644 --- a/inc/checkout/class-cart.php +++ b/inc/checkout/class-cart.php @@ -229,6 +229,22 @@ class Cart implements \JsonSerializable { */ protected $extra = []; + /** + * Custom amounts for PWYW products. + * + * @since 2.0.0 + * @var array + */ + protected $custom_amounts = []; + + /** + * Customer's recurring choice for PWYW products. + * + * @since 2.0.0 + * @var array + */ + protected $pwyw_recurring = []; + /** * The cart description. * @@ -260,54 +276,66 @@ public function __construct($args) { /* * Cart Type. */ - 'cart_type' => 'new', + 'cart_type' => 'new', /* * The list of products being bought. */ - 'products' => [], + 'products' => [], /* * The duration parameters * This will dictate which price variations we are going to use. */ - 'duration' => false, - 'duration_unit' => false, + 'duration' => false, + 'duration_unit' => false, /* * The membership ID. * This is passed when we want to handle a upgrade/downgrade/addon. */ - 'membership_id' => false, + 'membership_id' => false, /* * Payment ID. * This is passed when we are trying to recovered a abandoned/pending payment. */ - 'payment_id' => false, + 'payment_id' => false, /* * The discount code to be used. */ - 'discount_code' => false, + 'discount_code' => false, /* * If we should auto-renew or not. */ - 'auto_renew' => true, + 'auto_renew' => true, /* * The country, state, and city of the customer. * Used for taxation purposes. */ - 'country' => '', - 'state' => '', - 'city' => '', + 'country' => '', + 'state' => '', + 'city' => '', /* * Currency */ - 'currency' => '', + 'currency' => '', + + /* + * Custom amounts for PWYW products. + * Keyed by product ID => custom amount. + */ + 'custom_amounts' => [], + + /* + * Customer's recurring choice for PWYW products. + * Keyed by product ID => boolean. + */ + 'pwyw_recurring' => [], ], $args @@ -335,13 +363,15 @@ public function __construct($args) { /* * Set the country, duration and duration_unit. */ - $this->cart_type = $this->attributes->cart_type; - $this->country = $this->attributes->country; - $this->state = $this->attributes->state; - $this->city = $this->attributes->city; - $this->currency = $this->attributes->currency; - $this->duration = $this->attributes->duration; - $this->duration_unit = $this->attributes->duration_unit; + $this->cart_type = $this->attributes->cart_type; + $this->country = $this->attributes->country; + $this->state = $this->attributes->state; + $this->city = $this->attributes->city; + $this->currency = $this->attributes->currency; + $this->duration = $this->attributes->duration; + $this->duration_unit = $this->attributes->duration_unit; + $this->custom_amounts = is_array($this->attributes->custom_amounts) ? $this->attributes->custom_amounts : []; + $this->pwyw_recurring = is_array($this->attributes->pwyw_recurring) ? $this->attributes->pwyw_recurring : []; /* * Loads the current customer, if it exists. @@ -1665,7 +1695,7 @@ public function add_product($product_id_or_slug, $quantity = 1): bool { * If a price variation doesn't exist, we add an error to * the cart. */ - if ($product->is_free() === false) { + if ($product->is_free() === false && ! $product->is_pay_what_you_want()) { if (absint($this->duration) !== $product->get_duration() || $product->get_duration_unit() !== $this->duration_unit) { $price_variation = $product->get_price_variation($this->duration, $this->duration_unit); @@ -1690,15 +1720,73 @@ public function add_product($product_id_or_slug, $quantity = 1): bool { } } + /* + * Handle Pay What You Want pricing. + */ + $is_recurring = $product->is_recurring(); + + if ($product->is_pay_what_you_want()) { + $custom_amount = $this->get_custom_amount_for_product($product->get_id()); + + if (null !== $custom_amount) { + $minimum = $product->get_pwyw_minimum_amount(); + + if ($custom_amount < $minimum) { + $this->errors->add( + 'pwyw-below-minimum', + sprintf( + // translators: %1$s is the product name, %2$s is the minimum amount formatted as currency + __('The amount for %1$s must be at least %2$s.', 'ultimate-multisite'), + $product->get_name(), + wu_format_currency($minimum, $product->get_currency()) + ) + ); + + return false; + } + + $amount = (float) $custom_amount; + } else { + // Use suggested amount as default + $amount = $product->get_pwyw_suggested_amount(); + } + + // Determine recurring status based on product mode and customer choice + $recurring_mode = $product->get_pwyw_recurring_mode(); + + if ('force_recurring' === $recurring_mode) { + $is_recurring = true; + } elseif ('force_one_time' === $recurring_mode) { + $is_recurring = false; + } else { + // customer_choice - check customer's selection, default to false (one-time) + $is_recurring = $this->get_pwyw_recurring_for_product($product->get_id()); + } + } + + // Build line item data + $line_item_base = [ + 'product' => $product, + 'quantity' => $quantity, + 'unit_price' => $amount, + 'duration' => $duration, + 'duration_unit' => $duration_unit, + ]; + + // For PWYW products, explicitly set the recurring status + if ($product->is_pay_what_you_want()) { + $line_item_base['recurring'] = $is_recurring; + + // If recurring, ensure duration values are set + if ($is_recurring && (! $duration || ! $duration_unit)) { + $line_item_base['duration'] = $product->get_duration(); + $line_item_base['duration_unit'] = $product->get_duration_unit(); + } + } + $line_item_data = apply_filters( 'wu_add_product_line_item', - [ - 'product' => $product, - 'quantity' => $quantity, - 'unit_price' => $amount, - 'duration' => $duration, - 'duration_unit' => $duration_unit, - ], + $line_item_base, $product, $duration, $duration_unit, @@ -2324,7 +2412,13 @@ public function apply_discounts_to_item($line_item) { return $line_item; } - if (is_wp_error($this->discount_code->is_valid($line_item->get_product_id()))) { + $is_valid = $this->discount_code->is_valid( + $line_item->get_product_id(), + $line_item->get_duration(), + $line_item->get_duration_unit() + ); + + if (is_wp_error($is_valid)) { return $line_item; } @@ -2836,4 +2930,32 @@ protected function cancel_conflicting_pending_payments(): void { // Pending payment is the same. Nothing to do, Let the form element show the pay pending payment message. } } + + /** + * Get the custom amount for a PWYW product. + * + * @since 2.0.0 + * @param int $product_id The product ID. + * @return float|null The custom amount or null if not set. + */ + public function get_custom_amount_for_product($product_id) { + + $product_id = (int) $product_id; + + return isset($this->custom_amounts[ $product_id ]) ? (float) $this->custom_amounts[ $product_id ] : null; + } + + /** + * Get whether this PWYW product should be recurring. + * + * @since 2.0.0 + * @param int $product_id The product ID. + * @return bool + */ + public function get_pwyw_recurring_for_product($product_id): bool { + + $product_id = (int) $product_id; + + return (bool) wu_get_isset($this->pwyw_recurring, $product_id, false); + } } diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index 3fc8a1a2..7b23ea1a 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -1618,17 +1618,19 @@ public function create_order(): void { apply_filters( 'wu_cart_parameters', [ - 'products' => $this->request_or_session('products', []), - 'discount_code' => $this->request_or_session('discount_code'), - 'country' => $country, - 'state' => $state, - 'city' => $city, - 'membership_id' => $this->request_or_session('membership_id'), - 'payment_id' => $this->request_or_session('payment_id'), - 'auto_renew' => $this->request_or_session('auto_renew', false), - 'duration' => $this->request_or_session('duration'), - 'duration_unit' => $this->request_or_session('duration_unit'), - 'cart_type' => $this->request_or_session('cart_type', 'new'), + 'products' => $this->request_or_session('products', []), + 'discount_code' => $this->request_or_session('discount_code'), + 'country' => $country, + 'state' => $state, + 'city' => $city, + 'membership_id' => $this->request_or_session('membership_id'), + 'payment_id' => $this->request_or_session('payment_id'), + 'auto_renew' => $this->request_or_session('auto_renew', false), + 'duration' => $this->request_or_session('duration'), + 'duration_unit' => $this->request_or_session('duration_unit'), + 'cart_type' => $this->request_or_session('cart_type', 'new'), + 'custom_amounts' => $this->request_or_session('custom_amounts', []), + 'pwyw_recurring' => $this->request_or_session('pwyw_recurring', []), ], $this ) diff --git a/inc/models/class-product.php b/inc/models/class-product.php index 90eec5b1..b813e175 100644 --- a/inc/models/class-product.php +++ b/inc/models/class-product.php @@ -75,6 +75,21 @@ class Product extends Base_Model implements Limitable { */ const META_LEGACY_OPTIONS = 'legacy_options'; + /** + * Meta key for PWYW minimum amount. + */ + const META_PWYW_MINIMUM_AMOUNT = 'wu_pwyw_minimum_amount'; + + /** + * Meta key for PWYW suggested amount. + */ + const META_PWYW_SUGGESTED_AMOUNT = 'wu_pwyw_suggested_amount'; + + /** + * Meta key for PWYW recurring mode. + */ + const META_PWYW_RECURRING_MODE = 'wu_pwyw_recurring_mode'; + /** * The product name. * @@ -319,6 +334,32 @@ class Product extends Base_Model implements Limitable { */ protected $legacy_options; + /** + * PWYW minimum amount. + * + * @since 2.0.0 + * @var float + */ + protected $pwyw_minimum_amount; + + /** + * PWYW suggested amount. + * + * @since 2.0.0 + * @var float + */ + protected $pwyw_suggested_amount; + + /** + * PWYW recurring mode. + * + * Can be 'customer_choice', 'force_recurring', or 'force_one_time'. + * + * @since 2.0.0 + * @var string + */ + protected $pwyw_recurring_mode; + /** * Query Class to the static query methods. * @@ -360,28 +401,31 @@ public function validation_rules() { $currency = wu_get_setting('currency_symbol', 'USD'); return [ - 'featured_image_id' => 'integer', - 'currency' => "required|default:{$currency}", - 'pricing_type' => 'required|in:free,paid,contact_us', - 'trial_duration' => 'integer', - 'trial_duration_unit' => 'in:day,week,month,year|default:month', - 'parent_id' => 'integer', - 'amount' => 'numeric|default:0', - 'recurring' => 'default:0', - 'setup_fee' => 'numeric', - 'duration' => 'numeric|default:1', - 'duration_unit' => 'in:day,week,month,year|default:month', - 'billing_cycles' => 'integer|default:0', - 'active' => 'default:1', - 'price_variations' => "price_variations:{$duration},{$duration_unit}", - 'type' => "required|default:plan|in:{$allowed_types}", - 'slug' => "required|unique:\WP_Ultimo\Models\Product,slug,{$id}|min:2", - 'taxable' => 'boolean|default:0', - 'tax_category' => 'default:', - 'contact_us_label' => 'default:', - 'contact_us_link' => 'url:http,https', - 'customer_role' => 'alpha_dash', - 'network_id' => 'integer|nullable', + 'featured_image_id' => 'integer', + 'currency' => "required|default:{$currency}", + 'pricing_type' => 'required|in:free,paid,contact_us,pay_what_you_want', + 'trial_duration' => 'integer', + 'trial_duration_unit' => 'in:day,week,month,year|default:month', + 'parent_id' => 'integer', + 'amount' => 'numeric|default:0', + 'recurring' => 'default:0', + 'setup_fee' => 'numeric', + 'duration' => 'numeric|default:1', + 'duration_unit' => 'in:day,week,month,year|default:month', + 'billing_cycles' => 'integer|default:0', + 'active' => 'default:1', + 'price_variations' => "price_variations:{$duration},{$duration_unit}", + 'type' => "required|default:plan|in:{$allowed_types}", + 'slug' => "required|unique:\WP_Ultimo\Models\Product,slug,{$id}|min:2", + 'taxable' => 'boolean|default:0', + 'tax_category' => 'default:', + 'contact_us_label' => 'default:', + 'contact_us_link' => 'url:http,https', + 'customer_role' => 'alpha_dash', + 'network_id' => 'integer|nullable', + 'pwyw_minimum_amount' => 'numeric|default:0', + 'pwyw_suggested_amount' => 'numeric|default:0', + 'pwyw_recurring_mode' => 'in:customer_choice,force_recurring,force_one_time|default:customer_choice', ]; } @@ -548,11 +592,11 @@ public function get_pricing_type() { } /** - * Set pricing type can be one of 'free', 'paid', and 'contact_us'. + * Set pricing type can be one of 'free', 'paid', 'contact_us', and 'pay_what_you_want'. * * @since 2.0.0 - * @param string $pricing_type The pricing type can be 'free', 'paid' or 'contact_us'. - * @options free,paid,contact_us + * @param string $pricing_type The pricing type can be 'free', 'paid', 'contact_us', or 'pay_what_you_want'. + * @options free,paid,contact_us,pay_what_you_want * @return void */ public function set_pricing_type($pricing_type): void { @@ -564,6 +608,9 @@ public function set_pricing_type($pricing_type): void { $this->set_recurring(false); } + + // For PWYW, we don't force amount to 0 - it uses suggested_amount as default + // and recurring is determined by pwyw_recurring_mode } /** @@ -667,7 +714,7 @@ public function set_duration_unit($duration_unit): void { /** * Get the product amount. * - * @return int + * @return int|float */ public function get_amount() { @@ -679,6 +726,11 @@ public function get_amount() { return 0; } + // For PWYW, return the suggested amount as the default + if ($this->is_pay_what_you_want()) { + return $this->get_pwyw_suggested_amount(); + } + return $this->amount; } @@ -699,6 +751,17 @@ public function get_formatted_amount($key = 'amount') { return $this->get_contact_us_label() ?: __('Contact Us', 'ultimate-multisite'); } + if ($this->is_pay_what_you_want()) { + $minimum = $this->get_pwyw_minimum_amount(); + + if ($minimum > 0) { + // translators: %s is the minimum amount formatted as currency + return sprintf(__('From %s', 'ultimate-multisite'), wu_format_currency($minimum, $this->get_currency())); + } + + return __('Name Your Price', 'ultimate-multisite'); + } + return wu_format_currency($this->get_amount(), $this->get_currency()); } @@ -1533,6 +1596,121 @@ public function set_legacy_options($legacy_options): void { $this->legacy_options = $this->meta[ self::META_LEGACY_OPTIONS ]; } + /** + * Checks if this product uses Pay What You Want pricing. + * + * @since 2.0.0 + * @return bool + */ + public function is_pay_what_you_want(): bool { + + return 'pay_what_you_want' === $this->get_pricing_type(); + } + + /** + * Checks if this PWYW product allows customer to choose recurring. + * + * @since 2.0.0 + * @return bool + */ + public function allows_customer_recurring_choice(): bool { + + return $this->is_pay_what_you_want() && 'customer_choice' === $this->get_pwyw_recurring_mode(); + } + + /** + * Get the PWYW minimum amount. + * + * @since 2.0.0 + * @return float + */ + public function get_pwyw_minimum_amount(): float { + + if (null === $this->pwyw_minimum_amount) { + $this->pwyw_minimum_amount = (float) $this->get_meta(self::META_PWYW_MINIMUM_AMOUNT, 0); + } + + return (float) $this->pwyw_minimum_amount; + } + + /** + * Set the PWYW minimum amount. + * + * @since 2.0.0 + * @param float $amount The minimum amount customers can pay. + * @return void + */ + public function set_pwyw_minimum_amount($amount): void { + + $this->meta[ self::META_PWYW_MINIMUM_AMOUNT ] = wu_to_float($amount); + + $this->pwyw_minimum_amount = $this->meta[ self::META_PWYW_MINIMUM_AMOUNT ]; + } + + /** + * Get the PWYW suggested amount. + * + * @since 2.0.0 + * @return float + */ + public function get_pwyw_suggested_amount(): float { + + if (null === $this->pwyw_suggested_amount) { + $this->pwyw_suggested_amount = (float) $this->get_meta(self::META_PWYW_SUGGESTED_AMOUNT, 0); + } + + return (float) $this->pwyw_suggested_amount; + } + + /** + * Set the PWYW suggested amount. + * + * @since 2.0.0 + * @param float $amount The suggested price shown as the default value. + * @return void + */ + public function set_pwyw_suggested_amount($amount): void { + + $this->meta[ self::META_PWYW_SUGGESTED_AMOUNT ] = wu_to_float($amount); + + $this->pwyw_suggested_amount = $this->meta[ self::META_PWYW_SUGGESTED_AMOUNT ]; + } + + /** + * Get the PWYW recurring mode. + * + * @since 2.0.0 + * @return string One of 'customer_choice', 'force_recurring', or 'force_one_time'. + */ + public function get_pwyw_recurring_mode(): string { + + if (null === $this->pwyw_recurring_mode) { + $this->pwyw_recurring_mode = $this->get_meta(self::META_PWYW_RECURRING_MODE, 'customer_choice'); + } + + return $this->pwyw_recurring_mode ?: 'customer_choice'; + } + + /** + * Set the PWYW recurring mode. + * + * @since 2.0.0 + * @param string $mode The recurring mode: 'customer_choice', 'force_recurring', or 'force_one_time'. + * @return void + */ + public function set_pwyw_recurring_mode($mode): void { + + $valid_modes = ['customer_choice', 'force_recurring', 'force_one_time']; + + if ( ! in_array($mode, $valid_modes, true)) { + $mode = 'customer_choice'; + } + + $this->meta[ self::META_PWYW_RECURRING_MODE ] = $mode; + + $this->pwyw_recurring_mode = $this->meta[ self::META_PWYW_RECURRING_MODE ]; + } + /** * List of limitations that need to be merged. * From ab41443966924507a14be42ac4050e23d0a5ae2f Mon Sep 17 00:00:00 2001 From: David Stone Date: Wed, 28 Jan 2026 22:30:45 -0700 Subject: [PATCH 09/32] Add support for pay what you want prices --- .../checkout/templates/pricing-table/list.php | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/views/checkout/templates/pricing-table/list.php b/views/checkout/templates/pricing-table/list.php index 450e4486..487cb816 100644 --- a/views/checkout/templates/pricing-table/list.php +++ b/views/checkout/templates/pricing-table/list.php @@ -55,8 +55,42 @@ class="wu-relative wu-block wu-rounded-lg wu-border wu-border-gray-300 wu-bg-whi
get_recurring_description()); ?>
+ is_pay_what_you_want()) : ?> +
+ +
+ + +
+ + allows_customer_recurring_choice()) : ?> + + + get_pwyw_recurring_mode()) : ?> + + +
+ + '; - }); - return; - } - - // Initialize addon - My_Addon::get_instance(); -}); - -/** - * Main addon class - */ -class My_Addon { - - use \WP_Ultimo\Traits\Singleton; - - /** - * Initialize the addon - */ - public function init() { - // Load dependencies - $this->load_dependencies(); - - // Setup hooks - $this->setup_hooks(); - - // Initialize components - $this->init_components(); - } - - /** - * Load required files - */ - private function load_dependencies() { - require_once MY_ADDON_PATH . 'inc/class-my-addon.php'; - } - - /** - * Setup WordPress hooks - */ - private function setup_hooks() { - // Activation/deactivation - register_activation_hook(MY_ADDON_PLUGIN_FILE, [$this, 'activate']); - register_deactivation_hook(MY_ADDON_PLUGIN_FILE, [$this, 'deactivate']); - - // WP Ultimo hooks - add_action('wu_checkout_completed', [$this, 'on_checkout_completed'], 10, 3); - add_filter('wu_checkout_form_fields', [$this, 'add_custom_fields'], 10, 2); - } - - /** - * Initialize addon components - */ - private function init_components() { - // Initialize admin pages, models, etc. - } - - /** - * Plugin activation - */ - public function activate() { - // Create custom tables, set options, etc. - $this->create_custom_table(); - update_option('my_addon_version', MY_ADDON_VERSION); - } - - /** - * Plugin deactivation - */ - public function deactivate() { - // Cleanup if needed - } - - /** - * Handle checkout completion - */ - public function on_checkout_completed($payment, $customer, $membership) { - // Custom logic when checkout completes - $this->send_welcome_email($customer); - $this->setup_customer_account($customer, $membership); - } - - /** - * Add custom checkout fields - */ - public function add_custom_fields($fields, $form) { - $fields['company_size'] = [ - 'type' => 'select', - 'title' => 'Company Size', - 'options' => [ - 'small' => '1-10 employees', - 'medium' => '11-100 employees', - 'large' => '100+ employees' - ], - 'required' => false - ]; - - return $fields; - } -} -``` - -### Custom Model Example - -```php -table_name = "{$wpdb->prefix}my_addon_leads"; - } - - /** - * Get the company name - */ - public function get_company() { - return $this->get_meta('company'); - } - - /** - * Set the company name - */ - public function set_company($company) { - return $this->add_meta('company', $company); - } - - /** - * Convert lead to customer - */ - public function convert_to_customer($user_data = []) { - // Create WordPress user - $user_id = wp_create_user( - $user_data['username'] ?? $this->get_email(), - $user_data['password'] ?? wp_generate_password(), - $this->get_email() - ); - - if (is_wp_error($user_id)) { - return $user_id; - } - - // Create WP Ultimo customer - $customer = wu_create_customer([ - 'user_id' => $user_id, - 'email_verification' => 'verified', - 'type' => 'customer' - ]); - - if (is_wp_error($customer)) { - return $customer; - } - - // Copy lead data to customer - $customer->add_meta('company', $this->get_company()); - $customer->add_meta('lead_source', $this->get_source()); - - // Mark lead as converted - $this->set_status('converted'); - $this->add_meta('converted_customer_id', $customer->get_id()); - $this->save(); - - return $customer; - } -} -``` - -### Admin Page Integration - -```php -id, [ - 'title' => __('Leads', 'my-addon'), - 'menu_title' => __('Leads', 'my-addon'), - 'capability' => 'wu_read_leads', - 'position' => $this->position, - 'parent' => 'wp-ultimo', - 'callback' => [$this, 'render'] - ]); - } - - /** - * Render the page - */ - public function render() { - // Get leads data - $leads = My_Addon\Models\Lead::query([ - 'number' => 20, - 'paged' => absint($_GET['paged'] ?? 1) - ]); - - // Render template - wu_get_template('admin/leads-list', [ - 'leads' => $leads, - 'page_title' => __('Manage Leads', 'my-addon') - ]); - } -} -``` - ---- - -## Code Examples - -### Advanced Integration Examples - -#### 1. Multi-Gateway Payment Processing - -```php -/** - * Process payment with fallback gateways - */ -class Smart_Payment_Processor { - - private $gateway_priority = ['stripe', 'paypal', 'manual']; - - public function process_payment_with_fallback($payment, $cart) { - foreach ($this->gateway_priority as $gateway_id) { - $gateway = wu_get_gateway($gateway_id); - - if (!$gateway || !$gateway->is_available()) { - continue; - } - - $result = $gateway->process_single_payment($payment, $cart); - - if (!is_wp_error($result)) { - // Payment successful - do_action('wu_payment_processed_successfully', $payment, $gateway_id); - return $result; - } - - // Log failed attempt - wu_log_add('payment-processing', sprintf( - 'Gateway %s failed for payment %d: %s', - $gateway_id, - $payment->get_id(), - $result->get_error_message() - )); - } - - // All gateways failed - do_action('wu_payment_processing_failed', $payment); - return new WP_Error('all_gateways_failed', 'All payment methods failed'); - } -} -``` - -#### 2. Dynamic Pricing Engine - -```php -/** - * Advanced pricing rules engine - */ -class Dynamic_Pricing_Engine { - - public function __construct() { - add_filter('wu_cart_total', [$this, 'apply_dynamic_pricing'], 20, 2); - add_filter('wu_product_price', [$this, 'modify_product_price'], 10, 3); - } - - public function apply_dynamic_pricing($total, $cart) { - $customer = $cart->get_customer(); - $rules = $this->get_pricing_rules(); - - foreach ($rules as $rule) { - if ($this->rule_applies($rule, $cart, $customer)) { - $total = $this->apply_rule($rule, $total, $cart); - } - } - - return $total; - } - - private function get_pricing_rules() { - return [ - [ - 'type' => 'volume_discount', - 'condition' => ['total_greater_than' => 100], - 'discount' => 0.1 // 10% - ], - [ - 'type' => 'loyalty_discount', - 'condition' => ['customer_tenure_months' => 12], - 'discount' => 0.15 // 15% - ], - [ - 'type' => 'seasonal_promo', - 'condition' => ['date_range' => ['2024-11-01', '2024-12-31']], - 'discount' => 0.2 // 20% - ] - ]; - } - - private function rule_applies($rule, $cart, $customer) { - foreach ($rule['condition'] as $condition => $value) { - switch ($condition) { - case 'total_greater_than': - if ($cart->get_total() <= $value) return false; - break; - - case 'customer_tenure_months': - if (!$customer || $customer->get_months_active() < $value) return false; - break; - - case 'date_range': - $now = current_time('Y-m-d'); - if ($now < $value[0] || $now > $value[1]) return false; - break; - } - } - - return true; - } - - private function apply_rule($rule, $total, $cart) { - $discount_amount = $total * $rule['discount']; - - // Log the discount application - wu_log_add('pricing', sprintf( - 'Applied %s rule: %.2f discount on total %.2f', - $rule['type'], - $discount_amount, - $total - )); - - return $total - $discount_amount; - } -} - -new Dynamic_Pricing_Engine(); -``` - -#### 3. Advanced Site Provisioning - -```php -/** - * Custom site provisioning with external services - */ -class Advanced_Site_Provisioner { - - public function __construct() { - add_action('wu_site_published', [$this, 'provision_site'], 10, 2); - add_action('wu_membership_status_to_expired', [$this, 'suspend_site_services']); - } - - public function provision_site($site, $membership) { - $plan = $membership->get_plan(); - - // Configure based on plan features - switch_to_blog($site->get_id()); - - // Install plugins based on plan - $this->install_plan_plugins($plan); - - // Configure SSL - if ($plan->has_feature('ssl')) { - $this->setup_ssl($site); - } - - // Setup CDN - if ($plan->has_feature('cdn')) { - $this->configure_cdn($site); - } - - // Configure backups - if ($plan->has_feature('backups')) { - $this->setup_automated_backups($site, $plan->get_backup_frequency()); - } - - // Setup monitoring - $this->setup_site_monitoring($site, $membership->get_customer()); - - restore_current_blog(); - - // Send completion notification - $this->send_provisioning_complete_email($site, $membership); - } - - private function install_plan_plugins($plan) { - $plugins = $plan->get_included_plugins(); - - foreach ($plugins as $plugin_slug) { - if ($this->plugin_exists($plugin_slug)) { - activate_plugin($plugin_slug); - - // Configure plugin if needed - $this->configure_plugin($plugin_slug, $plan); - } - } - } - - private function setup_ssl($site) { - $domain = $site->get_domain(); - - // API call to SSL provider - $ssl_service = new SSL_Provider_API(); - $result = $ssl_service->request_certificate($domain); - - if ($result->success) { - $site->add_meta('ssl_certificate_id', $result->certificate_id); - $site->add_meta('ssl_status', 'active'); - } - } - - private function configure_cdn($site) { - $cdn_service = new CDN_Provider_API(); - - $zone = $cdn_service->create_zone([ - 'name' => $site->get_domain(), - 'type' => 'full' - ]); - - if ($zone->success) { - $site->add_meta('cdn_zone_id', $zone->id); - - // Update DNS records - $this->update_cdn_dns($site, $zone); - } - } - - private function setup_automated_backups($site, $frequency) { - $backup_service = new Backup_Provider_API(); - - $schedule = $backup_service->create_schedule([ - 'site_id' => $site->get_id(), - 'frequency' => $frequency, - 'retention' => 30 // days - ]); - - $site->add_meta('backup_schedule_id', $schedule->id); - } - - private function setup_site_monitoring($site, $customer) { - $monitoring_service = new Monitoring_API(); - - $monitor = $monitoring_service->create_monitor([ - 'url' => $site->get_domain(), - 'customer_email' => $customer->get_email(), - 'check_interval' => 300 // 5 minutes - ]); - - $site->add_meta('monitoring_id', $monitor->id); - } -} - -new Advanced_Site_Provisioner(); -``` - -#### 4. Custom Limitations System - -```php -/** - * Advanced limitations with usage tracking - */ -class Advanced_Limitations { - - public function __construct() { - add_filter('wu_limitation_plugins_allowed', [$this, 'check_plugin_limit'], 10, 3); - add_filter('wu_limitation_storage_allowed', [$this, 'check_storage_limit'], 10, 3); - add_action('activated_plugin', [$this, 'track_plugin_activation'], 10, 2); - } - - public function check_plugin_limit($allowed, $site_id, $membership) { - $plan = $membership->get_plan(); - $max_plugins = $plan->get_limit('max_plugins', 10); - - // Count active plugins - switch_to_blog($site_id); - $active_plugins = count(get_option('active_plugins', [])); - restore_current_blog(); - - if ($active_plugins >= $max_plugins) { - // Send warning notification - $this->send_limit_warning($membership->get_customer(), 'plugins', $max_plugins); - return false; - } - - return true; - } - - public function check_storage_limit($allowed, $site_id, $membership) { - $plan = $membership->get_plan(); - $max_storage = $plan->get_limit('max_storage_mb', 1000); // MB - - $current_usage = $this->get_site_storage_usage($site_id); - - if ($current_usage >= $max_storage) { - // Log limit reached - wu_log_add('limitations', sprintf( - 'Site %d reached storage limit: %dMB/%dMB', - $site_id, - $current_usage, - $max_storage - )); - - return false; - } - - // Warn at 80% usage - if ($current_usage >= ($max_storage * 0.8)) { - $this->send_storage_warning($membership->get_customer(), $current_usage, $max_storage); - } - - return true; - } - - private function get_site_storage_usage($site_id) { - // Calculate actual storage usage - $upload_dir = wp_upload_dir(); - $size = $this->get_directory_size($upload_dir['basedir']); - - // Convert to MB - return round($size / 1024 / 1024, 2); - } - - private function get_directory_size($directory) { - $size = 0; - $files = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($directory) - ); - - foreach ($files as $file) { - if ($file->isFile()) { - $size += $file->getSize(); - } - } - - return $size; - } - - private function send_limit_warning($customer, $limit_type, $limit_value) { - wu_mail_customer($customer, 'limit_warning', [ - 'limit_type' => $limit_type, - 'limit_value' => $limit_value, - 'upgrade_url' => wu_get_current_url('upgrade') - ]); - } -} - -new Advanced_Limitations(); -``` - -### Testing Your Integration - -#### Unit Test Example - -```php -customer = wu_create_customer([ - 'user_id' => $this->factory->user->create(), - 'type' => 'customer' - ]); - - // Create test membership - $this->membership = wu_create_membership([ - 'customer_id' => $this->customer->get_id(), - 'plan_id' => $this->create_test_plan() - ]); - } - - public function test_custom_field_saves_correctly() { - $checkout = new WP_Ultimo\Checkout\Checkout(); - - // Simulate form submission - $_POST['company_size'] = 'medium'; - - $result = $checkout->process_step_data([ - 'company_size' => 'medium' - ]); - - $this->assertTrue($result); - - // Verify data was saved - $saved_value = $this->customer->get_meta('company_size'); - $this->assertEquals('medium', $saved_value); - } - - public function test_pricing_rule_applies() { - $engine = new Dynamic_Pricing_Engine(); - - $cart = new WP_Ultimo\Checkout\Cart([ - 'customer_id' => $this->customer->get_id(), - 'products' => ['test-plan'] - ]); - - $cart->set_total(150); // Above $100 threshold - - $new_total = $engine->apply_dynamic_pricing(150, $cart); - - // Should have 10% discount - $this->assertEquals(135, $new_total); - } - - private function create_test_plan() { - return wu_create_product([ - 'name' => 'Test Plan', - 'type' => 'plan', - 'price' => 50, - 'duration' => 1, - 'duration_unit' => 'month' - ])->get_id(); - } -} -``` - -This comprehensive documentation provides developers with all the tools and knowledge needed to integrate with, extend, and build upon WP Ultimo's powerful platform. The extensive API, hook system, and examples enable creation of sophisticated SaaS solutions and custom integrations. +See [Official Ultimate Multisite Documentation Site](https://ultimatemultisite.com/docs/developer) diff --git a/assets/js/checkout.min.js b/assets/js/checkout.min.js index 7f42f880..d870b366 100644 --- a/assets/js/checkout.min.js +++ b/assets/js/checkout.min.js @@ -1 +1 @@ -((n,r,s)=>{window.history.replaceState&&window.history.replaceState(null,null,wu_checkout.baseurl),r.addAction("wu_on_create_order","nextpress/wp-ultimo",function(e,t){void 0!==t.order.extra.template_id&&t.order.extra.template_id&&(e.template_id=t.order.extra.template_id)}),r.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(e){void 0!==window.wu_auto_submittable_field&&window.wu_auto_submittable_field&&e.$watch(window.wu_auto_submittable_field,function(){jQuery(this.$el).submit()},{deep:!0})}),r.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(t){wu_create_cookie("wu_template",""),wu_create_cookie("wu_selected_products",""),wu_listen_to_cookie_change("wu_template",function(e){e&&(t.template_id=e)})}),n(document).on("click",'[href|="#wu-checkout-add"]',function(e){e.preventDefault();var e=n(this),t=e.attr("href").split("#").pop().replace("wu-checkout-add-","");"undefined"!=typeof wu_checkout_form&&-1===wu_checkout_form.products.indexOf(t)&&(wu_checkout_form.add_product(t),e.html(wu_checkout.i18n.added_to_order))}),window.addEventListener("pageshow",function(e){e.persisted&&this.window.wu_checkout_form&&this.window.wu_checkout_form.unblock()}),n(document).ready(function(){var e;void 0!==window.Vue&&(Object.defineProperty(Vue.prototype,"$moment",{value:moment}),e={plan:(e=function(e){return isNaN(e)?e:parseInt(e,10)})(wu_checkout.plan),errors:[],order:wu_checkout.order,products:s.map(wu_checkout.products,e),template_id:wu_checkout.template_id,template_category:"",gateway:wu_checkout.gateway,request_billing_address:wu_checkout.request_billing_address,country:wu_checkout.country,state:"",city:"",site_url:wu_checkout.site_url,site_domain:wu_checkout.site_domain,is_subdomain:wu_checkout.is_subdomain,discount_code:wu_checkout.discount_code,toggle_discount_code:0,payment_method:"",username:"",email_address:"",payment_id:wu_checkout.payment_id,membership_id:wu_checkout.membership_id,cart_type:"new",auto_renew:1,duration:wu_checkout.duration,duration_unit:wu_checkout.duration_unit,prevent_submission:!1,valid_password:!0,stored_templates:{},state_list:[],city_list:[],labels:{},show_login_prompt:!1,login_prompt_field:"",checking_user_exists:!1,logging_in:!1,login_error:"",inline_login_password:""},r.applyFilters("wu_before_form_init",e),jQuery("#wu_form").length)&&(Vue.component("colorPicker",{props:["value"],template:'',mounted(){let o=this;n(this.$el).val(this.value).wpColorPicker({width:200,defaultColor:this.value,change(e,t){o.$emit("input",t.color.toString())}})},watch:{value(e){n(this.$el).wpColorPicker("color",e)}},destroyed(){n(this.$el).off().wpColorPicker("destroy")}}),window.wu_checkout_form=new Vue({el:"#wu_form",data:e,directives:{init:{bind(e,t,o){o.context[t.arg]=t.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(e,t){t=t.props.template;return e(t?{template:t}:"
nbsp;
")}}},computed:{hooks(){return wp.hooks},unique_products(){return s.uniq(this.products,!1,e=>parseInt(e,10))}},methods:{debounce(e){return s.debounce(e,200,!0)},open_url(e,t="_blank"){window.open(e,t)},get_template(e,t){void 0===t.id&&(t.id="default");var o=e+"/"+t.id;return void 0!==this.stored_templates[o]?this.stored_templates[o]:(o=this.hooks.applyFilters("wu_before_template_fetch",{duration:this.duration,duration_unit:this.duration_unit,products:this.products,...t},this),this.fetch_template(e,o),'
'+wu_checkout.i18n.loading+"
")},reset_templates(n){if(void 0===n)this.stored_templates={};else{let i={};s.forEach(this.stored_templates,function(e,t){var o=t.toString().substr(0,t.toString().indexOf("/"));!1===s.contains(n,o)&&(i[t]=e)}),this.stored_templates=i}},fetch_template(o,i){let n=this;void 0===i.id&&(i.id="default"),this.request("wu_render_field_template",{template:o,attributes:i},function(e){var t=o+"/"+i.id;e.success?Vue.set(n.stored_templates,t,e.data.html):Vue.set(n.stored_templates,t,"
"+e.data[0].message+"
")})},go_back(){this.block(),window.history.back()},set_prevent_submission(e){this.$nextTick(function(){this.prevent_submission=e})},remove_product(t,o){this.products=s.filter(this.products,function(e){return e!=t&&e!=o})},add_plan(e){this.plan&&this.remove_product(this.plan),this.plan=e,this.add_product(e)},add_product(e){this.products.push(e)},has_product(e){return-1',overlayCSS:{backgroundColor:e||"#ffffff",opacity:.6},css:{padding:0,margin:0,width:"50%",fontSize:"14px !important",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"none",backgroundColor:"none",cursor:"wait"}})},unblock(){jQuery(this.$el).wu_unblock()},request(e,t,o,i){var n="wu_validate_form"===e||"wu_create_order"===e||"wu_render_field_template"===e||"wu_check_user_exists"===e||"wu_inline_login"===e?wu_checkout.late_ajaxurl:wu_checkout.ajaxurl;jQuery.ajax({method:"POST",url:n+"&action="+e,data:t,success:o,error:i})},init_password_strength(){let t=this;var e=jQuery("#field-password");e.length&&void 0!==window.WU_PasswordStrength&&(this.password_strength_checker=new window.WU_PasswordStrength({pass1:e,result:jQuery("#pass-strength-result"),minStrength:3,onValidityChange:function(e){t.valid_password=e}}))},check_user_exists_debounced:s.debounce(function(e,t){this.check_user_exists(e,t)},500),check_user_exists(o,e){if(!e||e.length<3)this.show_login_prompt=!1;else{this.checking_user_exists=!0,this.login_error="";let t=this;this.request("wu_check_user_exists",{field_type:o,value:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.checking_user_exists=!1,e.success&&e.data.exists?(t.show_login_prompt=!0,t.login_prompt_field=o):t.show_login_prompt=!1},function(e){t.checking_user_exists=!1,t.show_login_prompt=!1})}},handle_inline_login(e){if(console.log("handle_inline_login called",e),e&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()),this.inline_login_password){this.logging_in=!0,this.login_error="";let t=this;e="email"===this.login_prompt_field?this.email_address||"":this.username||"";this.request("wu_inline_login",{username_or_email:e,password:this.inline_login_password,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.logging_in=!1,e.success&&window.location.reload()},function(e){t.logging_in=!1,e.responseJSON&&e.responseJSON.data&&e.responseJSON.data.message?t.login_error=e.responseJSON.data.message:t.login_error=wu_checkout.i18n.login_failed||"Login failed. Please try again."})}else this.login_error=wu_checkout.i18n.password_required||"Password is required";return!1},dismiss_login_prompt(){this.show_login_prompt=!1,this.inline_login_password="",this.login_error=""},setup_inline_login_handlers(){let _=this;["email","username"].forEach(function(n){var e=document.getElementById("wu-inline-login-password-"+n),t=document.getElementById("wu-inline-login-submit-"+n),r=document.getElementById("wu-dismiss-login-prompt-"+n);let s=document.getElementById("wu-login-error-"+n);var a=document.getElementById("wu-inline-login-prompt-"+n);if(e&&t){let o=t.cloneNode(!0),i=(t.parentNode.replaceChild(o,t),e.cloneNode(!0));function u(e){o.disabled=!1,o.textContent=wu_checkout.i18n.sign_in||"Sign in",e.data&&e.data.message?s.textContent=e.data.message:s.textContent=wu_checkout.i18n.login_failed||"Login failed. Please try again.",s.style.display="block"}function d(e){e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation();e=i.value;if(!e)return s.textContent=wu_checkout.i18n.password_required||"Password is required",!(s.style.display="block");o.disabled=!0,o.innerHTML=''+(wu_checkout.i18n.logging_in||"Logging in..."),s.style.display="none";var t="email"===n?_.email_address:_.username;return jQuery.ajax({method:"POST",url:wu_checkout.late_ajaxurl+"&action=wu_inline_login",data:{username_or_email:t,password:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},success:function(e){e.success?window.location.reload():u(e)},error:u}),!1}e.parentNode.replaceChild(i,e),a&&(a.addEventListener("click",function(e){e.stopPropagation()}),a.addEventListener("keydown",function(e){e.stopPropagation()}),a.addEventListener("keyup",function(e){e.stopPropagation()})),o.addEventListener("click",d),i.addEventListener("keydown",function(e){"Enter"===e.key&&d(e)}),r&&r.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),_.show_login_prompt=!1,_.inline_login_password="",i.value=""})}})}},updated(){this.$nextTick(function(){r.doAction("wu_on_form_updated",this),wu_initialize_tooltip(),this.setup_inline_login_handlers()})},mounted(){let i=this;jQuery(this.$el).on("click",function(e){n(this).data("submited_via",n(e.target))}),jQuery(this.$el).on("submit",async function(e){e.preventDefault();var t,e=jQuery(this).data("submited_via");e&&((t=jQuery("")).attr("type","hidden"),t.attr("name",e.attr("name")),t.attr("value",e.val()),jQuery(this).append(t)),i.block();try{var o=[];await Promise.all(r.applyFilters("wu_before_form_submitted",o,i,i.gateway))}catch(e){return i.errors=[],i.errors.push({code:"before-submit-error",message:e.message}),i.unblock(),void i.handle_errors(e)}i.validate_form(),r.doAction("wu_on_form_submitted",i,i.gateway)}),this.create_order(),r.doAction("wu_checkout_loaded",this),r.doAction("wu_on_change_gateway",this.gateway,this.gateway),this.init_password_strength(),wu_initialize_tooltip()},watch:{products(e,t){this.on_change_product(e,t)},toggle_discount_code(e){e||(this.discount_code="")},discount_code(e,t){this.on_change_discount_code(e,t)},gateway(e,t){this.on_change_gateway(e,t)},country(e,t){this.state="",this.on_change_country(e,t)},state(e,t){this.city="",this.on_change_state(e,t)},city(e,t){this.on_change_city(e,t)},duration(e,t){this.on_change_duration(e,t)},duration_unit(e,t){this.on_change_duration_unit(e,t)}}}))})})(jQuery,wp.hooks,_); \ No newline at end of file +((r,n,s)=>{window.history.replaceState&&window.history.replaceState(null,null,wu_checkout.baseurl),n.addAction("wu_on_create_order","nextpress/wp-ultimo",function(e,t){void 0!==t.order.extra.template_id&&t.order.extra.template_id&&(e.template_id=t.order.extra.template_id)}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(e){void 0!==window.wu_auto_submittable_field&&window.wu_auto_submittable_field&&e.$watch(window.wu_auto_submittable_field,function(){jQuery(this.$el).submit()},{deep:!0})}),n.addAction("wu_checkout_loaded","nextpress/wp-ultimo",function(t){wu_create_cookie("wu_template",""),wu_create_cookie("wu_selected_products",""),wu_listen_to_cookie_change("wu_template",function(e){e&&(t.template_id=e)})}),r(document).on("click",'[href|="#wu-checkout-add"]',function(e){e.preventDefault();var e=r(this),t=e.attr("href").split("#").pop().replace("wu-checkout-add-","");"undefined"!=typeof wu_checkout_form&&-1===wu_checkout_form.products.indexOf(t)&&(wu_checkout_form.add_product(t),e.html(wu_checkout.i18n.added_to_order))}),window.addEventListener("pageshow",function(e){e.persisted&&this.window.wu_checkout_form&&this.window.wu_checkout_form.unblock()}),r(document).ready(function(){var e;void 0!==window.Vue&&(Object.defineProperty(Vue.prototype,"$moment",{value:moment}),e={plan:(e=function(e){return isNaN(e)?e:parseInt(e,10)})(wu_checkout.plan),errors:[],order:wu_checkout.order,products:s.map(wu_checkout.products,e),template_id:wu_checkout.template_id,template_category:"",gateway:wu_checkout.gateway,request_billing_address:wu_checkout.request_billing_address,country:wu_checkout.country,state:"",city:"",site_url:wu_checkout.site_url,site_domain:wu_checkout.site_domain,is_subdomain:wu_checkout.is_subdomain,discount_code:wu_checkout.discount_code,toggle_discount_code:0,payment_method:"",username:"",email_address:"",payment_id:wu_checkout.payment_id,membership_id:wu_checkout.membership_id,cart_type:"new",auto_renew:1,duration:wu_checkout.duration,duration_unit:wu_checkout.duration_unit,prevent_submission:!1,valid_password:!0,stored_templates:{},state_list:[],city_list:[],labels:{},show_login_prompt:!1,login_prompt_field:"",checking_user_exists:!1,logging_in:!1,login_error:"",inline_login_password:"",custom_amounts:wu_checkout.custom_amounts||{},pwyw_recurring:wu_checkout.pwyw_recurring||{}},n.applyFilters("wu_before_form_init",e),jQuery("#wu_form").length)&&(Vue.component("colorPicker",{props:["value"],template:'',mounted(){let o=this;r(this.$el).val(this.value).wpColorPicker({width:200,defaultColor:this.value,change(e,t){o.$emit("input",t.color.toString())}})},watch:{value(e){r(this.$el).wpColorPicker("color",e)}},destroyed(){r(this.$el).off().wpColorPicker("destroy")}}),window.wu_checkout_form=new Vue({el:"#wu_form",data:e,directives:{init:{bind(e,t,o){o.context[t.arg]=t.value}}},components:{dynamic:{functional:!0,template:"#dynamic",props:["template"],render(e,t){t=t.props.template;return e(t?{template:t}:"
nbsp;
")}}},computed:{hooks(){return wp.hooks},unique_products(){return s.uniq(this.products,!1,e=>parseInt(e,10))}},methods:{debounce(e){return s.debounce(e,200,!0)},open_url(e,t="_blank"){window.open(e,t)},get_template(e,t){void 0===t.id&&(t.id="default");var o=e+"/"+t.id;return void 0!==this.stored_templates[o]?this.stored_templates[o]:(o=this.hooks.applyFilters("wu_before_template_fetch",{duration:this.duration,duration_unit:this.duration_unit,products:this.products,...t},this),this.fetch_template(e,o),'
'+wu_checkout.i18n.loading+"
")},reset_templates(r){if(void 0===r)this.stored_templates={};else{let i={};s.forEach(this.stored_templates,function(e,t){var o=t.toString().substr(0,t.toString().indexOf("/"));!1===s.contains(r,o)&&(i[t]=e)}),this.stored_templates=i}},fetch_template(o,i){let r=this;void 0===i.id&&(i.id="default"),this.request("wu_render_field_template",{template:o,attributes:i},function(e){var t=o+"/"+i.id;e.success?Vue.set(r.stored_templates,t,e.data.html):Vue.set(r.stored_templates,t,"
"+e.data[0].message+"
")})},go_back(){this.block(),window.history.back()},set_prevent_submission(e){this.$nextTick(function(){this.prevent_submission=e})},remove_product(t,o){this.products=s.filter(this.products,function(e){return e!=t&&e!=o})},add_plan(e){this.plan&&this.remove_product(this.plan),this.plan=e,this.add_product(e)},add_product(e){this.products.push(e)},has_product(e){return-1',overlayCSS:{backgroundColor:e||"#ffffff",opacity:.6},css:{padding:0,margin:0,width:"50%",fontSize:"14px !important",top:"40%",left:"35%",textAlign:"center",color:"#000",border:"none",backgroundColor:"none",cursor:"wait"}})},unblock(){jQuery(this.$el).wu_unblock()},request(e,t,o,i){var r="wu_validate_form"===e||"wu_create_order"===e||"wu_render_field_template"===e||"wu_check_user_exists"===e||"wu_inline_login"===e?wu_checkout.late_ajaxurl:wu_checkout.ajaxurl;jQuery.ajax({method:"POST",url:r+"&action="+e,data:t,success:o,error:i})},init_password_strength(){let t=this;var e=jQuery("#field-password");e.length&&void 0!==window.WU_PasswordStrength&&(this.password_strength_checker=new window.WU_PasswordStrength({pass1:e,result:jQuery("#pass-strength-result"),minStrength:3,onValidityChange:function(e){t.valid_password=e}}))},check_user_exists_debounced:s.debounce(function(e,t){this.check_user_exists(e,t)},500),check_user_exists(o,e){if(!e||e.length<3)this.show_login_prompt=!1;else{this.checking_user_exists=!0,this.login_error="";let t=this;this.request("wu_check_user_exists",{field_type:o,value:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.checking_user_exists=!1,e.success&&e.data.exists?(t.show_login_prompt=!0,t.login_prompt_field=o):t.show_login_prompt=!1},function(e){t.checking_user_exists=!1,t.show_login_prompt=!1})}},handle_inline_login(e){if(console.log("handle_inline_login called",e),e&&(e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation()),this.inline_login_password){this.logging_in=!0,this.login_error="";let t=this;e="email"===this.login_prompt_field?this.email_address||"":this.username||"";this.request("wu_inline_login",{username_or_email:e,password:this.inline_login_password,_wpnonce:jQuery('[name="_wpnonce"]').val()},function(e){t.logging_in=!1,e.success&&window.location.reload()},function(e){t.logging_in=!1,e.responseJSON&&e.responseJSON.data&&e.responseJSON.data.message?t.login_error=e.responseJSON.data.message:t.login_error=wu_checkout.i18n.login_failed||"Login failed. Please try again."})}else this.login_error=wu_checkout.i18n.password_required||"Password is required";return!1},dismiss_login_prompt(){this.show_login_prompt=!1,this.inline_login_password="",this.login_error=""},setup_inline_login_handlers(){let d=this;["email","username"].forEach(function(r){var e=document.getElementById("wu-inline-login-password-"+r),t=document.getElementById("wu-inline-login-submit-"+r),n=document.getElementById("wu-dismiss-login-prompt-"+r);let s=document.getElementById("wu-login-error-"+r);var a=document.getElementById("wu-inline-login-prompt-"+r);if(e&&t){let o=t.cloneNode(!0),i=(t.parentNode.replaceChild(o,t),e.cloneNode(!0));function u(e){o.disabled=!1,o.textContent=wu_checkout.i18n.sign_in||"Sign in",e.data&&e.data.message?s.textContent=e.data.message:s.textContent=wu_checkout.i18n.login_failed||"Login failed. Please try again.",s.style.display="block"}function _(e){e.preventDefault(),e.stopPropagation(),e.stopImmediatePropagation();e=i.value;if(!e)return s.textContent=wu_checkout.i18n.password_required||"Password is required",!(s.style.display="block");o.disabled=!0,o.innerHTML=''+(wu_checkout.i18n.logging_in||"Logging in..."),s.style.display="none";var t="email"===r?d.email_address:d.username;return jQuery.ajax({method:"POST",url:wu_checkout.late_ajaxurl+"&action=wu_inline_login",data:{username_or_email:t,password:e,_wpnonce:jQuery('[name="_wpnonce"]').val()},success:function(e){e.success?window.location.reload():u(e)},error:u}),!1}e.parentNode.replaceChild(i,e),a&&(a.addEventListener("click",function(e){e.stopPropagation()}),a.addEventListener("keydown",function(e){e.stopPropagation()}),a.addEventListener("keyup",function(e){e.stopPropagation()})),o.addEventListener("click",_),i.addEventListener("keydown",function(e){"Enter"===e.key&&_(e)}),n&&n.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),d.show_login_prompt=!1,d.inline_login_password="",i.value=""})}})}},updated(){this.$nextTick(function(){n.doAction("wu_on_form_updated",this),wu_initialize_tooltip(),this.setup_inline_login_handlers()})},mounted(){let i=this;jQuery(this.$el).on("click",function(e){r(this).data("submited_via",r(e.target))}),jQuery(this.$el).on("submit",async function(e){e.preventDefault();var t,e=jQuery(this).data("submited_via");e&&((t=jQuery("")).attr("type","hidden"),t.attr("name",e.attr("name")),t.attr("value",e.val()),jQuery(this).append(t)),i.block();try{var o=[];await Promise.all(n.applyFilters("wu_before_form_submitted",o,i,i.gateway))}catch(e){return i.errors=[],i.errors.push({code:"before-submit-error",message:e.message}),i.unblock(),void i.handle_errors(e)}i.validate_form(),n.doAction("wu_on_form_submitted",i,i.gateway)}),this.create_order(),n.doAction("wu_checkout_loaded",this),n.doAction("wu_on_change_gateway",this.gateway,this.gateway),this.init_password_strength(),wu_initialize_tooltip()},watch:{products(e,t){this.on_change_product(e,t)},toggle_discount_code(e){e||(this.discount_code="")},discount_code(e,t){this.on_change_discount_code(e,t)},gateway(e,t){this.on_change_gateway(e,t)},country(e,t){this.state="",this.on_change_country(e,t)},state(e,t){this.city="",this.on_change_state(e,t)},city(e,t){this.on_change_city(e,t)},duration(e,t){this.on_change_duration(e,t)},duration_unit(e,t){this.on_change_duration_unit(e,t)}}}))})})(jQuery,wp.hooks,_); \ No newline at end of file diff --git a/inc/berlindb-dynamic-hooks.php b/inc/berlindb-dynamic-hooks.php new file mode 100644 index 00000000..e5b17cb0 --- /dev/null +++ b/inc/berlindb-dynamic-hooks.php @@ -0,0 +1,1674 @@ +gateway_id = $gateway->get_id(); + /* + * Set the order early so that validation_rules() + * can check should_collect_payment() to skip + * billing field requirements for free trials. + */ + $this->order = $cart; + /* * Now we need to validate the form. * @@ -777,13 +784,6 @@ public function process_order() { return $validation; } - /* - * From now on, logic can be delegated to - * special methods, so we need to set - * the order as globally accessible. - */ - $this->order = $cart; - /* * Handles display names, if needed. */ @@ -1868,7 +1868,7 @@ public function get_checkout_variables() { 'city' => $this->request_or_session('billing_city'), 'duration' => $duration, 'duration_unit' => $duration_unit, - 'site_url' => $this->request_or_session('site_url'), + 'site_url' => $this->request_or_session('site_url') === 'autogenerate' ? '' : $this->request_or_session('site_url'), 'site_domain' => $this->request_or_session('site_domain', preg_replace('#^https?://#', '', $site_domain)), 'is_subdomain' => is_subdomain_install(), 'gateway' => wu_request('gateway', $default_gateway), @@ -2069,6 +2069,17 @@ public function get_validation_rules() { } } + /* + * Remove billing field requirements when payment is not needed + * (e.g. free trials with allow_trial_without_payment_method enabled). + */ + if ($this->order && $this->order->should_collect_payment() === false) { + $validation_rules['billing_country'] = ''; + $validation_rules['billing_zip_code'] = ''; + $validation_rules['billing_state'] = ''; + $validation_rules['billing_city'] = ''; + } + /** * Allow plugin developers to filter the validation rules. * diff --git a/inc/checkout/signup-fields/class-signup-field-billing-address.php b/inc/checkout/signup-fields/class-signup-field-billing-address.php index f8c223cc..8f2d9d9a 100644 --- a/inc/checkout/signup-fields/class-signup-field-billing-address.php +++ b/inc/checkout/signup-fields/class-signup-field-billing-address.php @@ -267,7 +267,9 @@ public function to_fields_array($attributes) { } foreach ($fields as &$field) { - $field['wrapper_classes'] = trim(wu_get_isset($field, 'wrapper_classes', '') . ' ' . $attributes['element_classes']); + $field['wrapper_classes'] = trim(wu_get_isset($field, 'wrapper_classes', '') . ' ' . $attributes['element_classes']); + $field['wrapper_html_attr']['v-show'] = 'order.should_collect_payment'; + $field['wrapper_html_attr']['v-cloak'] = 1; } uasort($fields, 'wu_sort_by_order'); diff --git a/inc/documentation/berlindb-dynamic-hooks.php b/inc/documentation/berlindb-dynamic-hooks.php new file mode 100644 index 00000000..94fca5da --- /dev/null +++ b/inc/documentation/berlindb-dynamic-hooks.php @@ -0,0 +1,1901 @@ + $line) { + if (preg_match($pattern, $line)) { + return $i + 1; + } + } + + return 0; +} + +// Pre-compute line numbers for each hook pattern in Query.php. +$hook_lines = [ + 'transition' => find_line_number($query_php_absolute, '/do_action\(\s*\$key_action/'), + 'pre_get' => find_line_number($query_php_absolute, '/pre_get_.*item_name_plural/'), + 'parse_query' => find_line_number($query_php_absolute, '/parse_.*item_name_plural.*_query/'), + 'query_clauses' => find_line_number($query_php_absolute, '/item_name_plural.*_query_clauses/'), + 'search_columns' => find_line_number($query_php_absolute, '/item_name_plural.*_search_columns/'), + 'the_items' => find_line_number($query_php_absolute, '/the_.*item_name_plural/'), + 'filter_item' => find_line_number($query_php_absolute, '/filter_.*item_name.*_item/'), + 'found_query' => find_line_number($query_php_absolute, '/found_.*item_name_plural.*_query/'), +]; + +// ── Discover query classes ────────────────────────────────────────────────── + +$database_dir = $plugin_root . '/inc/database'; +$query_files = glob($database_dir . '/*/class-*-query.php'); + +if (empty($query_files)) { + fwrite(STDERR, "No query class files found in {$database_dir}\n"); + exit(1); +} + +// Skip the engine base class. +$query_files = array_filter($query_files, function ($file) { + return strpos($file, '/engine/') === false; +}); + +// ── Extract metadata from each query class ────────────────────────────────── + +/** + * Parse a single PHP class file and extract protected property values. + * + * @param string $file Absolute path to the PHP file. + * @param array $properties List of property names to extract. + * @return array Associative array of property => value. + */ +function extract_class_properties(string $file, array $properties): array { + + $source = file_get_contents($file); + $result = []; + + foreach ($properties as $prop) { + // Match: protected $prop = 'value'; + // protected $prop = SomeClass::class; + if (preg_match('/protected\s+\$' . preg_quote($prop, '/') . '\s*=\s*(.+?);/s', $source, $m)) { + $raw = trim($m[1]); + + // String literal + if (preg_match("/^['\"](.+?)['\"]$/", $raw, $sm)) { + $result[$prop] = $sm[1]; + } + // Class reference (Foo::class or \Foo\Bar::class) + elseif (preg_match('/^(.+?)::class$/', $raw, $cm)) { + $result[$prop] = trim($cm[1], '\\'); + } else { + $result[$prop] = $raw; + } + } + } + + return $result; +} + +/** + * Parse a schema class and return column definitions that have 'transition' => true. + * + * @param string $file Absolute path to the schema class file. + * @return array List of ['name' => string, 'type' => string] for transition columns. + */ +function extract_schema_columns(string $file): array { + + $source = file_get_contents($file); + + // Extract the $columns array block. + if (!preg_match('/\$columns\s*=\s*\[(.+)\];/s', $source, $m)) { + return []; + } + + $columns_block = $m[1]; + $all_columns = []; + $transition_columns = []; + $seen_names = []; + + // Split into individual column arrays. + // Each column is delimited by [ ... ], + preg_match_all('/\[\s*(.*?)\s*\]/s', $columns_block, $entries); + + foreach ($entries[1] as $entry) { + $col = []; + + // Extract name + if (preg_match("/'name'\s*=>\s*'([^']+)'/", $entry, $nm)) { + $col['name'] = $nm[1]; + } else { + continue; + } + + // Extract type + if (preg_match("/'type'\s*=>\s*'([^']+)'/", $entry, $tm)) { + $col['type'] = $tm[1]; + } else { + $col['type'] = 'mixed'; + } + + // Deduplicate columns (some schemas define the same column twice). + if (isset($seen_names[$col['name']])) { + continue; + } + + $seen_names[$col['name']] = true; + $all_columns[] = $col; + + // Check for transition => true + if (preg_match("/'transition'\s*=>\s*true/", $entry)) { + $transition_columns[] = $col; + } + } + + return [ + 'all' => $all_columns, + 'transition' => $transition_columns, + ]; +} + +/** + * Resolve a schema class reference to a file path. + * + * @param string $class_fqn Fully qualified class name without leading \. + * @param string $plugin_root Plugin root directory. + * @return string|null File path or null. + */ +function resolve_schema_file(string $class_fqn, string $plugin_root): ?string { + + // WP_Ultimo\Database\Memberships\Memberships_Schema + // → inc/database/memberships/class-memberships-schema.php + $parts = explode('\\', $class_fqn); + $class_name = array_pop($parts); // Memberships_Schema + + // Convert class name to filename: Memberships_Schema → class-memberships-schema.php + $filename = 'class-' . str_replace('_', '-', strtolower($class_name)) . '.php'; + + // Build the directory path from namespace parts after WP_Ultimo + // WP_Ultimo\Database\Memberships → inc/database/memberships + $ns_path = ''; + $found_root = false; + + foreach ($parts as $part) { + if ($part === 'WP_Ultimo') { + $found_root = true; + $ns_path = 'inc'; + continue; + } + + if ($found_root) { + $ns_path .= '/' . strtolower(str_replace('_', '-', $part)); + } + } + + $full_path = $plugin_root . '/' . $ns_path . '/' . $filename; + + return file_exists($full_path) ? $full_path : null; +} + +/** + * Map a MySQL type string to a PHPDoc type. + * + * @param string $mysql_type The MySQL column type. + * @return string PHPDoc type. + */ +function mysql_type_to_phpdoc(string $mysql_type): string { + + $type = strtolower($mysql_type); + + if (preg_match('/^(big)?int|smallint|tinyint|mediumint/', $type)) { + return 'int'; + } + + if (preg_match('/^decimal|float|double/', $type)) { + return 'string'; // Stored as string in PHP for precision. + } + + if (preg_match('/^datetime|date|timestamp/', $type)) { + return 'string'; + } + + if (preg_match('/^enum/', $type)) { + return 'string'; + } + + return 'string'; +} + +/** + * Convert an item_name to a human-readable label. + * + * @param string $item_name The item name (e.g. 'discount_code'). + * @return string Human-readable label (e.g. 'discount code'). + */ +function humanize(string $item_name): string { + + return str_replace('_', ' ', $item_name); +} + +// ── Collect all model metadata ────────────────────────────────────────────── + +$models = []; + +foreach ($query_files as $query_file) { + $props = extract_class_properties($query_file, [ + 'item_name', + 'item_name_plural', + 'table_name', + 'table_schema', + ]); + + if (empty($props['item_name']) || empty($props['item_name_plural'])) { + fwrite(STDERR, "Skipping {$query_file}: missing item_name or item_name_plural\n"); + continue; + } + + $schema_file = null; + $columns = ['all' => [], 'transition' => []]; + + if (!empty($props['table_schema'])) { + $schema_file = resolve_schema_file($props['table_schema'], $plugin_root); + + if ($schema_file) { + $columns = extract_schema_columns($schema_file); + } else { + fwrite(STDERR, "Schema file not found for {$props['table_schema']}\n"); + } + } + + $key = $props['item_name'] . '|' . $props['item_name_plural']; + + // Deduplicate: Broadcast_Query, Email_Query, and Post_Query all share + // item_name = 'post'. We only need one set of hooks per unique + // item_name/item_name_plural combination. + if (isset($models[$key])) { + continue; + } + + $models[$key] = [ + 'item_name' => $props['item_name'], + 'item_name_plural' => $props['item_name_plural'], + 'table_name' => $props['table_name'] ?? $props['item_name_plural'], + 'transition_cols' => $columns['transition'], + 'all_cols' => $columns['all'], + 'source_file' => basename($query_file), + ]; +} + +// Sort by item name for consistent output. +ksort($models); + +fwrite(STDERR, sprintf("Found %d unique models with %d total transition columns\n", + count($models), + array_sum(array_map(function ($m) { + return count($m['transition_cols']); + }, $models)) +)); + +// ── Generate the output file ──────────────────────────────────────────────── + +$output_file = __DIR__ . '/berlindb-dynamic-hooks.php'; +$prefix = 'wu'; +$src = $query_php_relative; + +ob_start(); + +echo "merge($product->get_limitations()); diff --git a/inc/limits/class-site-template-limits.php b/inc/limits/class-site-template-limits.php index e62b7a02..8a6fc18a 100644 --- a/inc/limits/class-site-template-limits.php +++ b/inc/limits/class-site-template-limits.php @@ -72,7 +72,7 @@ public function maybe_filter_template_selection_options($attributes) { [$plan, $additional_products] = wu_segregate_products($products); - $products = array_merge([$plan], $additional_products); + $products = array_filter(array_merge([$plan], $additional_products)); foreach ($products as $product) { $limits = $limits->merge($product->get_limitations()); @@ -168,7 +168,7 @@ protected function is_template_available($products, $template_id) { [$plan, $additional_products] = wu_segregate_products($products); - $products = array_merge([$plan], $additional_products); + $products = array_filter(array_merge([$plan], $additional_products)); foreach ($products as $product) { $limits = $limits->merge($product->get_limitations()); diff --git a/inc/stuff.php b/inc/stuff.php index d170a4dc..df971734 100644 --- a/inc/stuff.php +++ b/inc/stuff.php @@ -1,5 +1,5 @@ 'JOgRkxnYU/T77rarLGeUH2VENDdVc1d4ajdFeklhSm5SRFlVaW11M0k1WnFZMithRWpZZlZvMDVxbk8xR0RwejQwbjZMOEJRYmNGb3A4a0Q=', - 1 => 'T/CdTxvsrndQXyrK46n4gnRxYSt0OTFiZEk2V3k2aWptRHNSS0NKMFh0TGd2dko1eDI0OG14OGFwN243c1gvWWkzN3FzdlpxY2kvQlpsR1I=', -); +return array ( + 0 => 'm8OHTALz1+lpJmH7wOZMqm5BZ09XRlpUdE9UK2RMN0dqbzNobk1OMk1XQUQwcDl5U1R5aHkxZlJHTmQrOExyZlV1WktGcTJlWTlGaXhxOWU=', + 1 => '+11XF9OjjG7eYuLSg8bnKzRIMHh4akl3WFNNdzZ0UzkwbFd6SDBCeDhoNFhSVm5OQUhtN01VOWNCN2JZRFhmcVFENWFzZlVsakVxUmFYSFM=', +); \ No newline at end of file diff --git a/inc/ui/class-template-previewer.php b/inc/ui/class-template-previewer.php index bd9bd772..f345903e 100644 --- a/inc/ui/class-template-previewer.php +++ b/inc/ui/class-template-previewer.php @@ -282,7 +282,7 @@ public function template_previewer(): void { [$plan, $additional_products] = wu_segregate_products($products); - $products = array_merge([$plan], $additional_products); + $products = array_filter(array_merge([$plan], $additional_products)); foreach ($products as $product) { $limits = $limits->merge($product->get_limitations()); diff --git a/lang/ultimate-multisite.pot b/lang/ultimate-multisite.pot index 8da2f382..400896fd 100644 --- a/lang/ultimate-multisite.pot +++ b/lang/ultimate-multisite.pot @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-01-23T20:10:39+00:00\n" +"POT-Creation-Date: 2026-02-02T16:16:01+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: ultimate-multisite\n" @@ -123,7 +123,7 @@ msgid "Premium" msgstr "" #: inc/admin-pages/class-addons-admin-page.php:458 -#: inc/admin-pages/class-product-edit-admin-page.php:299 +#: inc/admin-pages/class-product-edit-admin-page.php:300 #: inc/installers/class-default-content-installer.php:264 #: inc/list-tables/class-membership-list-table-widget.php:166 #: inc/list-tables/class-membership-list-table.php:129 @@ -143,8 +143,8 @@ msgid "Growth & Scaling" msgstr "" #: inc/admin-pages/class-addons-admin-page.php:470 -#: inc/class-settings.php:1526 -#: inc/class-settings.php:1527 +#: inc/class-settings.php:1542 +#: inc/class-settings.php:1543 msgid "Integrations" msgstr "" @@ -214,7 +214,7 @@ msgstr "" #: inc/admin-pages/class-broadcast-edit-admin-page.php:96 #: inc/admin-pages/class-checkout-form-edit-admin-page.php:1320 #: inc/admin-pages/class-customer-edit-admin-page.php:811 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:244 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:266 #: inc/admin-pages/class-domain-edit-admin-page.php:315 #: inc/admin-pages/class-event-list-admin-page.php:176 #: inc/admin-pages/class-event-list-admin-page.php:187 @@ -278,8 +278,8 @@ msgstr "" #: inc/admin-pages/class-broadcast-edit-admin-page.php:133 #: inc/admin-pages/class-membership-edit-admin-page.php:472 -#: inc/admin-pages/class-membership-list-admin-page.php:118 -#: inc/admin-pages/class-membership-list-admin-page.php:119 +#: inc/admin-pages/class-membership-list-admin-page.php:135 +#: inc/admin-pages/class-membership-list-admin-page.php:136 #: inc/admin-pages/class-payment-edit-admin-page.php:1042 #: inc/admin-pages/class-payment-edit-admin-page.php:1043 #: inc/admin-pages/class-payment-list-admin-page.php:105 @@ -359,7 +359,7 @@ msgstr "" #: inc/admin-pages/class-broadcast-edit-admin-page.php:370 #: inc/admin-pages/class-edit-admin-page.php:489 #: inc/admin-pages/class-tax-rates-admin-page.php:193 -#: inc/checkout/class-checkout.php:1810 +#: inc/checkout/class-checkout.php:1812 #: views/base/edit/widget-tabs.php:106 #: views/dashboard-statistics/filter.php:28 #: views/dashboard-statistics/widget-mrr-growth.php:16 @@ -403,7 +403,7 @@ msgstr "" #: inc/admin-pages/class-broadcast-edit-admin-page.php:428 #: inc/admin-pages/class-checkout-form-edit-admin-page.php:1459 #: inc/admin-pages/class-customer-edit-admin-page.php:1065 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:590 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:796 #: inc/admin-pages/class-domain-edit-admin-page.php:497 #: inc/admin-pages/class-edit-admin-page.php:277 #: inc/admin-pages/class-email-edit-admin-page.php:452 @@ -411,7 +411,7 @@ msgstr "" #: inc/admin-pages/class-event-view-admin-page.php:253 #: inc/admin-pages/class-membership-edit-admin-page.php:997 #: inc/admin-pages/class-payment-edit-admin-page.php:1225 -#: inc/admin-pages/class-product-edit-admin-page.php:1010 +#: inc/admin-pages/class-product-edit-admin-page.php:1093 #: inc/admin-pages/class-site-edit-admin-page.php:678 #: inc/admin-pages/class-view-logs-admin-page.php:281 #: inc/admin-pages/class-webhook-edit-admin-page.php:343 @@ -660,6 +660,7 @@ msgid "Save Field" msgstr "" #: inc/admin-pages/class-checkout-form-edit-admin-page.php:724 +#: inc/admin-pages/class-membership-list-admin-page.php:125 #: inc/compat/class-legacy-shortcodes.php:352 #: inc/models/class-checkout-form.php:841 #: views/legacy/signup/pricing-table/frequency-selector.php:31 @@ -783,9 +784,9 @@ msgid "Add custom snippets in HTML (with javascript support) to add conversion t msgstr "" #: inc/admin-pages/class-checkout-form-edit-admin-page.php:1245 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:230 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:252 #: inc/admin-pages/class-email-edit-admin-page.php:294 -#: inc/class-settings.php:1752 +#: inc/class-settings.php:1753 msgid "Advanced Options" msgstr "" @@ -857,14 +858,14 @@ msgstr "" #: inc/admin-pages/class-checkout-form-edit-admin-page.php:1370 #: inc/admin-pages/class-checkout-form-edit-admin-page.php:1374 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:378 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:382 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:400 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:404 #: inc/admin-pages/class-domain-edit-admin-page.php:376 #: inc/admin-pages/class-domain-edit-admin-page.php:388 #: inc/admin-pages/class-email-edit-admin-page.php:279 #: inc/admin-pages/class-email-edit-admin-page.php:283 -#: inc/admin-pages/class-product-edit-admin-page.php:515 -#: inc/admin-pages/class-product-edit-admin-page.php:519 +#: inc/admin-pages/class-product-edit-admin-page.php:598 +#: inc/admin-pages/class-product-edit-admin-page.php:602 #: inc/admin-pages/class-site-edit-admin-page.php:543 #: inc/admin-pages/class-site-edit-admin-page.php:547 #: inc/admin-pages/class-template-previewer-customize-admin-page.php:106 @@ -1151,10 +1152,10 @@ msgid "Value" msgstr "" #: inc/admin-pages/class-customer-edit-admin-page.php:592 -#: inc/admin-pages/class-product-edit-admin-page.php:762 +#: inc/admin-pages/class-product-edit-admin-page.php:845 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:216 #: inc/checkout/signup-fields/class-signup-field-select.php:179 -#: inc/class-settings.php:1137 +#: inc/class-settings.php:1153 #: inc/list-tables/class-membership-line-item-list-table.php:139 #: inc/list-tables/class-payment-line-item-list-table.php:82 #: views/checkout/templates/order-bump/simple.php:49 @@ -1240,11 +1241,11 @@ msgid "Total Grossed" msgstr "" #: inc/admin-pages/class-customer-edit-admin-page.php:725 -#: inc/admin-pages/class-membership-list-admin-page.php:279 -#: inc/admin-pages/class-membership-list-admin-page.php:290 -#: inc/admin-pages/class-membership-list-admin-page.php:301 -#: inc/class-settings.php:950 -#: inc/class-settings.php:951 +#: inc/admin-pages/class-membership-list-admin-page.php:311 +#: inc/admin-pages/class-membership-list-admin-page.php:322 +#: inc/admin-pages/class-membership-list-admin-page.php:333 +#: inc/class-settings.php:966 +#: inc/class-settings.php:967 #: inc/debug/class-debug.php:195 #: inc/list-tables/class-customer-list-table.php:244 #: inc/list-tables/class-membership-list-table-widget.php:42 @@ -1263,7 +1264,7 @@ msgstr "" #: inc/admin-pages/class-email-edit-admin-page.php:298 #: inc/admin-pages/class-invoice-template-customize-admin-page.php:193 #: inc/admin-pages/class-membership-edit-admin-page.php:431 -#: inc/admin-pages/class-product-edit-admin-page.php:623 +#: inc/admin-pages/class-product-edit-admin-page.php:706 #: inc/admin-pages/class-template-previewer-customize-admin-page.php:132 #: inc/class-settings.php:550 #: inc/class-settings.php:551 @@ -1341,8 +1342,8 @@ msgstr "" #: inc/admin-pages/class-payment-list-admin-page.php:255 #: inc/admin-pages/class-payment-list-admin-page.php:266 #: inc/admin-pages/class-top-admin-nav-menu.php:115 -#: inc/class-settings.php:1357 -#: inc/class-settings.php:1358 +#: inc/class-settings.php:1373 +#: inc/class-settings.php:1374 #: inc/debug/class-debug.php:263 #: inc/list-tables/class-payment-list-table-widget.php:42 #: inc/list-tables/class-payment-list-table.php:42 @@ -1355,8 +1356,8 @@ msgstr "" #: inc/admin-pages/class-site-list-admin-page.php:517 #: inc/admin-pages/class-site-list-admin-page.php:528 #: inc/admin-pages/class-site-list-admin-page.php:539 -#: inc/class-settings.php:1197 -#: inc/class-settings.php:1198 +#: inc/class-settings.php:1213 +#: inc/class-settings.php:1214 #: inc/debug/class-debug.php:212 #: inc/list-tables/class-site-list-table.php:45 #: inc/managers/class-limitation-manager.php:276 @@ -1780,7 +1781,7 @@ msgid "Allows you to set an expiration date for this coupon code." msgstr "" #: inc/admin-pages/class-discount-code-edit-admin-page.php:184 -#: inc/admin-pages/class-membership-list-admin-page.php:135 +#: inc/admin-pages/class-membership-list-admin-page.php:152 #: inc/list-tables/class-membership-list-table.php:242 msgid "Expiration Date" msgstr "" @@ -1809,112 +1810,196 @@ msgstr "" msgid "Manually select to which products this discount code should be applicable." msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:268 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:226 +msgid "Limit Billing Periods" +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:227 +msgid "Restrict this discount code to specific billing periods (e.g., only monthly or only annual plans)." +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:236 +msgid "Select Billing Periods" +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:237 +msgid "Manually select which billing periods this discount code should be applicable to." +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:290 #: inc/checkout/signup-fields/class-signup-field-discount-code.php:58 #: inc/models/class-checkout-form.php:1019 #: inc/models/class-checkout-form.php:1020 msgid "Coupon Code" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:270 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:292 msgid "E.g. XMAS10OFF" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:271 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:293 msgid "The actual code your customers will enter during checkout." msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:284 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:306 #: inc/list-tables/class-line-item-list-table.php:172 #: inc/list-tables/class-payment-line-item-list-table.php:94 #: views/invoice/template.php:111 msgid "Discount" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:295 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:351 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:317 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:373 #: inc/admin-pages/class-payment-edit-admin-page.php:824 msgid "Percentage (%)" msgstr "" #. translators: %s is the currency symbol. e.g. $ -#: inc/admin-pages/class-discount-code-edit-admin-page.php:297 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:353 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:319 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:375 #, php-format msgid "Absolute (%s)" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:318 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:340 msgid "Apply to Renewals" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:319 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:341 msgid "By default, discounts are only applied to the first payment." msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:327 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:339 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:349 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:361 msgid "Setup Fee Discount" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:328 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:350 msgid "Also set a discount for setup fee?" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:383 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:405 msgid "Use this option to manually enable or disable this discount code for new sign-ups." msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:407 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:429 msgid "Make applicable to this product." msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:435 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:457 msgid "You do not have any products at this moment." msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:456 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:485 +msgid "Make applicable to this billing period." +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:513 +msgid "No billing periods found. Create products with different billing periods first." +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:602 +#: inc/functions/date.php:115 +#: views/dashboard-statistics/widget-tax-by-day.php:19 +#: views/dashboard-statistics/widget-tax-by-day.php:52 +msgid "Day" +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:603 +#: inc/admin-pages/class-membership-edit-admin-page.php:701 +#: inc/admin-pages/class-product-edit-admin-page.php:491 +#: inc/admin-pages/class-product-edit-admin-page.php:545 +#: inc/admin-pages/class-product-edit-admin-page.php:872 +#: inc/checkout/signup-fields/class-signup-field-period-selection.php:242 +#: inc/functions/date.php:115 +msgid "Days" +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:606 +msgid "Week" +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:607 +#: inc/admin-pages/class-membership-edit-admin-page.php:702 +#: inc/admin-pages/class-product-edit-admin-page.php:492 +#: inc/admin-pages/class-product-edit-admin-page.php:546 +#: inc/admin-pages/class-product-edit-admin-page.php:873 +#: inc/checkout/signup-fields/class-signup-field-period-selection.php:243 +msgid "Weeks" +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:610 +#: inc/functions/date.php:118 +msgid "Month" +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:611 +#: inc/admin-pages/class-membership-edit-admin-page.php:703 +#: inc/admin-pages/class-product-edit-admin-page.php:493 +#: inc/admin-pages/class-product-edit-admin-page.php:547 +#: inc/admin-pages/class-product-edit-admin-page.php:874 +#: inc/checkout/signup-fields/class-signup-field-period-selection.php:244 +#: inc/functions/date.php:118 +msgid "Months" +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:614 +#: inc/functions/date.php:121 +msgid "Year" +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:615 +#: inc/admin-pages/class-membership-edit-admin-page.php:704 +#: inc/admin-pages/class-product-edit-admin-page.php:494 +#: inc/admin-pages/class-product-edit-admin-page.php:548 +#: inc/admin-pages/class-product-edit-admin-page.php:875 +#: inc/checkout/signup-fields/class-signup-field-period-selection.php:245 +#: inc/functions/date.php:121 +msgid "Years" +msgstr "" + +#: inc/admin-pages/class-discount-code-edit-admin-page.php:662 msgid "Legacy Add-ons" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:469 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:491 -#: inc/admin-pages/class-product-edit-admin-page.php:575 -#: inc/admin-pages/class-product-edit-admin-page.php:597 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:675 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:697 +#: inc/admin-pages/class-product-edit-admin-page.php:658 +#: inc/admin-pages/class-product-edit-admin-page.php:680 msgid "Legacy Options" msgstr "" #. translators: %s is the comma-separated list of legacy add-ons. -#: inc/admin-pages/class-discount-code-edit-admin-page.php:471 -#: inc/admin-pages/class-product-edit-admin-page.php:577 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:677 +#: inc/admin-pages/class-product-edit-admin-page.php:660 #, php-format msgid "Options for %s, and others." msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:548 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:559 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:582 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:754 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:765 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:788 msgid "Edit Discount Code" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:548 -#: inc/admin-pages/class-discount-code-edit-admin-page.php:583 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:754 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:789 msgid "Add new Discount Code" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:584 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:790 msgid "Discount Code updated successfully!" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:585 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:791 msgid "Enter Discount Code" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:587 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:793 msgid "Save Discount Code" msgstr "" -#: inc/admin-pages/class-discount-code-edit-admin-page.php:589 +#: inc/admin-pages/class-discount-code-edit-admin-page.php:795 msgid "Delete Discount Code" msgstr "" @@ -1962,8 +2047,8 @@ msgstr "" #: inc/managers/class-notes-manager.php:407 #: inc/ui/class-current-membership-element.php:454 #: inc/ui/class-domain-mapping-element.php:459 -#: inc/ui/class-site-actions-element.php:541 -#: inc/ui/class-site-actions-element.php:886 +#: inc/ui/class-site-actions-element.php:568 +#: inc/ui/class-site-actions-element.php:1058 msgid "This action can not be undone." msgstr "" @@ -2841,7 +2926,7 @@ msgstr "" #: inc/admin-pages/class-invoice-template-customize-admin-page.php:203 #: inc/admin-pages/class-invoice-template-customize-admin-page.php:287 -#: inc/admin-pages/class-product-edit-admin-page.php:298 +#: inc/admin-pages/class-product-edit-admin-page.php:299 #: inc/invoices/class-invoice.php:260 msgid "Paid" msgstr "" @@ -2968,7 +3053,7 @@ msgstr "" #: inc/admin-pages/class-product-list-admin-page.php:98 #: inc/admin-pages/class-product-list-admin-page.php:109 #: inc/admin-pages/class-top-admin-nav-menu.php:103 -#: inc/checkout/class-checkout.php:2125 +#: inc/checkout/class-checkout.php:2127 #: inc/checkout/signup-fields/class-signup-field-pricing-table.php:170 #: inc/checkout/signup-fields/class-signup-field-products.php:149 #: inc/checkout/signup-fields/class-signup-field-products.php:150 @@ -3061,7 +3146,7 @@ msgstr "" #: inc/admin-pages/class-membership-edit-admin-page.php:626 #: inc/admin-pages/class-payment-edit-admin-page.php:746 #: inc/admin-pages/class-payment-edit-admin-page.php:764 -#: inc/admin-pages/class-product-edit-admin-page.php:495 +#: inc/admin-pages/class-product-edit-admin-page.php:578 #, php-format msgid "E.g. %s" msgstr "" @@ -3084,48 +3169,13 @@ msgstr "" #. translators: placeholder %1$s is the amount, %2$s is the duration (such as 1, 2, 3), and %3$s is the unit (such as month, year, week) #: inc/admin-pages/class-membership-edit-admin-page.php:665 -#: inc/admin-pages/class-product-edit-admin-page.php:370 +#: inc/admin-pages/class-product-edit-admin-page.php:453 #, php-format msgid "The customer will be charged %1$s every %2$s %3$s(s)." msgstr "" -#: inc/admin-pages/class-membership-edit-admin-page.php:701 -#: inc/admin-pages/class-product-edit-admin-page.php:408 -#: inc/admin-pages/class-product-edit-admin-page.php:462 -#: inc/admin-pages/class-product-edit-admin-page.php:789 -#: inc/checkout/signup-fields/class-signup-field-period-selection.php:242 -#: inc/functions/date.php:115 -msgid "Days" -msgstr "" - -#: inc/admin-pages/class-membership-edit-admin-page.php:702 -#: inc/admin-pages/class-product-edit-admin-page.php:409 -#: inc/admin-pages/class-product-edit-admin-page.php:463 -#: inc/admin-pages/class-product-edit-admin-page.php:790 -#: inc/checkout/signup-fields/class-signup-field-period-selection.php:243 -msgid "Weeks" -msgstr "" - -#: inc/admin-pages/class-membership-edit-admin-page.php:703 -#: inc/admin-pages/class-product-edit-admin-page.php:410 -#: inc/admin-pages/class-product-edit-admin-page.php:464 -#: inc/admin-pages/class-product-edit-admin-page.php:791 -#: inc/checkout/signup-fields/class-signup-field-period-selection.php:244 -#: inc/functions/date.php:118 -msgid "Months" -msgstr "" - -#: inc/admin-pages/class-membership-edit-admin-page.php:704 -#: inc/admin-pages/class-product-edit-admin-page.php:411 -#: inc/admin-pages/class-product-edit-admin-page.php:465 -#: inc/admin-pages/class-product-edit-admin-page.php:792 -#: inc/checkout/signup-fields/class-signup-field-period-selection.php:245 -#: inc/functions/date.php:121 -msgid "Years" -msgstr "" - #: inc/admin-pages/class-membership-edit-admin-page.php:737 -#: inc/admin-pages/class-product-edit-admin-page.php:418 +#: inc/admin-pages/class-product-edit-admin-page.php:501 msgid "Billing Cycles" msgstr "" @@ -3134,7 +3184,7 @@ msgid "E.g. 0" msgstr "" #: inc/admin-pages/class-membership-edit-admin-page.php:739 -#: inc/admin-pages/class-product-edit-admin-page.php:420 +#: inc/admin-pages/class-product-edit-admin-page.php:503 msgid "How many times should we bill this customer. Leave 0 to charge until cancelled." msgstr "" @@ -3240,7 +3290,7 @@ msgstr "" #: inc/admin-pages/class-membership-edit-admin-page.php:993 #: inc/admin-pages/class-payment-edit-admin-page.php:1221 -#: inc/admin-pages/class-product-edit-admin-page.php:1006 +#: inc/admin-pages/class-product-edit-admin-page.php:1089 msgid "This name will be used on pricing tables, invoices, and more." msgstr "" @@ -3364,45 +3414,85 @@ msgstr "" msgid "You can add multiples products to this membership." msgstr "" -#: inc/admin-pages/class-membership-list-admin-page.php:120 +#: inc/admin-pages/class-membership-list-admin-page.php:118 +#: inc/admin-pages/class-product-edit-admin-page.php:377 +msgid "Billing Period" +msgstr "" + +#: inc/admin-pages/class-membership-list-admin-page.php:119 +msgid "Select the billing period for this membership. Must match a price variation in the selected product." +msgstr "" + +#: inc/admin-pages/class-membership-list-admin-page.php:123 +msgid "Daily" +msgstr "" + +#: inc/admin-pages/class-membership-list-admin-page.php:124 +msgid "Weekly" +msgstr "" + +#: inc/admin-pages/class-membership-list-admin-page.php:126 +msgid "Quarterly (3 months)" +msgstr "" + +#: inc/admin-pages/class-membership-list-admin-page.php:127 +msgid "Semi-annually (6 months)" +msgstr "" + +#: inc/admin-pages/class-membership-list-admin-page.php:128 +#: inc/compat/class-legacy-shortcodes.php:362 +#: inc/models/class-checkout-form.php:851 +#: views/legacy/signup/pricing-table/frequency-selector.php:33 +msgid "Yearly" +msgstr "" + +#: inc/admin-pages/class-membership-list-admin-page.php:129 +msgid "Every 2 years" +msgstr "" + +#: inc/admin-pages/class-membership-list-admin-page.php:130 +msgid "Every 3 years" +msgstr "" + +#: inc/admin-pages/class-membership-list-admin-page.php:137 #: inc/apis/class-register-endpoint.php:376 msgid "The membership status." msgstr "" -#: inc/admin-pages/class-membership-list-admin-page.php:127 +#: inc/admin-pages/class-membership-list-admin-page.php:144 #: inc/list-tables/class-membership-list-table.php:202 msgid "Lifetime" msgstr "" -#: inc/admin-pages/class-membership-list-admin-page.php:128 +#: inc/admin-pages/class-membership-list-admin-page.php:145 msgid "Activate this toggle to mark the newly created membership as lifetime." msgstr "" -#: inc/admin-pages/class-membership-list-admin-page.php:136 +#: inc/admin-pages/class-membership-list-admin-page.php:153 msgid "Set the expiration date of the membership to be created." msgstr "" -#: inc/admin-pages/class-membership-list-admin-page.php:153 +#: inc/admin-pages/class-membership-list-admin-page.php:170 msgid "Create Membership" msgstr "" -#: inc/admin-pages/class-membership-list-admin-page.php:200 +#: inc/admin-pages/class-membership-list-admin-page.php:217 msgid "Products can not be empty." msgstr "" -#: inc/admin-pages/class-membership-list-admin-page.php:211 +#: inc/admin-pages/class-membership-list-admin-page.php:228 msgid "The selected customer does not exist." msgstr "" -#: inc/admin-pages/class-membership-list-admin-page.php:266 +#: inc/admin-pages/class-membership-list-admin-page.php:298 msgid "Membership removed successfully." msgstr "" -#: inc/admin-pages/class-membership-list-admin-page.php:267 +#: inc/admin-pages/class-membership-list-admin-page.php:299 msgid "Search Membership" msgstr "" -#: inc/admin-pages/class-membership-list-admin-page.php:314 +#: inc/admin-pages/class-membership-list-admin-page.php:346 msgid "Add Membership" msgstr "" @@ -3511,7 +3601,7 @@ msgid "Fee" msgstr "" #: inc/admin-pages/class-payment-edit-admin-page.php:669 -#: inc/checkout/class-cart.php:1302 +#: inc/checkout/class-cart.php:1332 msgid "Credit" msgstr "" @@ -3549,7 +3639,7 @@ msgid "Item quantity." msgstr "" #: inc/admin-pages/class-payment-edit-admin-page.php:725 -#: inc/admin-pages/class-product-edit-admin-page.php:419 +#: inc/admin-pages/class-product-edit-admin-page.php:502 msgid "E.g. 1" msgstr "" @@ -3575,7 +3665,7 @@ msgid "Refund, credit or fee amount." msgstr "" #: inc/admin-pages/class-payment-edit-admin-page.php:779 -#: inc/admin-pages/class-product-edit-admin-page.php:846 +#: inc/admin-pages/class-product-edit-admin-page.php:929 msgid "Is Taxable?" msgstr "" @@ -3864,58 +3954,118 @@ msgstr "" msgid "Product Options" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:292 +#: inc/admin-pages/class-product-edit-admin-page.php:293 msgid "Pricing Type" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:293 +#: inc/admin-pages/class-product-edit-admin-page.php:294 msgid "Select Pricing Type" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:294 -msgid "Products can be free, paid, or require further contact for pricing." +#: inc/admin-pages/class-product-edit-admin-page.php:295 +msgid "Products can be free, paid, pay what you want, or require further contact for pricing." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:300 -#: inc/models/class-product.php:699 +#: inc/admin-pages/class-product-edit-admin-page.php:301 +msgid "Pay What You Want" +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:302 +#: inc/models/class-product.php:751 msgid "Contact Us" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:311 +#: inc/admin-pages/class-product-edit-admin-page.php:313 msgid "Button Label" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:312 +#: inc/admin-pages/class-product-edit-admin-page.php:314 msgid "E.g. Contact us" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:313 +#: inc/admin-pages/class-product-edit-admin-page.php:315 msgid "This will be used on the pricing table CTA button, as the label." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:322 +#: inc/admin-pages/class-product-edit-admin-page.php:324 msgid "Button Link" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:323 +#: inc/admin-pages/class-product-edit-admin-page.php:325 msgid "E.g. https://contactus.page.com" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:324 +#: inc/admin-pages/class-product-edit-admin-page.php:326 msgid "This will be used on the pricing table CTA button." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:333 +#: inc/admin-pages/class-product-edit-admin-page.php:335 +msgid "Minimum Price" +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:337 +msgid "The minimum amount customers can pay. Leave at 0 for truly \"pay what you want\"." +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:347 +msgid "Suggested Price" +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:349 +msgid "A suggested price shown as the default value in the price input." +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:359 +msgid "Recurring Mode" +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:360 +msgid "Control whether customers can choose between one-time and recurring payments." +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:363 +msgid "Customer Chooses (One-time or Recurring)" +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:364 +msgid "Force Recurring Only" +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:365 +msgid "Force One-time Only" +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:378 +msgid "The billing period for recurring PWYW payments. Only applies when recurring is enabled." +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:406 +msgid "Day(s)" +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:407 +msgid "Week(s)" +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:408 +msgid "Month(s)" +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:409 +msgid "Year(s)" +msgstr "" + +#: inc/admin-pages/class-product-edit-admin-page.php:416 msgid "Is Recurring?" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:334 +#: inc/admin-pages/class-product-edit-admin-page.php:417 msgid "Check this if this product has a recurring charge." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:352 -#: inc/admin-pages/class-product-edit-admin-page.php:353 -#: inc/admin-pages/class-product-edit-admin-page.php:368 +#: inc/admin-pages/class-product-edit-admin-page.php:435 +#: inc/admin-pages/class-product-edit-admin-page.php:436 +#: inc/admin-pages/class-product-edit-admin-page.php:451 #: inc/list-tables/class-membership-list-table-widget.php:265 #: inc/list-tables/class-membership-list-table.php:180 #: inc/list-tables/class-product-list-table.php:256 @@ -3923,171 +4073,171 @@ msgstr "" msgid "Price" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:430 +#: inc/admin-pages/class-product-edit-admin-page.php:513 msgid "Offer Trial" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:431 +#: inc/admin-pages/class-product-edit-admin-page.php:514 msgid "Check if you want to add a trial period to this product." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:443 +#: inc/admin-pages/class-product-edit-admin-page.php:526 msgid "Trial" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:472 +#: inc/admin-pages/class-product-edit-admin-page.php:555 msgid "Add Setup Fee?" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:473 +#: inc/admin-pages/class-product-edit-admin-page.php:556 msgid "Check if you want to add a setup fee." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:492 +#: inc/admin-pages/class-product-edit-admin-page.php:575 #: inc/list-tables/class-product-list-table.php:257 msgid "Setup Fee" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:493 +#: inc/admin-pages/class-product-edit-admin-page.php:576 msgid "The setup fee will be added to the first charge, in addition to the regular price of the product." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:520 +#: inc/admin-pages/class-product-edit-admin-page.php:603 msgid "Use this option to manually enable or disable this product for new sign-ups." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:530 -#: inc/admin-pages/class-product-edit-admin-page.php:535 +#: inc/admin-pages/class-product-edit-admin-page.php:613 +#: inc/admin-pages/class-product-edit-admin-page.php:618 msgid "Product Image" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:536 +#: inc/admin-pages/class-product-edit-admin-page.php:619 msgid "This image is used on product list tables and other places." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:624 +#: inc/admin-pages/class-product-edit-admin-page.php:707 msgid "General product options such as product slug, type, etc." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:633 +#: inc/admin-pages/class-product-edit-admin-page.php:716 #: inc/list-tables/class-membership-line-item-list-table.php:115 msgid "Product Slug" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:634 +#: inc/admin-pages/class-product-edit-admin-page.php:717 msgid "e.g. premium" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:635 +#: inc/admin-pages/class-product-edit-admin-page.php:718 msgid "This serves as a id to the product in a number of different contexts." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:637 +#: inc/admin-pages/class-product-edit-admin-page.php:720 msgid "Lowercase alpha-numeric characters with dashes or underlines. No spaces allowed." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:646 -#: inc/admin-pages/class-product-edit-admin-page.php:647 +#: inc/admin-pages/class-product-edit-admin-page.php:729 +#: inc/admin-pages/class-product-edit-admin-page.php:730 msgid "Product Type" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:648 +#: inc/admin-pages/class-product-edit-admin-page.php:731 msgid "Different product types have different options." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:657 +#: inc/admin-pages/class-product-edit-admin-page.php:740 msgid "Customer Role" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:658 +#: inc/admin-pages/class-product-edit-admin-page.php:741 msgid "Select the role Ultimate Multisite should use when adding the user to their newly created site." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:675 +#: inc/admin-pages/class-product-edit-admin-page.php:758 msgid "Up & Downgrades" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:676 +#: inc/admin-pages/class-product-edit-admin-page.php:759 msgid "Settings related to upgrade and downgrade flows." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:682 +#: inc/admin-pages/class-product-edit-admin-page.php:765 msgid "Plan Group" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:683 +#: inc/admin-pages/class-product-edit-admin-page.php:766 msgid "Add related plans to the same group to have them show up as upgrade/downgrade paths." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:684 -#: inc/admin-pages/class-product-edit-admin-page.php:696 +#: inc/admin-pages/class-product-edit-admin-page.php:767 +#: inc/admin-pages/class-product-edit-admin-page.php:779 msgid "Type and press enter to search and/or add." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:687 +#: inc/admin-pages/class-product-edit-admin-page.php:770 msgid "Select Group" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:694 +#: inc/admin-pages/class-product-edit-admin-page.php:777 msgid "Product Order" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:695 +#: inc/admin-pages/class-product-edit-admin-page.php:778 msgid "Plans are shown in the order determined by this parameter, from the lowest to the highest." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:702 +#: inc/admin-pages/class-product-edit-admin-page.php:785 msgid "Offer Add-ons" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:703 +#: inc/admin-pages/class-product-edit-admin-page.php:786 msgid "Search for a package or service" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:704 +#: inc/admin-pages/class-product-edit-admin-page.php:787 msgid "This products will be offered inside upgrade/downgrade forms as order bumps." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:726 +#: inc/admin-pages/class-product-edit-admin-page.php:809 msgid "Price Variations" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:727 +#: inc/admin-pages/class-product-edit-admin-page.php:810 msgid "Discounts for longer membership commitments." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:736 +#: inc/admin-pages/class-product-edit-admin-page.php:819 msgid "Enable Price Variations" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:737 +#: inc/admin-pages/class-product-edit-admin-page.php:820 msgid "Price Variations are an easy way to offer discounted prices for longer subscription commitments." msgstr "" #. translators: 1 is the price, 2 is the duration and 3 the duration unit -#: inc/admin-pages/class-product-edit-admin-page.php:746 +#: inc/admin-pages/class-product-edit-admin-page.php:829 #, php-format msgid "A discounted price of %1$s will be used when memberships are created with the recurrence of %2$s %3$s(s) instead of the regular period." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:769 +#: inc/admin-pages/class-product-edit-admin-page.php:852 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:222 msgid "Duration" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:781 +#: inc/admin-pages/class-product-edit-admin-page.php:864 msgid "Period" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:805 +#: inc/admin-pages/class-product-edit-admin-page.php:888 msgid "New Price" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:818 +#: inc/admin-pages/class-product-edit-admin-page.php:901 msgid "Add new Price Variation" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:837 +#: inc/admin-pages/class-product-edit-admin-page.php:920 #: inc/list-tables/class-line-item-list-table.php:216 #: inc/tax/class-dashboard-taxes-tab.php:63 #: inc/tax/class-dashboard-taxes-tab.php:151 @@ -4097,102 +4247,102 @@ msgstr "" msgid "Taxes" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:838 +#: inc/admin-pages/class-product-edit-admin-page.php:921 msgid "Tax settings for your products." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:847 +#: inc/admin-pages/class-product-edit-admin-page.php:930 msgid "Enable this if you plan to collect taxes for this product." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:855 +#: inc/admin-pages/class-product-edit-admin-page.php:938 msgid "Tax Category" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:856 +#: inc/admin-pages/class-product-edit-admin-page.php:939 msgid "Select the product tax category." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:868 -#: inc/admin-pages/class-product-edit-admin-page.php:911 +#: inc/admin-pages/class-product-edit-admin-page.php:951 +#: inc/admin-pages/class-product-edit-admin-page.php:994 msgid "Site Templates" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:869 +#: inc/admin-pages/class-product-edit-admin-page.php:952 msgid "Limit which site templates are available for this particular template." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:880 +#: inc/admin-pages/class-product-edit-admin-page.php:963 msgid "Allow Site Templates" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:881 +#: inc/admin-pages/class-product-edit-admin-page.php:964 msgid "Toggle this option on to allow this plan to use Site Templates. If this option is disabled, sign-ups on this plan will get a default WordPress site." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:891 -#: inc/admin-pages/class-product-edit-admin-page.php:892 +#: inc/admin-pages/class-product-edit-admin-page.php:974 +#: inc/admin-pages/class-product-edit-admin-page.php:975 msgid "Site Template Selection Mode" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:893 +#: inc/admin-pages/class-product-edit-admin-page.php:976 msgid "Select the type of limitation you want to apply." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:894 +#: inc/admin-pages/class-product-edit-admin-page.php:977 msgid "\"Default\" will follow the settings of the checkout form: if you have a template selection field in there, all the templates selected will show up. If no field is present, then a default WordPress site will be created.

\"Assign Site Template\" forces new accounts with this plan to use a particular template site (this option removes the template selection field from the signup, if one exists).

Finally, \"Choose Available Site Templates\", overrides the templates selected on the checkout form with the templates selected here, while also giving you the chance of pre-select a template to be used as default." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:897 +#: inc/admin-pages/class-product-edit-admin-page.php:980 msgid "Default - Allow All Site Templates" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:898 +#: inc/admin-pages/class-product-edit-admin-page.php:981 msgid "Assign Site Template" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:899 +#: inc/admin-pages/class-product-edit-admin-page.php:982 msgid "Choose Available Site Templates" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:912 +#: inc/admin-pages/class-product-edit-admin-page.php:995 msgid "Select the Site Template to assign." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:912 +#: inc/admin-pages/class-product-edit-admin-page.php:995 msgid "Customize the access level of each Site Template below." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:954 -#: inc/admin-pages/class-product-edit-admin-page.php:965 -#: inc/admin-pages/class-product-edit-admin-page.php:1002 +#: inc/admin-pages/class-product-edit-admin-page.php:1037 +#: inc/admin-pages/class-product-edit-admin-page.php:1048 +#: inc/admin-pages/class-product-edit-admin-page.php:1085 msgid "Edit Product" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:954 -#: inc/admin-pages/class-product-edit-admin-page.php:1003 +#: inc/admin-pages/class-product-edit-admin-page.php:1037 +#: inc/admin-pages/class-product-edit-admin-page.php:1086 #: views/memberships/product-list.php:17 #: views/memberships/product-list.php:21 msgid "Add new Product" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:983 +#: inc/admin-pages/class-product-edit-admin-page.php:1066 msgid "Click to copy Shareable Link" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:1004 +#: inc/admin-pages/class-product-edit-admin-page.php:1087 msgid "Product updated with success!" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:1005 +#: inc/admin-pages/class-product-edit-admin-page.php:1088 msgid "Enter Product Name" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:1007 +#: inc/admin-pages/class-product-edit-admin-page.php:1090 msgid "Save Product" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:1009 +#: inc/admin-pages/class-product-edit-admin-page.php:1092 msgid "Delete Product" msgstr "" @@ -4322,8 +4472,8 @@ msgid "This action cannot be undone. Make sure you have a backup of your current msgstr "" #: inc/admin-pages/class-settings-admin-page.php:711 -#: inc/class-settings.php:1608 -#: inc/class-settings.php:1621 +#: inc/class-settings.php:1624 +#: inc/class-settings.php:1637 msgid "Import Settings" msgstr "" @@ -4595,7 +4745,7 @@ msgid "Tell your customers what this site is about." msgstr "" #: inc/admin-pages/class-site-edit-admin-page.php:358 -#: inc/class-settings.php:1207 +#: inc/class-settings.php:1223 msgid "Site Options" msgstr "" @@ -4746,9 +4896,9 @@ msgid "This name will be used as the site title." msgstr "" #: inc/admin-pages/class-site-edit-admin-page.php:677 -#: inc/ui/class-site-actions-element.php:445 -#: inc/ui/class-site-actions-element.php:548 -#: inc/ui/class-site-actions-element.php:549 +#: inc/ui/class-site-actions-element.php:472 +#: inc/ui/class-site-actions-element.php:575 +#: inc/ui/class-site-actions-element.php:576 msgid "Delete Site" msgstr "" @@ -4891,7 +5041,7 @@ msgstr "" #: inc/admin-pages/class-system-info-admin-page.php:479 #: inc/admin-pages/class-system-info-admin-page.php:484 #: inc/admin-pages/class-system-info-admin-page.php:489 -#: inc/class-settings.php:1726 +#: inc/class-settings.php:1742 msgid "Disabled" msgstr "" @@ -5573,6 +5723,66 @@ msgstr "" msgid "The customer id sent does not correspond to a valid customer." msgstr "" +#: inc/apis/class-settings-endpoint.php:70 +msgid "The setting key to retrieve." +msgstr "" + +#: inc/apis/class-settings-endpoint.php:101 +msgid "The setting key to update." +msgstr "" + +#: inc/apis/class-settings-endpoint.php:107 +msgid "The new value for the setting." +msgstr "" + +#: inc/apis/class-settings-endpoint.php:158 +msgid "This setting is protected and cannot be retrieved via the API." +msgstr "" + +#. translators: %s is the setting key +#: inc/apis/class-settings-endpoint.php:174 +#, php-format +msgid "Setting \"%s\" not found." +msgstr "" + +#: inc/apis/class-settings-endpoint.php:214 +msgid "No valid settings provided. Please provide a \"settings\" object with key-value pairs." +msgstr "" + +#. translators: %s is the setting key +#: inc/apis/class-settings-endpoint.php:227 +#, php-format +msgid "Setting \"%s\" is protected and cannot be modified via the API." +msgstr "" + +#. translators: %s is the setting key +#: inc/apis/class-settings-endpoint.php:238 +#, php-format +msgid "Invalid setting key format: \"%s\"." +msgstr "" + +#: inc/apis/class-settings-endpoint.php:250 +msgid "No valid settings to update after filtering." +msgstr "" + +#: inc/apis/class-settings-endpoint.php:306 +msgid "This setting is protected and cannot be modified via the API." +msgstr "" + +#: inc/apis/class-settings-endpoint.php:322 +msgid "The \"value\" parameter is required." +msgstr "" + +#. translators: %s is the setting key +#: inc/apis/class-settings-endpoint.php:334 +#, php-format +msgid "Failed to update setting \"%s\"." +msgstr "" + +#: inc/apis/class-settings-endpoint.php:360 +msgid "An object containing setting key-value pairs to update." +msgstr "" + #: inc/apis/schemas/broadcast-create.php:23 #: inc/apis/schemas/broadcast-update.php:23 #: inc/apis/schemas/checkout-form-create.php:83 @@ -6755,173 +6965,179 @@ msgstr "" msgid "Limit response to specific fields. Defaults to id, name" msgstr "" -#: inc/checkout/class-cart.php:564 +#: inc/checkout/class-cart.php:594 msgid "The payment in question was not found." msgstr "" -#: inc/checkout/class-cart.php:591 +#: inc/checkout/class-cart.php:621 msgid "You are not allowed to modify this payment." msgstr "" -#: inc/checkout/class-cart.php:602 -#: inc/checkout/class-cart.php:750 +#: inc/checkout/class-cart.php:632 +#: inc/checkout/class-cart.php:780 msgid "The membership in question was not found." msgstr "" -#: inc/checkout/class-cart.php:691 +#: inc/checkout/class-cart.php:721 msgid "The payment in question has an invalid status." msgstr "" -#: inc/checkout/class-cart.php:770 +#: inc/checkout/class-cart.php:800 #: inc/gateways/class-base-stripe-gateway.php:269 msgid "You are not allowed to modify this membership." msgstr "" -#: inc/checkout/class-cart.php:802 -#: inc/checkout/class-cart.php:824 -#: inc/checkout/class-cart.php:946 +#: inc/checkout/class-cart.php:832 +#: inc/checkout/class-cart.php:854 +#: inc/checkout/class-cart.php:976 msgid "This cart proposes no changes to the current membership." msgstr "" #. translators: %1$d: current number of posts, %2$s: post type name, %3$d: posts quota, %4$s: post type name, %5$d: number of posts to be deleted, %6$s: post type name. -#: inc/checkout/class-cart.php:966 +#: inc/checkout/class-cart.php:996 #, php-format msgid "Your site currently has %1$d %2$s but the new plan is limited to %3$d %4$s. You must trash %5$d %6$s before you can downgrade your plan." msgstr "" -#: inc/checkout/class-cart.php:992 +#: inc/checkout/class-cart.php:1022 msgid "This new plan does NOT support custom domains. You must remove all custom domains before you can downgrade your plan." msgstr "" #. translators: %1$d: current number of custom domains, %2$s: 'custom domain' or 'custom domains', %3$d: domain limit, %4$s: 'custom domain' or 'custom domains', %5$d: number of domains to be removed, %6$s: 'custom domain' or 'custom domains'. -#: inc/checkout/class-cart.php:1000 +#: inc/checkout/class-cart.php:1030 #, php-format msgid "Your site currently has %1$d %2$s but the new plan is limited to %3$d %4$s. You must remove %5$d %6$s before you can downgrade your plan." msgstr "" -#: inc/checkout/class-cart.php:1002 -#: inc/checkout/class-cart.php:1004 -#: inc/checkout/class-cart.php:1006 +#: inc/checkout/class-cart.php:1032 +#: inc/checkout/class-cart.php:1034 +#: inc/checkout/class-cart.php:1036 msgid "custom domains" msgstr "" -#: inc/checkout/class-cart.php:1002 -#: inc/checkout/class-cart.php:1004 -#: inc/checkout/class-cart.php:1006 +#: inc/checkout/class-cart.php:1032 +#: inc/checkout/class-cart.php:1034 +#: inc/checkout/class-cart.php:1036 msgid "custom domain" msgstr "" #. Translators: Placeholder receives the recurring period description -#: inc/checkout/class-cart.php:1080 +#: inc/checkout/class-cart.php:1110 #, php-format msgid "You already have an active %s agreement." msgstr "" -#: inc/checkout/class-cart.php:1101 +#: inc/checkout/class-cart.php:1131 msgid "Scheduled Swap Credit" msgstr "" -#: inc/checkout/class-cart.php:1102 +#: inc/checkout/class-cart.php:1132 msgid "Swap scheduled to next billing cycle." msgstr "" -#: inc/checkout/class-cart.php:1303 +#: inc/checkout/class-cart.php:1333 msgid "Prorated amount based on the previous membership." msgstr "" #. translators: %s is the coupon code being used, all-caps. e.g. PROMO10OFF -#: inc/checkout/class-cart.php:1339 +#: inc/checkout/class-cart.php:1369 #, php-format msgid "The code %s do not exist or is no longer valid." msgstr "" #. translators: two intervals -#: inc/checkout/class-cart.php:1420 +#: inc/checkout/class-cart.php:1450 #, php-format msgid "Interval %1$s and %2$s do not match." msgstr "" -#: inc/checkout/class-cart.php:1583 +#: inc/checkout/class-cart.php:1613 msgid "The product you are trying to add does not exist." msgstr "" -#: inc/checkout/class-cart.php:1602 +#: inc/checkout/class-cart.php:1632 msgid "The product you are trying to add does not exist for the selected duration." msgstr "" -#: inc/checkout/class-cart.php:1617 +#: inc/checkout/class-cart.php:1647 msgid "There's already a plan in this membership." msgstr "" #. translators: respectively, product name, duration, and duration unit. -#: inc/checkout/class-cart.php:1684 +#: inc/checkout/class-cart.php:1714 #, php-format msgid "%1$s does not have a valid price variation for that billing period (every %2$s %3$s(s)) and was not added to the cart." msgstr "" +#. translators: %1$s is the product name, %2$s is the minimum amount formatted as currency +#: inc/checkout/class-cart.php:1739 +#, php-format +msgid "The amount for %1$s must be at least %2$s." +msgstr "" + #. translators: placeholder is the product name. -#: inc/checkout/class-cart.php:1748 +#: inc/checkout/class-cart.php:1836 #, php-format msgid "Signup Fee for %s" msgstr "" #. translators: placeholder is the product name. -#: inc/checkout/class-cart.php:1748 +#: inc/checkout/class-cart.php:1836 #, php-format msgid "Signup Credit for %s" msgstr "" -#: inc/checkout/class-checkout-pages.php:108 +#: inc/checkout/class-checkout-pages.php:117 msgid "Ultimate Multisite Compatibility Mode" msgstr "" -#: inc/checkout/class-checkout-pages.php:109 +#: inc/checkout/class-checkout-pages.php:118 msgid "Toggle this option on if Ultimate Multisite elements are not loading correctly or at all." msgstr "" -#: inc/checkout/class-checkout-pages.php:188 +#: inc/checkout/class-checkout-pages.php:197 msgid "Error: The password you entered is incorrect." msgstr "" -#: inc/checkout/class-checkout-pages.php:220 +#: inc/checkout/class-checkout-pages.php:229 #: inc/integrations/host-providers/class-closte-host-provider.php:292 msgid "Something went wrong" msgstr "" #. translators: %1$s and %2$s are HTML tags -#: inc/checkout/class-checkout-pages.php:424 +#: inc/checkout/class-checkout-pages.php:433 #, php-format msgid "Your email address is not yet verified. Your site %1$s will only be activated %2$s after your email address is verified. Check your inbox and verify your email address." msgstr "" -#: inc/checkout/class-checkout-pages.php:428 +#: inc/checkout/class-checkout-pages.php:437 msgid "Resend verification email →" msgstr "" -#: inc/checkout/class-checkout-pages.php:633 +#: inc/checkout/class-checkout-pages.php:642 msgid "Ultimate Multisite - Register Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:634 +#: inc/checkout/class-checkout-pages.php:643 msgid "Ultimate Multisite - Login Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:635 +#: inc/checkout/class-checkout-pages.php:644 msgid "Ultimate Multisite - Site Blocked Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:636 +#: inc/checkout/class-checkout-pages.php:645 msgid "Ultimate Multisite - Membership Update Page" msgstr "" -#: inc/checkout/class-checkout-pages.php:637 +#: inc/checkout/class-checkout-pages.php:646 msgid "Ultimate Multisite - New Site Page" msgstr "" #: inc/checkout/class-checkout.php:752 #: inc/checkout/class-checkout.php:760 -#: inc/checkout/class-checkout.php:2349 -#: inc/checkout/class-checkout.php:2355 +#: inc/checkout/class-checkout.php:2351 +#: inc/checkout/class-checkout.php:2357 msgid "Payment gateway not registered." msgstr "" @@ -6933,99 +7149,99 @@ msgstr "" msgid "Something wrong happened while attempting to save the customer billing address" msgstr "" -#: inc/checkout/class-checkout.php:1674 +#: inc/checkout/class-checkout.php:1676 msgid "Invalid request" msgstr "" -#: inc/checkout/class-checkout.php:1686 +#: inc/checkout/class-checkout.php:1688 msgid "Too many requests. Please try again later." msgstr "" -#: inc/checkout/class-checkout.php:1728 +#: inc/checkout/class-checkout.php:1730 msgid "Please provide both username/email and password." msgstr "" -#: inc/checkout/class-checkout.php:1741 +#: inc/checkout/class-checkout.php:1743 msgid "Too many login attempts. Please try again in a few minutes." msgstr "" -#: inc/checkout/class-checkout.php:1770 +#: inc/checkout/class-checkout.php:1772 msgid "Invalid username or password." msgstr "" -#: inc/checkout/class-checkout.php:1788 +#: inc/checkout/class-checkout.php:1790 msgid "Login successful!" msgstr "" -#: inc/checkout/class-checkout.php:1811 +#: inc/checkout/class-checkout.php:1813 msgid "The item was added!" msgstr "" -#: inc/checkout/class-checkout.php:1812 +#: inc/checkout/class-checkout.php:1814 msgid "The Password entered is too weak." msgstr "" -#: inc/checkout/class-checkout.php:1813 +#: inc/checkout/class-checkout.php:1815 msgid "Password is required" msgstr "" -#: inc/checkout/class-checkout.php:1814 +#: inc/checkout/class-checkout.php:1816 msgid "Login failed. Please try again." msgstr "" -#: inc/checkout/class-checkout.php:1815 +#: inc/checkout/class-checkout.php:1817 msgid "Logging in..." msgstr "" -#: inc/checkout/class-checkout.php:1816 +#: inc/checkout/class-checkout.php:1818 #: views/checkout/partials/inline-login-prompt.php:17 msgid "Already have an account?" msgstr "" -#: inc/checkout/class-checkout.php:1817 +#: inc/checkout/class-checkout.php:1819 #: views/checkout/partials/inline-login-prompt.php:59 msgid "Sign in" msgstr "" -#: inc/checkout/class-checkout.php:1818 +#: inc/checkout/class-checkout.php:1820 #: views/checkout/partials/inline-login-prompt.php:51 msgid "Forgot password?" msgstr "" -#: inc/checkout/class-checkout.php:1819 +#: inc/checkout/class-checkout.php:1821 #: inc/list-tables/class-payment-list-table.php:111 #: views/dashboard-widgets/current-membership.php:315 msgid "Cancel" msgstr "" -#: inc/checkout/class-checkout.php:2121 +#: inc/checkout/class-checkout.php:2123 msgid "Password confirmation" msgstr "" -#: inc/checkout/class-checkout.php:2122 +#: inc/checkout/class-checkout.php:2124 msgid "Email confirmation" msgstr "" -#: inc/checkout/class-checkout.php:2123 +#: inc/checkout/class-checkout.php:2125 msgid "Template ID" msgstr "" -#: inc/checkout/class-checkout.php:2124 +#: inc/checkout/class-checkout.php:2126 msgid "Valid password" msgstr "" -#: inc/checkout/class-checkout.php:2126 +#: inc/checkout/class-checkout.php:2128 msgid "Payment Gateway" msgstr "" #. translators: %s payment id. -#: inc/checkout/class-checkout.php:2318 +#: inc/checkout/class-checkout.php:2320 #, php-format msgid "Payment (%s) not found." msgstr "" #. translators: %s is the membership ID -#: inc/checkout/class-checkout.php:2453 +#: inc/checkout/class-checkout.php:2455 #, php-format msgid "Checkout failed for customer %s: " msgstr "" @@ -7040,8 +7256,8 @@ msgstr "" #: inc/checkout/class-legacy-checkout.php:252 #: inc/models/class-membership.php:941 -#: inc/models/class-product.php:695 -#: inc/models/class-product.php:776 +#: inc/models/class-product.php:747 +#: inc/models/class-product.php:839 #: views/checkout/templates/pricing-table/legacy.php:163 #: views/legacy/signup/pricing-table/plan.php:29 #: views/legacy/signup/pricing-table/plan.php:48 @@ -7963,24 +8179,24 @@ msgid "Minimal" msgstr "" #. translators: %s the url for login. -#: inc/class-addon-repository.php:166 +#: inc/class-addon-repository.php:172 #, php-format msgid "You must Connect to UltimateMultisite.com first." msgstr "" -#: inc/class-addon-repository.php:185 +#: inc/class-addon-repository.php:191 msgid "403 Access Denied returned from server. Ensure you have an active subscription for this addon." msgstr "" -#: inc/class-addon-repository.php:189 +#: inc/class-addon-repository.php:195 msgid "Failed to connect to the update server. Please try again later." msgstr "" -#: inc/class-addon-repository.php:235 +#: inc/class-addon-repository.php:241 msgid "Successfully connected your site to UltimateMultisite.com." msgstr "" -#: inc/class-addon-repository.php:244 +#: inc/class-addon-repository.php:250 msgid "Failed to authenticate with UltimateMultisite.com." msgstr "" @@ -8363,7 +8579,7 @@ msgstr "" #: inc/class-orphaned-tables-manager.php:140 #: inc/class-orphaned-users-manager.php:133 -#: inc/class-settings.php:1645 +#: inc/class-settings.php:1661 msgid "Warning:" msgstr "" @@ -8647,7 +8863,7 @@ msgid "Currency Options" msgstr "" #: inc/class-settings.php:636 -#: inc/class-settings.php:1368 +#: inc/class-settings.php:1384 msgid "The following options affect how prices are displayed on the frontend, the backend and in reports." msgstr "" @@ -8698,627 +8914,627 @@ msgstr "" msgid "Number of Decimals" msgstr "" -#: inc/class-settings.php:719 -#: inc/class-settings.php:720 +#: inc/class-settings.php:715 +#: views/settings/widget-settings-body.php:278 +#: views/settings/widget-settings-body.php:283 +msgid "Help Improve Ultimate Multisite" +msgstr "" + +#. translators: %s is a link to the privacy policy +#: inc/class-settings.php:718 +#, php-format +msgid "Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. Learn more." +msgstr "" + +#: inc/class-settings.php:735 +#: inc/class-settings.php:736 msgid "Login & Registration" msgstr "" -#: inc/class-settings.php:729 +#: inc/class-settings.php:745 msgid "Login and Registration Options" msgstr "" -#: inc/class-settings.php:730 +#: inc/class-settings.php:746 msgid "Options related to registration and login behavior." msgstr "" -#: inc/class-settings.php:739 +#: inc/class-settings.php:755 msgid "Enable Registration" msgstr "" -#: inc/class-settings.php:740 +#: inc/class-settings.php:756 msgid "Turning this toggle off will disable registration in all checkout forms across the network." msgstr "" -#: inc/class-settings.php:750 +#: inc/class-settings.php:766 msgid "Email verification" msgstr "" -#: inc/class-settings.php:751 +#: inc/class-settings.php:767 msgid "Controls if email verification is required during registration. If set, sites will not be created until the customer email verification status is changed to verified." msgstr "" -#: inc/class-settings.php:754 +#: inc/class-settings.php:770 msgid "Never require email verification" msgstr "" -#: inc/class-settings.php:755 +#: inc/class-settings.php:771 msgid "Only for free plans" msgstr "" -#: inc/class-settings.php:756 +#: inc/class-settings.php:772 msgid "Always require email verification" msgstr "" -#: inc/class-settings.php:777 +#: inc/class-settings.php:793 msgid "Default Registration Page" msgstr "" -#: inc/class-settings.php:778 -#: inc/class-settings.php:810 -#: inc/class-settings.php:962 -#: inc/class-settings.php:1219 +#: inc/class-settings.php:794 +#: inc/class-settings.php:826 +#: inc/class-settings.php:978 +#: inc/class-settings.php:1235 msgid "Search pages on the main site..." msgstr "" -#: inc/class-settings.php:779 -#: inc/class-settings.php:963 -#: inc/class-settings.php:1220 +#: inc/class-settings.php:795 +#: inc/class-settings.php:979 +#: inc/class-settings.php:1236 msgid "Only published pages on the main site are available for selection, and you need to make sure they contain a [wu_checkout] shortcode." msgstr "" -#: inc/class-settings.php:797 +#: inc/class-settings.php:813 msgid "Use Custom Login Page" msgstr "" -#: inc/class-settings.php:798 +#: inc/class-settings.php:814 msgid "Turn this toggle on to select a custom page to be used as the login page." msgstr "" -#: inc/class-settings.php:809 +#: inc/class-settings.php:825 msgid "Default Login Page" msgstr "" -#: inc/class-settings.php:811 +#: inc/class-settings.php:827 msgid "Only published pages on the main site are available for selection, and you need to make sure they contain a [wu_login_form] shortcode." msgstr "" -#: inc/class-settings.php:831 +#: inc/class-settings.php:847 msgid "Obfuscate the Original Login URL (wp-login.php)" msgstr "" -#: inc/class-settings.php:832 +#: inc/class-settings.php:848 msgid "If this option is enabled, we will display a 404 error when a user tries to access the original wp-login.php link. This is useful to prevent brute-force attacks." msgstr "" -#: inc/class-settings.php:845 +#: inc/class-settings.php:861 msgid "Use Sub-site logo on Login Page" msgstr "" -#: inc/class-settings.php:846 +#: inc/class-settings.php:862 msgid "Toggle this option to replace the WordPress logo on the sub-site login page with the logo set for that sub-site. If unchecked, the network logo will be used instead." msgstr "" -#: inc/class-settings.php:859 +#: inc/class-settings.php:875 msgid "Force Synchronous Site Publication " msgstr "" -#: inc/class-settings.php:860 +#: inc/class-settings.php:876 msgid "By default, when a new pending site needs to be converted into a real network site, the publishing process happens via Job Queue, asynchronously. Enable this option to force the publication to happen in the same request as the signup. Be careful, as this can cause timeouts depending on the size of the site templates being copied." msgstr "" -#: inc/class-settings.php:870 +#: inc/class-settings.php:886 msgid "Password Strength" msgstr "" -#: inc/class-settings.php:871 +#: inc/class-settings.php:887 msgid "Configure password strength requirements for user registration." msgstr "" -#: inc/class-settings.php:880 +#: inc/class-settings.php:896 msgid "Minimum Password Strength" msgstr "" -#: inc/class-settings.php:881 +#: inc/class-settings.php:897 msgid "Set the minimum password strength required during registration and password reset. \"Super Strong\" requires at least 12 characters, including uppercase, lowercase, numbers, and special characters." msgstr "" -#: inc/class-settings.php:885 +#: inc/class-settings.php:901 msgid "Medium" msgstr "" -#: inc/class-settings.php:886 +#: inc/class-settings.php:902 msgid "Strong" msgstr "" -#: inc/class-settings.php:887 +#: inc/class-settings.php:903 msgid "Super Strong (12+ chars, mixed case, numbers, symbols)" msgstr "" -#: inc/class-settings.php:896 -#: inc/class-settings.php:1663 -#: inc/class-settings.php:1664 +#: inc/class-settings.php:912 +#: inc/class-settings.php:1679 +#: inc/class-settings.php:1680 msgid "Other Options" msgstr "" -#: inc/class-settings.php:897 +#: inc/class-settings.php:913 msgid "Other registration-related options." msgstr "" -#: inc/class-settings.php:906 +#: inc/class-settings.php:922 msgid "Default Role" msgstr "" -#: inc/class-settings.php:907 +#: inc/class-settings.php:923 msgid "Set the role to be applied to the user during the signup process." msgstr "" -#: inc/class-settings.php:918 +#: inc/class-settings.php:934 msgid "Add Users to the Main Site as well?" msgstr "" -#: inc/class-settings.php:919 +#: inc/class-settings.php:935 msgid "Enabling this option will also add the user to the main site of your network." msgstr "" -#: inc/class-settings.php:929 +#: inc/class-settings.php:945 msgid "Add to Main Site with Role..." msgstr "" -#: inc/class-settings.php:930 +#: inc/class-settings.php:946 msgid "Select the role Ultimate Multisite should use when adding the user to the main site of your network. Be careful." msgstr "" -#: inc/class-settings.php:961 +#: inc/class-settings.php:977 msgid "Default Membership Update Page" msgstr "" -#: inc/class-settings.php:981 +#: inc/class-settings.php:997 msgid "Block Frontend Access" msgstr "" -#: inc/class-settings.php:982 +#: inc/class-settings.php:998 msgid "Block the frontend access of network sites after a membership is no longer active." msgstr "" -#: inc/class-settings.php:983 +#: inc/class-settings.php:999 msgid "By default, if a user does not pay and the account goes inactive, only the admin panel will be blocked, but the user's site will still be accessible on the frontend. If enabled, this option will also block frontend access in those cases." msgstr "" -#: inc/class-settings.php:993 +#: inc/class-settings.php:1009 msgid "Frontend Block Grace Period" msgstr "" -#: inc/class-settings.php:994 +#: inc/class-settings.php:1010 msgid "Select the number of days Ultimate Multisite should wait after the membership goes inactive before blocking the frontend access. Leave 0 to block immediately after the membership becomes inactive." msgstr "" -#: inc/class-settings.php:1008 +#: inc/class-settings.php:1024 msgid "Frontend Block Page" msgstr "" -#: inc/class-settings.php:1009 +#: inc/class-settings.php:1025 msgid "Select a page on the main site to redirect user if access is blocked" msgstr "" -#: inc/class-settings.php:1029 +#: inc/class-settings.php:1045 msgid "Enable Multiple Memberships per Customer" msgstr "" -#: inc/class-settings.php:1030 +#: inc/class-settings.php:1046 msgid "Enabling this option will allow your users to create more than one membership." msgstr "" -#: inc/class-settings.php:1040 +#: inc/class-settings.php:1056 msgid "Enable Multiple Sites per Membership" msgstr "" -#: inc/class-settings.php:1041 +#: inc/class-settings.php:1057 msgid "Enabling this option will allow your customers to create more than one site. You can limit how many sites your users can create in a per plan basis." msgstr "" -#: inc/class-settings.php:1051 +#: inc/class-settings.php:1067 msgid "Block Sites on Downgrade" msgstr "" -#: inc/class-settings.php:1052 +#: inc/class-settings.php:1068 msgid "Choose how Ultimate Multisite should handle client sites above their plan quota on downgrade." msgstr "" -#: inc/class-settings.php:1056 +#: inc/class-settings.php:1072 msgid "Keep sites as is (do nothing)" msgstr "" -#: inc/class-settings.php:1057 +#: inc/class-settings.php:1073 msgid "Block only frontend access" msgstr "" -#: inc/class-settings.php:1058 +#: inc/class-settings.php:1074 msgid "Block only backend access" msgstr "" -#: inc/class-settings.php:1059 +#: inc/class-settings.php:1075 msgid "Block both frontend and backend access" msgstr "" -#: inc/class-settings.php:1071 +#: inc/class-settings.php:1087 msgid "Move Posts on Downgrade" msgstr "" -#: inc/class-settings.php:1072 +#: inc/class-settings.php:1088 msgid "Select how you want to handle the posts above the quota on downgrade. This will apply to all post types with quotas set." msgstr "" -#: inc/class-settings.php:1076 +#: inc/class-settings.php:1092 msgid "Keep posts as is (do nothing)" msgstr "" -#: inc/class-settings.php:1077 +#: inc/class-settings.php:1093 msgid "Move posts above the new quota to the Trash" msgstr "" -#: inc/class-settings.php:1078 +#: inc/class-settings.php:1094 msgid "Mark posts above the new quota as Drafts" msgstr "" -#: inc/class-settings.php:1088 +#: inc/class-settings.php:1104 msgid "Emulated Post Types" msgstr "" -#: inc/class-settings.php:1089 +#: inc/class-settings.php:1105 msgid "Emulates the registering of a custom post type to be able to create limits for it without having to activate plugins on the main site." msgstr "" -#: inc/class-settings.php:1098 +#: inc/class-settings.php:1114 msgid "By default, Ultimate Multisite only allows super admins to limit post types that are registered on the main site. This makes sense from a technical stand-point but it also forces you to have plugins network-activated in order to be able to set limitations for their custom post types. Using this option, you can emulate the registering of a post type. This will register them on the main site and allow you to create limits for them on your products." msgstr "" -#: inc/class-settings.php:1109 +#: inc/class-settings.php:1125 msgid "Add the first post type using the button below." msgstr "" -#: inc/class-settings.php:1143 +#: inc/class-settings.php:1159 msgid "Post Type Slug" msgstr "" -#: inc/class-settings.php:1144 +#: inc/class-settings.php:1160 msgid "e.g. product" msgstr "" -#: inc/class-settings.php:1153 +#: inc/class-settings.php:1169 msgid "Post Type Label" msgstr "" -#: inc/class-settings.php:1154 +#: inc/class-settings.php:1170 msgid "e.g. Products" msgstr "" -#: inc/class-settings.php:1170 +#: inc/class-settings.php:1186 msgid "+ Add Post Type" msgstr "" -#: inc/class-settings.php:1208 +#: inc/class-settings.php:1224 msgid "Configure certain aspects of how network Sites behave." msgstr "" -#: inc/class-settings.php:1218 +#: inc/class-settings.php:1234 msgid "Default New Site Page" msgstr "" -#: inc/class-settings.php:1238 +#: inc/class-settings.php:1254 msgid "Enable Visits Limitation & Counting" msgstr "" -#: inc/class-settings.php:1239 +#: inc/class-settings.php:1255 msgid "Enabling this option will add visits limitation settings to the plans and add the functionality necessary to count site visits on the front-end." msgstr "" -#: inc/class-settings.php:1249 +#: inc/class-settings.php:1265 msgid "Enable Screenshot Generator" msgstr "" -#: inc/class-settings.php:1250 +#: inc/class-settings.php:1266 msgid "With this option is enabled, Ultimate Multisite will take a screenshot for every newly created site on your network and set the resulting image as that site's featured image. This features requires a valid license key to work and it is not supported for local sites." msgstr "" -#: inc/class-settings.php:1260 +#: inc/class-settings.php:1276 msgid "WordPress Features" msgstr "" -#: inc/class-settings.php:1261 +#: inc/class-settings.php:1277 msgid "Override default WordPress settings for network Sites." msgstr "" -#: inc/class-settings.php:1270 +#: inc/class-settings.php:1286 msgid "Enable Plugins Menu" msgstr "" -#: inc/class-settings.php:1271 +#: inc/class-settings.php:1287 msgid "Do you want to let users on the network to have access to the Plugins page, activating plugins for their sites? If this option is disabled, the customer will not be able to manage the site plugins." msgstr "" -#: inc/class-settings.php:1272 +#: inc/class-settings.php:1288 msgid "You can select which plugins the user will be able to use for each plan." msgstr "" -#: inc/class-settings.php:1282 +#: inc/class-settings.php:1298 msgid "Add New Users" msgstr "" -#: inc/class-settings.php:1283 +#: inc/class-settings.php:1299 msgid "Allow site administrators to add new users to their site via the \"Users → Add New\" page." msgstr "" -#: inc/class-settings.php:1284 +#: inc/class-settings.php:1300 msgid "You can limit the number of users allowed for each plan." msgstr "" -#: inc/class-settings.php:1294 +#: inc/class-settings.php:1310 msgid "Site Template Options" msgstr "" -#: inc/class-settings.php:1295 +#: inc/class-settings.php:1311 msgid "Configure certain aspects of how Site Templates behave." msgstr "" -#: inc/class-settings.php:1304 +#: inc/class-settings.php:1320 msgid "Allow Template Switching" msgstr "" -#: inc/class-settings.php:1305 +#: inc/class-settings.php:1321 msgid "Enabling this option will add an option on your client's dashboard to switch their site template to another one available on the catalog of available templates. The data is lost after a switch as the data from the new template is copied over." msgstr "" -#: inc/class-settings.php:1315 +#: inc/class-settings.php:1331 msgid "Allow Users to use their own Sites as Templates" msgstr "" -#: inc/class-settings.php:1316 +#: inc/class-settings.php:1332 msgid "Enabling this option will add the user own sites to the template screen, allowing them to create a new site based on the content and customizations they made previously." msgstr "" -#: inc/class-settings.php:1329 +#: inc/class-settings.php:1345 msgid "Copy Media on Template Duplication?" msgstr "" -#: inc/class-settings.php:1330 +#: inc/class-settings.php:1346 msgid "Checking this option will copy the media uploaded on the template site to the newly created site. This can be overridden on each of the plans." msgstr "" -#: inc/class-settings.php:1340 +#: inc/class-settings.php:1356 msgid "Prevent Search Engines from indexing Site Templates" msgstr "" -#: inc/class-settings.php:1341 +#: inc/class-settings.php:1357 msgid "Checking this option will discourage search engines from indexing all the Site Templates on your network." msgstr "" -#: inc/class-settings.php:1367 +#: inc/class-settings.php:1383 msgid "Payment Settings" msgstr "" -#: inc/class-settings.php:1378 +#: inc/class-settings.php:1394 msgid "Force Auto-Renew" msgstr "" -#: inc/class-settings.php:1379 +#: inc/class-settings.php:1395 msgid "Enable this option if you want to make sure memberships are created with auto-renew activated whenever the selected gateway supports it. Disabling this option will show an auto-renew option during checkout." msgstr "" -#: inc/class-settings.php:1390 +#: inc/class-settings.php:1406 msgid "Allow Trials without Payment Method" msgstr "" -#: inc/class-settings.php:1391 +#: inc/class-settings.php:1407 msgid "By default, Ultimate Multisite asks customers to add a payment method on sign-up even if a trial period is present. Enable this option to only ask for a payment method when the trial period is over." msgstr "" -#: inc/class-settings.php:1402 +#: inc/class-settings.php:1418 msgid "Send Invoice on Payment Confirmation" msgstr "" -#: inc/class-settings.php:1403 +#: inc/class-settings.php:1419 msgid "Enabling this option will attach a PDF invoice (marked paid) with the payment confirmation email. This option does not apply to the Manual Gateway, which sends invoices regardless of this option." msgstr "" -#: inc/class-settings.php:1404 +#: inc/class-settings.php:1420 msgid "The invoice files will be saved on the wp-content/uploads/wu-invoices folder." msgstr "" -#: inc/class-settings.php:1414 +#: inc/class-settings.php:1430 msgid "Invoice Numbering Scheme" msgstr "" -#: inc/class-settings.php:1415 +#: inc/class-settings.php:1431 msgid "What should Ultimate Multisite use as the invoice number?" msgstr "" -#: inc/class-settings.php:1420 +#: inc/class-settings.php:1436 msgid "Payment Reference Code" msgstr "" -#: inc/class-settings.php:1421 +#: inc/class-settings.php:1437 msgid "Sequential Number" msgstr "" -#: inc/class-settings.php:1430 +#: inc/class-settings.php:1446 msgid "Next Invoice Number" msgstr "" -#: inc/class-settings.php:1431 +#: inc/class-settings.php:1447 msgid "This number will be used as the invoice number for the next invoice generated on the system. It is incremented by one every time a new invoice is created. You can change it and save it to reset the invoice sequential number to a specific value." msgstr "" -#: inc/class-settings.php:1445 +#: inc/class-settings.php:1461 msgid "Invoice Number Prefix" msgstr "" -#: inc/class-settings.php:1446 +#: inc/class-settings.php:1462 msgid "INV00" msgstr "" #. translators: %%YEAR%%, %%MONTH%%, and %%DAY%% are placeholders but are replaced before shown to the user but are used as examples. -#: inc/class-settings.php:1448 +#: inc/class-settings.php:1464 #, php-format msgid "Use %%YEAR%%, %%MONTH%%, and %%DAY%% to create a dynamic placeholder. E.g. %%YEAR%%-%%MONTH%%-INV will become %s." msgstr "" -#: inc/class-settings.php:1462 +#: inc/class-settings.php:1478 #: inc/ui/class-jumper.php:209 msgid "Payment Gateways" msgstr "" -#: inc/class-settings.php:1463 +#: inc/class-settings.php:1479 msgid "Activate and configure the installed payment gateways in this section." msgstr "" -#: inc/class-settings.php:1478 -#: inc/class-settings.php:1479 +#: inc/class-settings.php:1494 +#: inc/class-settings.php:1495 #: inc/list-tables/class-broadcast-list-table.php:481 #: inc/list-tables/class-email-list-table.php:40 #: inc/ui/class-jumper.php:211 msgid "Emails" msgstr "" -#: inc/class-settings.php:1494 -#: inc/class-settings.php:1495 +#: inc/class-settings.php:1510 +#: inc/class-settings.php:1511 msgid "Domain Mapping" msgstr "" -#: inc/class-settings.php:1510 -#: inc/class-settings.php:1511 +#: inc/class-settings.php:1526 +#: inc/class-settings.php:1527 msgid "Single Sign-On" msgstr "" -#: inc/class-settings.php:1536 +#: inc/class-settings.php:1552 msgid "Hosting or Panel Providers" msgstr "" -#: inc/class-settings.php:1537 +#: inc/class-settings.php:1553 msgid "Configure and manage the integration with your Hosting or Panel Provider." msgstr "" -#: inc/class-settings.php:1553 +#: inc/class-settings.php:1569 msgid "Import/Export" msgstr "" -#: inc/class-settings.php:1554 +#: inc/class-settings.php:1570 msgid "Export your settings to a JSON file or import settings from a previously exported file." msgstr "" -#: inc/class-settings.php:1565 -#: inc/class-settings.php:1590 +#: inc/class-settings.php:1581 +#: inc/class-settings.php:1606 msgid "Export Settings" msgstr "" -#: inc/class-settings.php:1566 +#: inc/class-settings.php:1582 msgid "Download all your Ultimate Multisite settings as a JSON file for backup or migration purposes." msgstr "" -#: inc/class-settings.php:1578 +#: inc/class-settings.php:1594 msgid "The exported file will contain all ultimate multisite settings defined on this page. This includes general settings, payment gateway configurations, email settings, domain mapping settings, and all other plugin configurations. It does not include products, sites, domains, customers and other entities." msgstr "" -#: inc/class-settings.php:1609 +#: inc/class-settings.php:1625 msgid "Upload a previously exported JSON file to restore settings." msgstr "" -#: inc/class-settings.php:1622 +#: inc/class-settings.php:1638 msgid "Import and Replace All Settings" msgstr "" -#: inc/class-settings.php:1646 +#: inc/class-settings.php:1662 msgid "Importing settings will replace ALL current settings with the values from the uploaded file. This action cannot be undone. We recommend exporting your current settings as a backup before importing." msgstr "" -#: inc/class-settings.php:1674 +#: inc/class-settings.php:1690 msgid "Miscellaneous" msgstr "" -#: inc/class-settings.php:1675 +#: inc/class-settings.php:1691 msgid "Other options that do not fit anywhere else." msgstr "" -#: inc/class-settings.php:1686 +#: inc/class-settings.php:1702 msgid "Hide UI Tours" msgstr "" -#: inc/class-settings.php:1687 +#: inc/class-settings.php:1703 msgid "The UI tours showed by Ultimate Multisite should permanently hide themselves after being seen but if they persist for whatever reason, toggle this option to force them into their viewed state - which will prevent them from showing up again." msgstr "" -#: inc/class-settings.php:1699 +#: inc/class-settings.php:1715 msgid "Disable \"Hover to Zoom\"" msgstr "" -#: inc/class-settings.php:1700 +#: inc/class-settings.php:1716 msgid "By default, Ultimate Multisite adds a \"hover to zoom\" feature, allowing network admins to see larger version of site screenshots and other images across the UI in full-size when hovering over them. You can disable that feature here. Preview tags like the above are not affected." msgstr "" -#: inc/class-settings.php:1710 +#: inc/class-settings.php:1726 msgid "Logging" msgstr "" -#: inc/class-settings.php:1711 +#: inc/class-settings.php:1727 msgid "Log Ultimate Multisite data. This is useful for debugging purposes." msgstr "" -#: inc/class-settings.php:1720 +#: inc/class-settings.php:1736 msgid "Logging Level" msgstr "" -#: inc/class-settings.php:1721 +#: inc/class-settings.php:1737 msgid "Select the level of logging you want to use." msgstr "" -#: inc/class-settings.php:1725 +#: inc/class-settings.php:1741 msgid "PHP Default" msgstr "" -#: inc/class-settings.php:1727 +#: inc/class-settings.php:1743 msgid "Errors Only" msgstr "" -#: inc/class-settings.php:1728 +#: inc/class-settings.php:1744 msgid "Everything" msgstr "" -#: inc/class-settings.php:1737 -#: views/settings/widget-settings-body.php:278 -#: views/settings/widget-settings-body.php:283 -msgid "Help Improve Ultimate Multisite" -msgstr "" - -#. translators: %s is a link to the privacy policy -#: inc/class-settings.php:1740 -#, php-format -msgid "Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. Learn more." -msgstr "" - -#: inc/class-settings.php:1753 +#: inc/class-settings.php:1754 msgid "Change the plugin and wordpress behavior." msgstr "" -#: inc/class-settings.php:1768 +#: inc/class-settings.php:1769 msgid "Run Migration Again" msgstr "" -#: inc/class-settings.php:1770 +#: inc/class-settings.php:1771 msgid "Rerun the Migration Wizard if you experience data-loss after migrate." msgstr "" -#: inc/class-settings.php:1773 +#: inc/class-settings.php:1774 msgid "Important: This process can have unexpected behavior with your current Ultimo models.
We recommend that you create a backup before continue." msgstr "" -#: inc/class-settings.php:1776 +#: inc/class-settings.php:1777 msgid "Migrate" msgstr "" -#: inc/class-settings.php:1799 +#: inc/class-settings.php:1800 msgid "Security Mode" msgstr "" #. Translators: Placeholder adds the security mode key and current site url with query string -#: inc/class-settings.php:1801 +#: inc/class-settings.php:1802 #, php-format msgid "Only Ultimate Multisite and other must-use plugins will run on your WordPress install while this option is enabled.
Important: Copy the following URL to disable security mode if something goes wrong and this page becomes unavailable:%2$s
" msgstr "" -#: inc/class-settings.php:1812 +#: inc/class-settings.php:1813 msgid "Remove Data on Uninstall" msgstr "" -#: inc/class-settings.php:1813 +#: inc/class-settings.php:1814 msgid "Remove all saved data for Ultimate Multisite when the plugin is uninstalled." msgstr "" @@ -9508,12 +9724,6 @@ msgstr "" msgid "Quarterly" msgstr "" -#: inc/compat/class-legacy-shortcodes.php:362 -#: inc/models/class-checkout-form.php:851 -#: views/legacy/signup/pricing-table/frequency-selector.php:33 -msgid "Yearly" -msgstr "" - #: inc/compat/class-legacy-shortcodes.php:372 #: inc/list-tables/class-product-list-table.php:313 #: inc/models/class-checkout-form.php:551 @@ -14926,20 +15136,6 @@ msgstr "" msgid "We were not able to find a user with the given user_id." msgstr "" -#: inc/functions/date.php:115 -#: views/dashboard-statistics/widget-tax-by-day.php:19 -#: views/dashboard-statistics/widget-tax-by-day.php:52 -msgid "Day" -msgstr "" - -#: inc/functions/date.php:118 -msgid "Month" -msgstr "" - -#: inc/functions/date.php:121 -msgid "Year" -msgstr "" - #. translators: %s: date. #: inc/functions/date.php:148 #, php-format @@ -15343,7 +15539,8 @@ msgstr "" #: inc/managers/class-form-manager.php:499 #: inc/ui/class-billing-info-element.php:383 #: inc/ui/class-current-site-element.php:487 -#: inc/ui/class-site-actions-element.php:698 +#: inc/ui/class-site-actions-element.php:725 +#: inc/ui/class-site-actions-element.php:841 msgid "Something went wrong." msgstr "" @@ -15682,7 +15879,7 @@ msgid "Oops! Your %1$s and %2$s don’t match." msgstr "" #: inc/helpers/class-validator.php:97 -#: inc/models/class-discount-code.php:670 +#: inc/models/class-discount-code.php:745 #: views/base/filter.php:123 #: views/base/filter.php:131 #: views/wizards/host-integrations/cloudflare-instructions.php:14 @@ -17344,7 +17541,7 @@ msgstr "" #: inc/list-tables/class-membership-list-table.php:139 #: inc/list-tables/class-product-list-table.php:146 #: inc/models/class-membership.php:874 -#: inc/models/class-product.php:838 +#: inc/models/class-product.php:901 #, php-format msgid "every %2$s" msgid_plural "every %1$s %2$s" @@ -17356,7 +17553,7 @@ msgstr[1] "" #: inc/list-tables/class-membership-list-table.php:147 #: inc/list-tables/class-product-list-table.php:154 #: inc/models/class-membership.php:926 -#: inc/models/class-product.php:795 +#: inc/models/class-product.php:858 #, php-format msgid "for %s cycle" msgid_plural "for %s cycles" @@ -18322,9 +18519,9 @@ msgstr "" #: inc/managers/class-membership-manager.php:387 #: inc/managers/class-payment-manager.php:336 #: inc/managers/class-payment-manager.php:381 -#: inc/ui/class-site-actions-element.php:594 -#: inc/ui/class-site-actions-element.php:940 -#: inc/ui/class-site-actions-element.php:1132 +#: inc/ui/class-site-actions-element.php:621 +#: inc/ui/class-site-actions-element.php:1112 +#: inc/ui/class-site-actions-element.php:1304 msgid "An unexpected error happened." msgstr "" @@ -18556,25 +18753,29 @@ msgstr "" msgid "none" msgstr "" -#: inc/models/class-discount-code.php:478 -#: inc/models/class-discount-code.php:500 #: inc/models/class-discount-code.php:508 -#: inc/models/class-discount-code.php:526 +#: inc/models/class-discount-code.php:530 +#: inc/models/class-discount-code.php:538 +#: inc/models/class-discount-code.php:555 msgid "This coupon code is not valid." msgstr "" -#: inc/models/class-discount-code.php:485 +#: inc/models/class-discount-code.php:515 msgid "This discount code was already redeemed the maximum amount of times allowed." msgstr "" +#: inc/models/class-discount-code.php:567 +msgid "This coupon code is not valid for the selected billing period." +msgstr "" + #. translators: placeholder is the value off. Can be wither $X.XX or X% -#: inc/models/class-discount-code.php:651 +#: inc/models/class-discount-code.php:726 #, php-format msgid "%1$s OFF on Subscriptions" msgstr "" #. translators: placeholder is the value off. Can be wither $X.XX or X% -#: inc/models/class-discount-code.php:665 +#: inc/models/class-discount-code.php:740 #, php-format msgid "%1$s OFF on Setup Fees" msgstr "" @@ -18622,7 +18823,7 @@ msgstr "" #. translators: %1$s is the formatted price, %2$s the duration, and %3$s the duration unit (day, week, month, etc) #: inc/models/class-membership.php:915 -#: inc/models/class-product.php:784 +#: inc/models/class-product.php:847 #, php-format msgid "%1$s every %3$s" msgid_plural "%1$s every %2$s %3$s" @@ -18631,7 +18832,7 @@ msgstr[1] "" #. translators: %1$s is the formatted price of the product #: inc/models/class-membership.php:935 -#: inc/models/class-product.php:804 +#: inc/models/class-product.php:867 #, php-format msgid "%1$s one time payment" msgstr "" @@ -18654,17 +18855,27 @@ msgstr "" msgid "Processed on %s" msgstr "" -#: inc/models/class-product.php:772 +#. translators: %s is the minimum amount formatted as currency +#: inc/models/class-product.php:759 +#, php-format +msgid "From %s" +msgstr "" + +#: inc/models/class-product.php:762 +msgid "Name Your Price" +msgstr "" + +#: inc/models/class-product.php:835 msgid "Contact us" msgstr "" #. translators: %1$s is the formatted price of the setup fee -#: inc/models/class-product.php:812 +#: inc/models/class-product.php:875 #, php-format msgid "Setup Fee of %1$s" msgstr "" -#: inc/models/class-product.php:833 +#: inc/models/class-product.php:896 msgid "one-time payment" msgstr "" @@ -18819,7 +19030,7 @@ msgid "Regular" msgstr "" #: inc/tax/class-tax.php:231 -#: inc/ui/class-site-actions-element.php:185 +#: inc/ui/class-site-actions-element.php:193 #: views/limitations/plugin-selector.php:80 msgid "Default" msgstr "" @@ -18914,7 +19125,7 @@ msgstr "" #: inc/traits/trait-wp-ultimo-settings-deprecated.php:79 #: inc/traits/trait-wp-ultimo-settings-deprecated.php:80 -#: inc/ui/class-site-actions-element.php:1050 +#: inc/ui/class-site-actions-element.php:1222 #: views/dashboard-statistics/widget-countries.php:95 msgid "Other" msgstr "" @@ -19137,8 +19348,8 @@ msgid "How many columns to use." msgstr "" #: inc/ui/class-current-membership-element.php:368 -#: inc/ui/class-site-actions-element.php:842 -#: inc/ui/class-site-actions-element.php:993 +#: inc/ui/class-site-actions-element.php:1014 +#: inc/ui/class-site-actions-element.php:1165 msgid "Membership not selected." msgstr "" @@ -19148,12 +19359,12 @@ msgstr "" #: inc/ui/class-current-membership-element.php:380 #: inc/ui/class-current-membership-element.php:535 -#: inc/ui/class-site-actions-element.php:503 -#: inc/ui/class-site-actions-element.php:600 -#: inc/ui/class-site-actions-element.php:848 -#: inc/ui/class-site-actions-element.php:950 -#: inc/ui/class-site-actions-element.php:999 -#: inc/ui/class-site-actions-element.php:1142 +#: inc/ui/class-site-actions-element.php:530 +#: inc/ui/class-site-actions-element.php:627 +#: inc/ui/class-site-actions-element.php:1020 +#: inc/ui/class-site-actions-element.php:1122 +#: inc/ui/class-site-actions-element.php:1171 +#: inc/ui/class-site-actions-element.php:1314 msgid "You are not allowed to do this." msgstr "" @@ -19733,172 +19944,225 @@ msgid "Toggle to show/hide the password link." msgstr "" #: inc/ui/class-site-actions-element.php:163 -msgid "Show Change Default Site" +msgid "Show Change Email" msgstr "" #: inc/ui/class-site-actions-element.php:164 -msgid "Toggle to show/hide the change default site link." +msgid "Toggle to show/hide the change email link." msgstr "" #: inc/ui/class-site-actions-element.php:171 -msgid "Show Change Payment Method" +msgid "Show Change Default Site" msgstr "" #: inc/ui/class-site-actions-element.php:172 +msgid "Toggle to show/hide the change default site link." +msgstr "" + +#: inc/ui/class-site-actions-element.php:179 +msgid "Show Change Payment Method" +msgstr "" + +#: inc/ui/class-site-actions-element.php:180 msgid "Toggle to show/hide the option to cancel the current payment method." msgstr "" -#: inc/ui/class-site-actions-element.php:193 +#: inc/ui/class-site-actions-element.php:201 msgid "Redirect After Delete" msgstr "" -#: inc/ui/class-site-actions-element.php:195 +#: inc/ui/class-site-actions-element.php:203 msgid "The page to redirect user after delete current site." msgstr "" -#: inc/ui/class-site-actions-element.php:381 +#: inc/ui/class-site-actions-element.php:399 msgid "Change Site Template" msgstr "" -#: inc/ui/class-site-actions-element.php:394 -#: inc/ui/class-site-actions-element.php:772 +#: inc/ui/class-site-actions-element.php:412 +#: inc/ui/class-site-actions-element.php:944 msgid "Change Default Site" msgstr "" -#: inc/ui/class-site-actions-element.php:403 +#: inc/ui/class-site-actions-element.php:421 msgid "Change Password" msgstr "" -#: inc/ui/class-site-actions-element.php:414 +#: inc/ui/class-site-actions-element.php:430 +#: inc/ui/class-site-actions-element.php:805 +msgid "Change Email" +msgstr "" + +#: inc/ui/class-site-actions-element.php:441 msgid "Cancel Current Payment Method" msgstr "" -#: inc/ui/class-site-actions-element.php:465 -#: inc/ui/class-site-actions-element.php:1089 -#: inc/ui/class-site-actions-element.php:1090 +#: inc/ui/class-site-actions-element.php:492 +#: inc/ui/class-site-actions-element.php:1261 +#: inc/ui/class-site-actions-element.php:1262 msgid "Cancel Membership" msgstr "" -#: inc/ui/class-site-actions-element.php:497 +#: inc/ui/class-site-actions-element.php:524 msgid "Site not selected." msgstr "" -#: inc/ui/class-site-actions-element.php:540 +#: inc/ui/class-site-actions-element.php:567 msgid "Confirm Site Deletion" msgstr "" -#: inc/ui/class-site-actions-element.php:646 +#: inc/ui/class-site-actions-element.php:673 +#: inc/ui/class-site-actions-element.php:789 msgid "Current Password" msgstr "" -#: inc/ui/class-site-actions-element.php:647 -#: inc/ui/class-site-actions-element.php:652 -#: inc/ui/class-site-actions-element.php:657 +#: inc/ui/class-site-actions-element.php:674 +#: inc/ui/class-site-actions-element.php:679 +#: inc/ui/class-site-actions-element.php:684 +#: inc/ui/class-site-actions-element.php:790 msgid "******" msgstr "" -#: inc/ui/class-site-actions-element.php:651 +#: inc/ui/class-site-actions-element.php:678 msgid "New Password" msgstr "" -#: inc/ui/class-site-actions-element.php:658 +#: inc/ui/class-site-actions-element.php:685 msgid "Confirm New Password" msgstr "" -#: inc/ui/class-site-actions-element.php:662 +#: inc/ui/class-site-actions-element.php:689 msgid "Reset Password" msgstr "" -#: inc/ui/class-site-actions-element.php:706 +#: inc/ui/class-site-actions-element.php:733 +#: inc/ui/class-site-actions-element.php:849 msgid "Your current password is wrong." msgstr "" -#: inc/ui/class-site-actions-element.php:715 +#: inc/ui/class-site-actions-element.php:742 msgid "The new password must be at least 6 characters long." msgstr "" -#: inc/ui/class-site-actions-element.php:721 +#: inc/ui/class-site-actions-element.php:748 msgid "New passwords do not match." msgstr "" -#: inc/ui/class-site-actions-element.php:762 +#: inc/ui/class-site-actions-element.php:781 +msgid "Current Email" +msgstr "" + +#: inc/ui/class-site-actions-element.php:791 +msgid "Enter your password to confirm this change." +msgstr "" + +#: inc/ui/class-site-actions-element.php:795 +msgid "New Email" +msgstr "" + +#: inc/ui/class-site-actions-element.php:796 +#: inc/ui/class-site-actions-element.php:800 +msgid "newemail@example.com" +msgstr "" + +#: inc/ui/class-site-actions-element.php:801 +msgid "Confirm New Email" +msgstr "" + +#: inc/ui/class-site-actions-element.php:858 +msgid "Please enter a valid email address." +msgstr "" + +#: inc/ui/class-site-actions-element.php:864 +msgid "Email addresses do not match." +msgstr "" + +#: inc/ui/class-site-actions-element.php:870 +msgid "The new email address is the same as your current email." +msgstr "" + +#: inc/ui/class-site-actions-element.php:879 +msgid "This email address is already in use." +msgstr "" + +#: inc/ui/class-site-actions-element.php:934 msgid "Primary Site" msgstr "" -#: inc/ui/class-site-actions-element.php:763 +#: inc/ui/class-site-actions-element.php:935 msgid "Change the primary site of your network." msgstr "" -#: inc/ui/class-site-actions-element.php:824 +#: inc/ui/class-site-actions-element.php:996 msgid "You need to select a new primary site." msgstr "" -#: inc/ui/class-site-actions-element.php:885 +#: inc/ui/class-site-actions-element.php:1057 msgid "Confirm Payment Method Cancellation" msgstr "" -#: inc/ui/class-site-actions-element.php:897 -#: inc/ui/class-site-actions-element.php:898 +#: inc/ui/class-site-actions-element.php:1069 +#: inc/ui/class-site-actions-element.php:1070 msgid "Cancel Payment Method" msgstr "" -#: inc/ui/class-site-actions-element.php:1036 +#: inc/ui/class-site-actions-element.php:1208 msgid "Please tell us why you are cancelling." msgstr "" -#: inc/ui/class-site-actions-element.php:1037 +#: inc/ui/class-site-actions-element.php:1209 msgid "We would love your feedback." msgstr "" -#: inc/ui/class-site-actions-element.php:1043 +#: inc/ui/class-site-actions-element.php:1215 msgid "Select a reason" msgstr "" -#: inc/ui/class-site-actions-element.php:1044 -#: inc/ui/class-site-actions-element.php:1150 +#: inc/ui/class-site-actions-element.php:1216 +#: inc/ui/class-site-actions-element.php:1322 msgid "I no longer need it" msgstr "" -#: inc/ui/class-site-actions-element.php:1045 -#: inc/ui/class-site-actions-element.php:1151 +#: inc/ui/class-site-actions-element.php:1217 +#: inc/ui/class-site-actions-element.php:1323 msgid "It's too expensive" msgstr "" -#: inc/ui/class-site-actions-element.php:1046 -#: inc/ui/class-site-actions-element.php:1152 +#: inc/ui/class-site-actions-element.php:1218 +#: inc/ui/class-site-actions-element.php:1324 msgid "I need more features" msgstr "" -#: inc/ui/class-site-actions-element.php:1047 -#: inc/ui/class-site-actions-element.php:1153 +#: inc/ui/class-site-actions-element.php:1219 +#: inc/ui/class-site-actions-element.php:1325 msgid "Switched to another service" msgstr "" -#: inc/ui/class-site-actions-element.php:1048 -#: inc/ui/class-site-actions-element.php:1154 +#: inc/ui/class-site-actions-element.php:1220 +#: inc/ui/class-site-actions-element.php:1326 msgid "Customer support is less than expected" msgstr "" -#: inc/ui/class-site-actions-element.php:1049 -#: inc/ui/class-site-actions-element.php:1155 +#: inc/ui/class-site-actions-element.php:1221 +#: inc/ui/class-site-actions-element.php:1327 msgid "Too complex" msgstr "" -#: inc/ui/class-site-actions-element.php:1055 +#: inc/ui/class-site-actions-element.php:1227 msgid "Please provide additional details." msgstr "" -#: inc/ui/class-site-actions-element.php:1066 +#: inc/ui/class-site-actions-element.php:1238 msgid "Type CANCEL to confirm this membership cancellation." msgstr "" #. translators: %s: Next charge date. -#: inc/ui/class-site-actions-element.php:1083 +#: inc/ui/class-site-actions-element.php:1255 #, php-format msgid "Your sites will stay working until %s." msgstr "" -#: inc/ui/class-site-actions-element.php:1095 +#: inc/ui/class-site-actions-element.php:1267 msgid "CANCEL" msgstr "" @@ -20603,6 +20867,14 @@ msgstr "" msgid "Select Plan" msgstr "" +#: views/checkout/templates/pricing-table/list.php:84 +msgid "Make this a recurring payment" +msgstr "" + +#: views/checkout/templates/pricing-table/list.php:87 +msgid "(Recurring subscription)" +msgstr "" + #: views/checkout/templates/steps/clean.php:25 #: views/checkout/templates/steps/minimal.php:25 msgid "Progress" diff --git a/package.json b/package.json index c1296d1c..77f3c5e6 100644 --- a/package.json +++ b/package.json @@ -36,9 +36,10 @@ "prebuild": "npm run makepot && composer install -o --no-dev", "prebuild:dev": "composer install", "copylibs": "node scripts/copy-libs.js", - "build": "npm-run-all --parallel copylibs uglify cleancss", + "build": "npm-run-all --parallel copylibs uglify cleancss generate:hooks", "postbuild": "npm run archive", - "build:dev": "npm run copylibs && npm run uglify && npm run cleancss && npm run makepot", + "generate:hooks": "php inc/documentation/generate-berlindb-hooks.php", + "build:dev": "npm run copylibs && npm run uglify && npm run cleancss && npm run makepot && npm run generate:hooks", "build:translate": "npm run copylibs && npm run uglify && npm run cleancss && npm run makepot && npm run translate", "prearchive": "php encrypt-secrets.php", "archive": "node scripts/archive.js", From a3b1bfd9ae04acc9e07e23fe1d36270550ffc2b9 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 3 Feb 2026 08:25:13 -0700 Subject: [PATCH 21/32] Skip billing validation when payment not required, code style fixes - Add should_collect_payment() method to Checkout class to determine payment requirements earlier in the flow - Only validate billing address when payment is actually being collected - Apply WordPress code style fixes to hooks documentation generator Co-Authored-By: Claude Opus 4.5 --- inc/checkout/class-checkout.php | 51 +++++-- inc/documentation/generate-berlindb-hooks.php | 130 ++++++++++-------- inc/stuff.php | 8 +- lang/ultimate-multisite.pot | 56 ++++---- .../email-smtp-test/email-smtp-test.php | 4 +- 5 files changed, 150 insertions(+), 99 deletions(-) diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index b167d76c..4a0a348f 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -1095,16 +1095,14 @@ protected function maybe_create_customer() { $billing_address->load_attributes_from_post($session); /* - * Validates the address. + * Validates the address when payment is being collected. */ - $valid_address = $billing_address->validate(); + if ($this->should_collect_payment()) { + $valid_address = $billing_address->validate(); - /* - * There's something invalid on the address, - * bail with the errors. - */ - if (is_wp_error($valid_address)) { - return $valid_address; + if (is_wp_error($valid_address)) { + return $valid_address; + } } $customer->set_billing_address($billing_address); @@ -1957,6 +1955,41 @@ public function get_checkout_variables() { return apply_filters('wu_get_checkout_variables', $variables, $this); } + /** + * Determines whether payment should be collected for the current checkout. + * + * Uses $this->order if available, otherwise builds a temporary Cart + * from the request/session data to check. + * + * @since 2.0.20 + * @return bool + */ + public function should_collect_payment(): bool { + + if ($this->order) { + return $this->order->should_collect_payment(); + } + + $products = $this->request_or_session('products', []); + + if (empty($products)) { + return true; + } + + try { + $cart = new Cart( + [ + 'products' => (array) $products, + 'country' => $this->request_or_session('billing_country'), + ] + ); + + return $cart->should_collect_payment(); + } catch (\Throwable $e) { + return true; + } + } + /** * Returns the validation rules for the fields. * @@ -2073,7 +2106,7 @@ public function get_validation_rules() { * Remove billing field requirements when payment is not needed * (e.g. free trials with allow_trial_without_payment_method enabled). */ - if ($this->order && $this->order->should_collect_payment() === false) { + if ( ! $this->should_collect_payment()) { $validation_rules['billing_country'] = ''; $validation_rules['billing_zip_code'] = ''; $validation_rules['billing_state'] = ''; diff --git a/inc/documentation/generate-berlindb-hooks.php b/inc/documentation/generate-berlindb-hooks.php index 5698641f..ab2b80d4 100644 --- a/inc/documentation/generate-berlindb-hooks.php +++ b/inc/documentation/generate-berlindb-hooks.php @@ -24,7 +24,7 @@ // We need the autoloader to resolve class references. $autoloader = $plugin_root . '/vendor/autoload.php'; -if (!file_exists($autoloader)) { +if (! file_exists($autoloader)) { fwrite(STDERR, "Composer autoloader not found at {$autoloader}\n"); fwrite(STDERR, "Run `composer install` inside {$plugin_root} first.\n"); exit(1); @@ -33,7 +33,7 @@ require_once $autoloader; // Minimal stubs so schema classes can load outside of WordPress. -if (!defined('ABSPATH')) { +if (! defined('ABSPATH')) { define('ABSPATH', '/'); } @@ -89,9 +89,12 @@ function find_line_number(string $file, string $pattern): int { } // Skip the engine base class. -$query_files = array_filter($query_files, function ($file) { - return strpos($file, '/engine/') === false; -}); +$query_files = array_filter( + $query_files, + function ($file) { + return strpos($file, '/engine/') === false; + } +); // ── Extract metadata from each query class ────────────────────────────────── @@ -109,19 +112,19 @@ function extract_class_properties(string $file, array $properties): array { foreach ($properties as $prop) { // Match: protected $prop = 'value'; - // protected $prop = SomeClass::class; + // protected $prop = SomeClass::class; if (preg_match('/protected\s+\$' . preg_quote($prop, '/') . '\s*=\s*(.+?);/s', $source, $m)) { $raw = trim($m[1]); // String literal if (preg_match("/^['\"](.+?)['\"]$/", $raw, $sm)) { - $result[$prop] = $sm[1]; + $result[ $prop ] = $sm[1]; } // Class reference (Foo::class or \Foo\Bar::class) elseif (preg_match('/^(.+?)::class$/', $raw, $cm)) { - $result[$prop] = trim($cm[1], '\\'); + $result[ $prop ] = trim($cm[1], '\\'); } else { - $result[$prop] = $raw; + $result[ $prop ] = $raw; } } } @@ -140,14 +143,14 @@ function extract_schema_columns(string $file): array { $source = file_get_contents($file); // Extract the $columns array block. - if (!preg_match('/\$columns\s*=\s*\[(.+)\];/s', $source, $m)) { + if (! preg_match('/\$columns\s*=\s*\[(.+)\];/s', $source, $m)) { return []; } - $columns_block = $m[1]; - $all_columns = []; + $columns_block = $m[1]; + $all_columns = []; $transition_columns = []; - $seen_names = []; + $seen_names = []; // Split into individual column arrays. // Each column is delimited by [ ... ], @@ -171,12 +174,12 @@ function extract_schema_columns(string $file): array { } // Deduplicate columns (some schemas define the same column twice). - if (isset($seen_names[$col['name']])) { + if (isset($seen_names[ $col['name'] ])) { continue; } - $seen_names[$col['name']] = true; - $all_columns[] = $col; + $seen_names[ $col['name'] ] = true; + $all_columns[] = $col; // Check for transition => true if (preg_match("/'transition'\s*=>\s*true/", $entry)) { @@ -201,7 +204,7 @@ function resolve_schema_file(string $class_fqn, string $plugin_root): ?string { // WP_Ultimo\Database\Memberships\Memberships_Schema // → inc/database/memberships/class-memberships-schema.php - $parts = explode('\\', $class_fqn); + $parts = explode('\\', $class_fqn); $class_name = array_pop($parts); // Memberships_Schema // Convert class name to filename: Memberships_Schema → class-memberships-schema.php @@ -209,7 +212,7 @@ function resolve_schema_file(string $class_fqn, string $plugin_root): ?string { // Build the directory path from namespace parts after WP_Ultimo // WP_Ultimo\Database\Memberships → inc/database/memberships - $ns_path = ''; + $ns_path = ''; $found_root = false; foreach ($parts as $part) { @@ -274,12 +277,15 @@ function humanize(string $item_name): string { $models = []; foreach ($query_files as $query_file) { - $props = extract_class_properties($query_file, [ - 'item_name', - 'item_name_plural', - 'table_name', - 'table_schema', - ]); + $props = extract_class_properties( + $query_file, + [ + 'item_name', + 'item_name_plural', + 'table_name', + 'table_schema', + ] + ); if (empty($props['item_name']) || empty($props['item_name_plural'])) { fwrite(STDERR, "Skipping {$query_file}: missing item_name or item_name_plural\n"); @@ -287,9 +293,12 @@ function humanize(string $item_name): string { } $schema_file = null; - $columns = ['all' => [], 'transition' => []]; + $columns = [ + 'all' => [], + 'transition' => [], + ]; - if (!empty($props['table_schema'])) { + if (! empty($props['table_schema'])) { $schema_file = resolve_schema_file($props['table_schema'], $plugin_root); if ($schema_file) { @@ -304,11 +313,11 @@ function humanize(string $item_name): string { // Deduplicate: Broadcast_Query, Email_Query, and Post_Query all share // item_name = 'post'. We only need one set of hooks per unique // item_name/item_name_plural combination. - if (isset($models[$key])) { + if (isset($models[ $key ])) { continue; } - $models[$key] = [ + $models[ $key ] = [ 'item_name' => $props['item_name'], 'item_name_plural' => $props['item_name_plural'], 'table_name' => $props['table_name'] ?? $props['item_name_plural'], @@ -321,12 +330,21 @@ function humanize(string $item_name): string { // Sort by item name for consistent output. ksort($models); -fwrite(STDERR, sprintf("Found %d unique models with %d total transition columns\n", - count($models), - array_sum(array_map(function ($m) { - return count($m['transition_cols']); - }, $models)) -)); +fwrite( + STDERR, + sprintf( + "Found %d unique models with %d total transition columns\n", + count($models), + array_sum( + array_map( + function ($m) { + return count($m['transition_cols']); + }, + $models + ) + ) + ) +); // ── Generate the output file ──────────────────────────────────────────────── @@ -341,7 +359,7 @@ function humanize(string $item_name): string { // Build the @see line for transition hooks. $transition_see = $hook_lines['transition'] ? "see {$src}:{$hook_lines['transition']}" : "see {$src}"; -echo <<
'm8OHTALz1+lpJmH7wOZMqm5BZ09XRlpUdE9UK2RMN0dqbzNobk1OMk1XQUQwcDl5U1R5aHkxZlJHTmQrOExyZlV1WktGcTJlWTlGaXhxOWU=', - 1 => '+11XF9OjjG7eYuLSg8bnKzRIMHh4akl3WFNNdzZ0UzkwbFd6SDBCeDhoNFhSVm5OQUhtN01VOWNCN2JZRFhmcVFENWFzZlVsakVxUmFYSFM=', -); \ No newline at end of file +return array( + 0 => 'm8OHTALz1+lpJmH7wOZMqm5BZ09XRlpUdE9UK2RMN0dqbzNobk1OMk1XQUQwcDl5U1R5aHkxZlJHTmQrOExyZlV1WktGcTJlWTlGaXhxOWU=', + 1 => '+11XF9OjjG7eYuLSg8bnKzRIMHh4akl3WFNNdzZ0UzkwbFd6SDBCeDhoNFhSVm5OQUhtN01VOWNCN2JZRFhmcVFENWFzZlVsakVxUmFYSFM=', +); diff --git a/lang/ultimate-multisite.pot b/lang/ultimate-multisite.pot index 400896fd..f86e8a62 100644 --- a/lang/ultimate-multisite.pot +++ b/lang/ultimate-multisite.pot @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-02-02T16:16:01+00:00\n" +"POT-Creation-Date: 2026-02-03T06:01:34+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: ultimate-multisite\n" @@ -359,7 +359,7 @@ msgstr "" #: inc/admin-pages/class-broadcast-edit-admin-page.php:370 #: inc/admin-pages/class-edit-admin-page.php:489 #: inc/admin-pages/class-tax-rates-admin-page.php:193 -#: inc/checkout/class-checkout.php:1812 +#: inc/checkout/class-checkout.php:1810 #: views/base/edit/widget-tabs.php:106 #: views/dashboard-statistics/filter.php:28 #: views/dashboard-statistics/widget-mrr-growth.php:16 @@ -3053,7 +3053,7 @@ msgstr "" #: inc/admin-pages/class-product-list-admin-page.php:98 #: inc/admin-pages/class-product-list-admin-page.php:109 #: inc/admin-pages/class-top-admin-nav-menu.php:103 -#: inc/checkout/class-checkout.php:2127 +#: inc/checkout/class-checkout.php:2171 #: inc/checkout/signup-fields/class-signup-field-pricing-table.php:170 #: inc/checkout/signup-fields/class-signup-field-products.php:149 #: inc/checkout/signup-fields/class-signup-field-products.php:150 @@ -7136,8 +7136,8 @@ msgstr "" #: inc/checkout/class-checkout.php:752 #: inc/checkout/class-checkout.php:760 -#: inc/checkout/class-checkout.php:2351 -#: inc/checkout/class-checkout.php:2357 +#: inc/checkout/class-checkout.php:2395 +#: inc/checkout/class-checkout.php:2401 msgid "Payment gateway not registered." msgstr "" @@ -7145,103 +7145,103 @@ msgstr "" msgid "The email address you entered is already in use." msgstr "" -#: inc/checkout/class-checkout.php:1119 +#: inc/checkout/class-checkout.php:1117 msgid "Something wrong happened while attempting to save the customer billing address" msgstr "" -#: inc/checkout/class-checkout.php:1676 +#: inc/checkout/class-checkout.php:1674 msgid "Invalid request" msgstr "" -#: inc/checkout/class-checkout.php:1688 +#: inc/checkout/class-checkout.php:1686 msgid "Too many requests. Please try again later." msgstr "" -#: inc/checkout/class-checkout.php:1730 +#: inc/checkout/class-checkout.php:1728 msgid "Please provide both username/email and password." msgstr "" -#: inc/checkout/class-checkout.php:1743 +#: inc/checkout/class-checkout.php:1741 msgid "Too many login attempts. Please try again in a few minutes." msgstr "" -#: inc/checkout/class-checkout.php:1772 +#: inc/checkout/class-checkout.php:1770 msgid "Invalid username or password." msgstr "" -#: inc/checkout/class-checkout.php:1790 +#: inc/checkout/class-checkout.php:1788 msgid "Login successful!" msgstr "" -#: inc/checkout/class-checkout.php:1813 +#: inc/checkout/class-checkout.php:1811 msgid "The item was added!" msgstr "" -#: inc/checkout/class-checkout.php:1814 +#: inc/checkout/class-checkout.php:1812 msgid "The Password entered is too weak." msgstr "" -#: inc/checkout/class-checkout.php:1815 +#: inc/checkout/class-checkout.php:1813 msgid "Password is required" msgstr "" -#: inc/checkout/class-checkout.php:1816 +#: inc/checkout/class-checkout.php:1814 msgid "Login failed. Please try again." msgstr "" -#: inc/checkout/class-checkout.php:1817 +#: inc/checkout/class-checkout.php:1815 msgid "Logging in..." msgstr "" -#: inc/checkout/class-checkout.php:1818 +#: inc/checkout/class-checkout.php:1816 #: views/checkout/partials/inline-login-prompt.php:17 msgid "Already have an account?" msgstr "" -#: inc/checkout/class-checkout.php:1819 +#: inc/checkout/class-checkout.php:1817 #: views/checkout/partials/inline-login-prompt.php:59 msgid "Sign in" msgstr "" -#: inc/checkout/class-checkout.php:1820 +#: inc/checkout/class-checkout.php:1818 #: views/checkout/partials/inline-login-prompt.php:51 msgid "Forgot password?" msgstr "" -#: inc/checkout/class-checkout.php:1821 +#: inc/checkout/class-checkout.php:1819 #: inc/list-tables/class-payment-list-table.php:111 #: views/dashboard-widgets/current-membership.php:315 msgid "Cancel" msgstr "" -#: inc/checkout/class-checkout.php:2123 +#: inc/checkout/class-checkout.php:2167 msgid "Password confirmation" msgstr "" -#: inc/checkout/class-checkout.php:2124 +#: inc/checkout/class-checkout.php:2168 msgid "Email confirmation" msgstr "" -#: inc/checkout/class-checkout.php:2125 +#: inc/checkout/class-checkout.php:2169 msgid "Template ID" msgstr "" -#: inc/checkout/class-checkout.php:2126 +#: inc/checkout/class-checkout.php:2170 msgid "Valid password" msgstr "" -#: inc/checkout/class-checkout.php:2128 +#: inc/checkout/class-checkout.php:2172 msgid "Payment Gateway" msgstr "" #. translators: %s payment id. -#: inc/checkout/class-checkout.php:2320 +#: inc/checkout/class-checkout.php:2364 #, php-format msgid "Payment (%s) not found." msgstr "" #. translators: %s is the membership ID -#: inc/checkout/class-checkout.php:2455 +#: inc/checkout/class-checkout.php:2499 #, php-format msgid "Checkout failed for customer %s: " msgstr "" diff --git a/mu-plugins/email-smtp-test/email-smtp-test.php b/mu-plugins/email-smtp-test/email-smtp-test.php index 041c421e..2e88785c 100644 --- a/mu-plugins/email-smtp-test/email-smtp-test.php +++ b/mu-plugins/email-smtp-test/email-smtp-test.php @@ -21,7 +21,7 @@ function TestConfigMailpit($phpmailer) { $phpmailer->From = 'test@example.local'; $phpmailer->FromName = 'Test Site'; - // Uncomment to enable SMTP debug output (helpful for troubleshooting) - $phpmailer->SMTPDebug = 2; + // SMTP debug output disabled to prevent corruption of AJAX responses + $phpmailer->SMTPDebug = 0; } add_action('phpmailer_init', 'TestConfigMailpit', 10, 1); From d181e9427d71290c5e77268d08a09e5f1324c5d1 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 3 Feb 2026 11:10:00 -0700 Subject: [PATCH 22/32] Apply suggestion from @coderabbitai[bot] Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- inc/documentation/generate-berlindb-hooks.php | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/inc/documentation/generate-berlindb-hooks.php b/inc/documentation/generate-berlindb-hooks.php index ab2b80d4..6d6c944a 100644 --- a/inc/documentation/generate-berlindb-hooks.php +++ b/inc/documentation/generate-berlindb-hooks.php @@ -51,32 +51,24 @@ */ function find_line_number(string $file, string $pattern): int { - static $lines = null; + static $cache = []; - if ($lines === null) { - $lines = file($file); - } + if (! is_readable($file)) { + return 0; + } - foreach ($lines as $i => $line) { - if (preg_match($pattern, $line)) { - return $i + 1; - } - } + if (! isset($cache[$file])) { + $cache[$file] = file($file, FILE_IGNORE_NEW_LINES); + } - return 0; -} + foreach ($cache[$file] as $i => $line) { + if (preg_match($pattern, $line)) { + return $i + 1; + } + } -// Pre-compute line numbers for each hook pattern in Query.php. -$hook_lines = [ - 'transition' => find_line_number($query_php_absolute, '/do_action\(\s*\$key_action/'), - 'pre_get' => find_line_number($query_php_absolute, '/pre_get_.*item_name_plural/'), - 'parse_query' => find_line_number($query_php_absolute, '/parse_.*item_name_plural.*_query/'), - 'query_clauses' => find_line_number($query_php_absolute, '/item_name_plural.*_query_clauses/'), - 'search_columns' => find_line_number($query_php_absolute, '/item_name_plural.*_search_columns/'), - 'the_items' => find_line_number($query_php_absolute, '/the_.*item_name_plural/'), - 'filter_item' => find_line_number($query_php_absolute, '/filter_.*item_name.*_item/'), - 'found_query' => find_line_number($query_php_absolute, '/found_.*item_name_plural.*_query/'), -]; + return 0; +} // ── Discover query classes ────────────────────────────────────────────────── From 1d66ab8ace4c4b5497ce07d11d6d36b93aa33389 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 3 Feb 2026 11:17:14 -0700 Subject: [PATCH 23/32] Address CodeRabbit review feedback and refactor Settings API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PWYW fixes: - Add custom_amounts and pwyw_recurring to Cart in process_order() - Add missing pricing inputs to should_collect_payment() temporary cart - Remove v-bind:name suppression for PWYW duration fields in product edit - Update Product::is_recurring() to check pwyw_recurring_mode - Add wu_product_is_recurring filter for extensibility Settings API refactor: - Create save_setting() helper for DRY code - Add sanitize_setting_value() using Field API for proper sanitization - Add get_field_definition() to look up field definitions from Settings - Remove body_params from API logging to avoid storing sensitive data Grammar and text fixes: - "This products" → "These products" - "do not exist" → "does not exist" - Remove trailing space from "Force Synchronous Site Publication" Security improvements: - Add rel="noopener noreferrer" and esc_url() to privacy policy link Cleanup: - Delete stale inc/berlindb-dynamic-hooks.php duplicate - Fix changelog: separate 2.4.10 (released) from 2.4.11 (unreleased) Co-Authored-By: Claude Opus 4.5 --- .../class-product-edit-admin-page.php | 10 +- inc/apis/class-settings-endpoint.php | 190 +- inc/berlindb-dynamic-hooks.php | 1674 ----------------- inc/checkout/class-cart.php | 2 +- inc/checkout/class-checkout.php | 33 +- inc/class-settings.php | 6 +- inc/models/class-product.php | 29 +- lang/ultimate-multisite.pot | 330 ++-- readme.txt | 12 +- 9 files changed, 363 insertions(+), 1923 deletions(-) delete mode 100644 inc/berlindb-dynamic-hooks.php diff --git a/inc/admin-pages/class-product-edit-admin-page.php b/inc/admin-pages/class-product-edit-admin-page.php index 905afedd..71df8559 100644 --- a/inc/admin-pages/class-product-edit-admin-page.php +++ b/inc/admin-pages/class-product-edit-admin-page.php @@ -388,9 +388,8 @@ public function register_widgets(): void { 'wrapper_classes' => 'wu-w-1/2', 'min' => 1, 'html_attr' => [ - 'v-model' => 'duration', - 'v-bind:name' => '""', - 'steps' => 1, + 'v-model' => 'duration', + 'steps' => 1, ], ], 'duration_unit' => [ @@ -399,8 +398,7 @@ public function register_widgets(): void { 'placeholder' => '', 'wrapper_classes' => 'wu-w-1/2 wu-ml-2', 'html_attr' => [ - 'v-model' => 'duration_unit', - 'v-bind:name' => '""', + 'v-model' => 'duration_unit', ], 'options' => [ 'day' => __('Day(s)', 'ultimate-multisite'), @@ -784,7 +782,7 @@ protected function get_product_option_sections() { 'type' => 'model', 'title' => __('Offer Add-ons', 'ultimate-multisite'), 'placeholder' => __('Search for a package or service', 'ultimate-multisite'), - 'desc' => __('This products will be offered inside upgrade/downgrade forms as order bumps.', 'ultimate-multisite'), + 'desc' => __('These products will be offered inside upgrade/downgrade forms as order bumps.', 'ultimate-multisite'), 'html_attr' => [ 'data-exclude' => implode(',', array_keys($plans_as_options)), 'data-model' => 'product', diff --git a/inc/apis/class-settings-endpoint.php b/inc/apis/class-settings-endpoint.php index 2ab6476c..ae21ca14 100644 --- a/inc/apis/class-settings-endpoint.php +++ b/inc/apis/class-settings-endpoint.php @@ -9,6 +9,9 @@ namespace WP_Ultimo\API; +use WP_Ultimo\Settings; +use WP_Ultimo\UI\Field; + // Exit if accessed directly defined('ABSPATH') || exit; @@ -216,35 +219,27 @@ public function update_settings($request) { ); } - // Validate and filter out sensitive settings - $errors = []; - $filtered_settings = []; + // Validate, filter, and save settings + $errors = []; + $updated = []; + $failed = []; foreach ($settings_to_update as $key => $value) { - if ($this->is_sensitive_setting($key)) { - $errors[] = sprintf( - /* translators: %s is the setting key */ - __('Setting "%s" is protected and cannot be modified via the API.', 'ultimate-multisite'), - $key - ); - continue; - } + $result = $this->save_setting($key, $value); - // Validate setting key format - $sanitized_key = sanitize_key($key); - if ($sanitized_key !== $key) { - $errors[] = sprintf( - /* translators: %s is the setting key */ - __('Invalid setting key format: "%s".', 'ultimate-multisite'), - $key - ); + if (is_wp_error($result)) { + $errors[] = $result->get_error_message(); continue; } - $filtered_settings[ $key ] = $value; + if ($result) { + $updated[] = $key; + } else { + $failed[] = $key; + } } - if (empty($filtered_settings)) { + if (empty($updated) && ! empty($errors)) { return new \WP_Error( 'no_valid_settings', __('No valid settings to update after filtering.', 'ultimate-multisite'), @@ -255,20 +250,6 @@ public function update_settings($request) { ); } - // Save each setting - $updated = []; - $failed = []; - - foreach ($filtered_settings as $key => $value) { - $result = wu_save_setting($key, $value); - - if ($result) { - $updated[] = $key; - } else { - $failed[] = $key; - } - } - $response_data = [ 'success' => ! empty($updated), 'updated' => $updated, @@ -299,23 +280,12 @@ public function update_setting($request) { $setting_key = $request->get_param('setting_key'); - // Check if this is a sensitive setting - if ($this->is_sensitive_setting($setting_key)) { - return new \WP_Error( - 'setting_protected', - __('This setting is protected and cannot be modified via the API.', 'ultimate-multisite'), - ['status' => 403] - ); - } - $params = $request->get_json_params(); if (empty($params)) { $params = $request->get_body_params(); } - $value = wu_get_isset($params, 'value'); - if (! isset($params['value'])) { return new \WP_Error( 'missing_value', @@ -324,7 +294,12 @@ public function update_setting($request) { ); } - $result = wu_save_setting($setting_key, $value); + $value = wu_get_isset($params, 'value'); + $result = $this->save_setting($setting_key, $value); + + if (is_wp_error($result)) { + return $result; + } if (! $result) { return new \WP_Error( @@ -347,6 +322,115 @@ public function update_setting($request) { ); } + /** + * Save a single setting with validation and sanitization. + * + * This method handles the common logic for saving settings: + * - Validates the setting key format + * - Checks if the setting is sensitive/protected + * - Sanitizes the value using the Field API if a field definition exists + * - Saves the setting to the database + * + * @since 2.4.0 + * + * @param string $key The setting key. + * @param mixed $value The value to save. + * @return bool|\WP_Error True on success, false on save failure, WP_Error on validation failure. + */ + protected function save_setting(string $key, $value) { + + // Check if this is a sensitive setting + if ($this->is_sensitive_setting($key)) { + return new \WP_Error( + 'setting_protected', + sprintf( + /* translators: %s is the setting key */ + __('Setting "%s" is protected and cannot be modified via the API.', 'ultimate-multisite'), + $key + ), + ['status' => 403] + ); + } + + // Validate setting key format + $sanitized_key = sanitize_key($key); + + if ($sanitized_key !== $key) { + return new \WP_Error( + 'invalid_key_format', + sprintf( + /* translators: %s is the setting key */ + __('Invalid setting key format: "%s".', 'ultimate-multisite'), + $key + ), + ['status' => 400] + ); + } + + // Sanitize the value using Field API if field definition exists + $sanitized_value = $this->sanitize_setting_value($key, $value); + + return wu_save_setting($key, $sanitized_value); + } + + /** + * Sanitize a setting value using the Field API. + * + * Looks up the field definition from Settings and uses the Field class + * to apply appropriate sanitization based on field type. + * + * @since 2.4.0 + * + * @param string $key The setting key. + * @param mixed $value The raw value to sanitize. + * @return mixed The sanitized value, or the original value if no field definition exists. + */ + protected function sanitize_setting_value(string $key, $value) { + + $field_definition = $this->get_field_definition($key); + + if (empty($field_definition)) { + // No field definition found, apply basic sanitization based on value type + if (is_string($value)) { + return sanitize_text_field($value); + } + + return $value; + } + + // Create a Field instance and use its sanitization + $field = new Field($key, $field_definition); + $field->set_value($value); + + return $field->get_value(); + } + + /** + * Get the field definition for a setting key. + * + * Searches through all settings sections to find the field definition + * that matches the given setting key. + * + * @since 2.4.0 + * + * @param string $key The setting key to look up. + * @return array|null The field definition array, or null if not found. + */ + protected function get_field_definition(string $key): ?array { + + $sections = Settings::get_instance()->get_sections(); + + foreach ($sections as $section) { + $fields = $section['fields'] ?? []; + + if (isset($fields[ $key ])) { + return $fields[ $key ]; + } + } + + return null; + } + /** * Get the arguments schema for the update endpoint. * @@ -406,7 +490,7 @@ protected function is_sensitive_setting(string $setting_key): bool { */ protected function filter_sensitive_settings(array $settings): array { - foreach ($settings as $key => $value) { + foreach ($settings as $key => $_) { if ($this->is_sensitive_setting($key)) { unset($settings[ $key ]); } @@ -418,6 +502,9 @@ protected function filter_sensitive_settings(array $settings): array { /** * Log API call if logging is enabled. * + * Note: Request body is intentionally not logged to avoid + * accidentally storing sensitive data like passwords or API keys. + * * @since 2.4.0 * * @param \WP_REST_Request $request The request object. @@ -427,10 +514,9 @@ protected function maybe_log_api_call($request): void { if (\WP_Ultimo\API::get_instance()->should_log_api_calls()) { $payload = [ - 'route' => $request->get_route(), - 'method' => $request->get_method(), - 'url_params' => $request->get_url_params(), - 'body_params' => $request->get_body(), + 'route' => $request->get_route(), + 'method' => $request->get_method(), + 'url_params' => $request->get_url_params(), ]; wu_log_add('api-calls', wp_json_encode($payload, JSON_PRETTY_PRINT)); diff --git a/inc/berlindb-dynamic-hooks.php b/inc/berlindb-dynamic-hooks.php deleted file mode 100644 index e5b17cb0..00000000 --- a/inc/berlindb-dynamic-hooks.php +++ /dev/null @@ -1,1674 +0,0 @@ -errors->add('discount_code', sprintf(__('The code %s do not exist or is no longer valid.', 'ultimate-multisite'), $code)); + $this->errors->add('discount_code', sprintf(__('The code %s does not exist or is no longer valid.', 'ultimate-multisite'), $code)); return false; } diff --git a/inc/checkout/class-checkout.php b/inc/checkout/class-checkout.php index 4a0a348f..cf0e7a93 100644 --- a/inc/checkout/class-checkout.php +++ b/inc/checkout/class-checkout.php @@ -695,17 +695,19 @@ public function process_order() { apply_filters( 'wu_cart_parameters', [ - 'products' => $this->request_or_session('products', []), - 'discount_code' => $this->request_or_session('discount_code'), - 'country' => $this->request_or_session('billing_country'), - 'state' => $this->request_or_session('billing_state'), - 'city' => $this->request_or_session('billing_city'), - 'membership_id' => $this->request_or_session('membership_id'), - 'payment_id' => $this->request_or_session('payment_id'), - 'auto_renew' => $this->request_or_session('auto_renew', false), - 'duration' => $this->request_or_session('duration'), - 'duration_unit' => $this->request_or_session('duration_unit'), - 'cart_type' => $this->request_or_session('cart_type', 'new'), + 'products' => $this->request_or_session('products', []), + 'discount_code' => $this->request_or_session('discount_code'), + 'country' => $this->request_or_session('billing_country'), + 'state' => $this->request_or_session('billing_state'), + 'city' => $this->request_or_session('billing_city'), + 'membership_id' => $this->request_or_session('membership_id'), + 'payment_id' => $this->request_or_session('payment_id'), + 'auto_renew' => $this->request_or_session('auto_renew', false), + 'duration' => $this->request_or_session('duration'), + 'duration_unit' => $this->request_or_session('duration_unit'), + 'cart_type' => $this->request_or_session('cart_type', 'new'), + 'custom_amounts' => $this->request_or_session('custom_amounts', []), + 'pwyw_recurring' => $this->request_or_session('pwyw_recurring', []), ], $this ) @@ -1979,8 +1981,13 @@ public function should_collect_payment(): bool { try { $cart = new Cart( [ - 'products' => (array) $products, - 'country' => $this->request_or_session('billing_country'), + 'products' => (array) $products, + 'country' => $this->request_or_session('billing_country'), + 'discount_code' => $this->request_or_session('discount_code'), + 'duration' => $this->request_or_session('duration'), + 'duration_unit' => $this->request_or_session('duration_unit'), + 'custom_amounts' => $this->request_or_session('custom_amounts', []), + 'pwyw_recurring' => $this->request_or_session('pwyw_recurring', []), ] ); diff --git a/inc/class-settings.php b/inc/class-settings.php index be022253..d58d5740 100644 --- a/inc/class-settings.php +++ b/inc/class-settings.php @@ -715,8 +715,8 @@ public function default_sections(): void { 'title' => __('Help Improve Ultimate Multisite', 'ultimate-multisite'), 'desc' => sprintf( /* translators: %s is a link to the privacy policy */ - __('Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. Learn more.', 'ultimate-multisite'), - 'https://ultimatemultisite.com/privacy-policy/' + __('Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. Learn more.', 'ultimate-multisite'), + esc_url('https://ultimatemultisite.com/privacy-policy/') ), 'type' => 'toggle', 'default' => 0, @@ -872,7 +872,7 @@ public function default_sections(): void { 'login-and-registration', 'force_publish_sites_sync', [ - 'title' => __('Force Synchronous Site Publication ', 'ultimate-multisite'), + 'title' => __('Force Synchronous Site Publication', 'ultimate-multisite'), 'desc' => __('By default, when a new pending site needs to be converted into a real network site, the publishing process happens via Job Queue, asynchronously. Enable this option to force the publication to happen in the same request as the signup. Be careful, as this can cause timeouts depending on the size of the site templates being copied.', 'ultimate-multisite'), 'type' => 'toggle', 'default' => 0, diff --git a/inc/models/class-product.php b/inc/models/class-product.php index b813e175..2960dfbf 100644 --- a/inc/models/class-product.php +++ b/inc/models/class-product.php @@ -1022,12 +1022,39 @@ public function set_parent_id($parent_id): void { /** * Get is this product recurring? * + * For PWYW products, recurring is determined by pwyw_recurring_mode: + * - 'force_recurring': always recurring + * - 'force_one_time': never recurring + * - 'customer_choice': depends on customer selection (treated as potentially recurring) + * * @since 2.0.0 * @return boolean */ public function is_recurring() { - return (bool) $this->recurring && (float) $this->get_amount() > 0; + $is_recurring = (bool) $this->recurring && (float) $this->get_amount() > 0; + + // PWYW products determine recurring status via pwyw_recurring_mode + if ($this->is_pay_what_you_want()) { + $pwyw_mode = $this->get_pwyw_recurring_mode(); + + if ('force_recurring' === $pwyw_mode) { + $is_recurring = true; + } elseif ('force_one_time' === $pwyw_mode) { + $is_recurring = false; + } + // 'customer_choice' uses the base $is_recurring value + } + + /** + * Filter whether a product is considered recurring. + * + * @since 2.4.0 + * + * @param bool $is_recurring Whether the product is recurring. + * @param Product $product The product instance. + */ + return apply_filters('wu_product_is_recurring', $is_recurring, $this); } /** diff --git a/lang/ultimate-multisite.pot b/lang/ultimate-multisite.pot index f86e8a62..82d921a6 100644 --- a/lang/ultimate-multisite.pot +++ b/lang/ultimate-multisite.pot @@ -9,7 +9,7 @@ msgstr "" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2026-02-03T06:01:34+00:00\n" +"POT-Creation-Date: 2026-02-03T18:06:12+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "X-Generator: WP-CLI 2.12.0\n" "X-Domain: ultimate-multisite\n" @@ -359,7 +359,7 @@ msgstr "" #: inc/admin-pages/class-broadcast-edit-admin-page.php:370 #: inc/admin-pages/class-edit-admin-page.php:489 #: inc/admin-pages/class-tax-rates-admin-page.php:193 -#: inc/checkout/class-checkout.php:1810 +#: inc/checkout/class-checkout.php:1812 #: views/base/edit/widget-tabs.php:106 #: views/dashboard-statistics/filter.php:28 #: views/dashboard-statistics/widget-mrr-growth.php:16 @@ -411,7 +411,7 @@ msgstr "" #: inc/admin-pages/class-event-view-admin-page.php:253 #: inc/admin-pages/class-membership-edit-admin-page.php:997 #: inc/admin-pages/class-payment-edit-admin-page.php:1225 -#: inc/admin-pages/class-product-edit-admin-page.php:1093 +#: inc/admin-pages/class-product-edit-admin-page.php:1091 #: inc/admin-pages/class-site-edit-admin-page.php:678 #: inc/admin-pages/class-view-logs-admin-page.php:281 #: inc/admin-pages/class-webhook-edit-admin-page.php:343 @@ -864,8 +864,8 @@ msgstr "" #: inc/admin-pages/class-domain-edit-admin-page.php:388 #: inc/admin-pages/class-email-edit-admin-page.php:279 #: inc/admin-pages/class-email-edit-admin-page.php:283 -#: inc/admin-pages/class-product-edit-admin-page.php:598 -#: inc/admin-pages/class-product-edit-admin-page.php:602 +#: inc/admin-pages/class-product-edit-admin-page.php:596 +#: inc/admin-pages/class-product-edit-admin-page.php:600 #: inc/admin-pages/class-site-edit-admin-page.php:543 #: inc/admin-pages/class-site-edit-admin-page.php:547 #: inc/admin-pages/class-template-previewer-customize-admin-page.php:106 @@ -1152,7 +1152,7 @@ msgid "Value" msgstr "" #: inc/admin-pages/class-customer-edit-admin-page.php:592 -#: inc/admin-pages/class-product-edit-admin-page.php:845 +#: inc/admin-pages/class-product-edit-admin-page.php:843 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:216 #: inc/checkout/signup-fields/class-signup-field-select.php:179 #: inc/class-settings.php:1153 @@ -1264,7 +1264,7 @@ msgstr "" #: inc/admin-pages/class-email-edit-admin-page.php:298 #: inc/admin-pages/class-invoice-template-customize-admin-page.php:193 #: inc/admin-pages/class-membership-edit-admin-page.php:431 -#: inc/admin-pages/class-product-edit-admin-page.php:706 +#: inc/admin-pages/class-product-edit-admin-page.php:704 #: inc/admin-pages/class-template-previewer-customize-admin-page.php:132 #: inc/class-settings.php:550 #: inc/class-settings.php:551 @@ -1907,9 +1907,9 @@ msgstr "" #: inc/admin-pages/class-discount-code-edit-admin-page.php:603 #: inc/admin-pages/class-membership-edit-admin-page.php:701 -#: inc/admin-pages/class-product-edit-admin-page.php:491 -#: inc/admin-pages/class-product-edit-admin-page.php:545 -#: inc/admin-pages/class-product-edit-admin-page.php:872 +#: inc/admin-pages/class-product-edit-admin-page.php:489 +#: inc/admin-pages/class-product-edit-admin-page.php:543 +#: inc/admin-pages/class-product-edit-admin-page.php:870 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:242 #: inc/functions/date.php:115 msgid "Days" @@ -1921,9 +1921,9 @@ msgstr "" #: inc/admin-pages/class-discount-code-edit-admin-page.php:607 #: inc/admin-pages/class-membership-edit-admin-page.php:702 -#: inc/admin-pages/class-product-edit-admin-page.php:492 -#: inc/admin-pages/class-product-edit-admin-page.php:546 -#: inc/admin-pages/class-product-edit-admin-page.php:873 +#: inc/admin-pages/class-product-edit-admin-page.php:490 +#: inc/admin-pages/class-product-edit-admin-page.php:544 +#: inc/admin-pages/class-product-edit-admin-page.php:871 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:243 msgid "Weeks" msgstr "" @@ -1935,9 +1935,9 @@ msgstr "" #: inc/admin-pages/class-discount-code-edit-admin-page.php:611 #: inc/admin-pages/class-membership-edit-admin-page.php:703 -#: inc/admin-pages/class-product-edit-admin-page.php:493 -#: inc/admin-pages/class-product-edit-admin-page.php:547 -#: inc/admin-pages/class-product-edit-admin-page.php:874 +#: inc/admin-pages/class-product-edit-admin-page.php:491 +#: inc/admin-pages/class-product-edit-admin-page.php:545 +#: inc/admin-pages/class-product-edit-admin-page.php:872 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:244 #: inc/functions/date.php:118 msgid "Months" @@ -1950,9 +1950,9 @@ msgstr "" #: inc/admin-pages/class-discount-code-edit-admin-page.php:615 #: inc/admin-pages/class-membership-edit-admin-page.php:704 -#: inc/admin-pages/class-product-edit-admin-page.php:494 -#: inc/admin-pages/class-product-edit-admin-page.php:548 -#: inc/admin-pages/class-product-edit-admin-page.php:875 +#: inc/admin-pages/class-product-edit-admin-page.php:492 +#: inc/admin-pages/class-product-edit-admin-page.php:546 +#: inc/admin-pages/class-product-edit-admin-page.php:873 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:245 #: inc/functions/date.php:121 msgid "Years" @@ -1964,14 +1964,14 @@ msgstr "" #: inc/admin-pages/class-discount-code-edit-admin-page.php:675 #: inc/admin-pages/class-discount-code-edit-admin-page.php:697 -#: inc/admin-pages/class-product-edit-admin-page.php:658 -#: inc/admin-pages/class-product-edit-admin-page.php:680 +#: inc/admin-pages/class-product-edit-admin-page.php:656 +#: inc/admin-pages/class-product-edit-admin-page.php:678 msgid "Legacy Options" msgstr "" #. translators: %s is the comma-separated list of legacy add-ons. #: inc/admin-pages/class-discount-code-edit-admin-page.php:677 -#: inc/admin-pages/class-product-edit-admin-page.php:660 +#: inc/admin-pages/class-product-edit-admin-page.php:658 #, php-format msgid "Options for %s, and others." msgstr "" @@ -3053,7 +3053,7 @@ msgstr "" #: inc/admin-pages/class-product-list-admin-page.php:98 #: inc/admin-pages/class-product-list-admin-page.php:109 #: inc/admin-pages/class-top-admin-nav-menu.php:103 -#: inc/checkout/class-checkout.php:2171 +#: inc/checkout/class-checkout.php:2178 #: inc/checkout/signup-fields/class-signup-field-pricing-table.php:170 #: inc/checkout/signup-fields/class-signup-field-products.php:149 #: inc/checkout/signup-fields/class-signup-field-products.php:150 @@ -3146,7 +3146,7 @@ msgstr "" #: inc/admin-pages/class-membership-edit-admin-page.php:626 #: inc/admin-pages/class-payment-edit-admin-page.php:746 #: inc/admin-pages/class-payment-edit-admin-page.php:764 -#: inc/admin-pages/class-product-edit-admin-page.php:578 +#: inc/admin-pages/class-product-edit-admin-page.php:576 #, php-format msgid "E.g. %s" msgstr "" @@ -3169,13 +3169,13 @@ msgstr "" #. translators: placeholder %1$s is the amount, %2$s is the duration (such as 1, 2, 3), and %3$s is the unit (such as month, year, week) #: inc/admin-pages/class-membership-edit-admin-page.php:665 -#: inc/admin-pages/class-product-edit-admin-page.php:453 +#: inc/admin-pages/class-product-edit-admin-page.php:451 #, php-format msgid "The customer will be charged %1$s every %2$s %3$s(s)." msgstr "" #: inc/admin-pages/class-membership-edit-admin-page.php:737 -#: inc/admin-pages/class-product-edit-admin-page.php:501 +#: inc/admin-pages/class-product-edit-admin-page.php:499 msgid "Billing Cycles" msgstr "" @@ -3184,7 +3184,7 @@ msgid "E.g. 0" msgstr "" #: inc/admin-pages/class-membership-edit-admin-page.php:739 -#: inc/admin-pages/class-product-edit-admin-page.php:503 +#: inc/admin-pages/class-product-edit-admin-page.php:501 msgid "How many times should we bill this customer. Leave 0 to charge until cancelled." msgstr "" @@ -3290,7 +3290,7 @@ msgstr "" #: inc/admin-pages/class-membership-edit-admin-page.php:993 #: inc/admin-pages/class-payment-edit-admin-page.php:1221 -#: inc/admin-pages/class-product-edit-admin-page.php:1089 +#: inc/admin-pages/class-product-edit-admin-page.php:1087 msgid "This name will be used on pricing tables, invoices, and more." msgstr "" @@ -3639,7 +3639,7 @@ msgid "Item quantity." msgstr "" #: inc/admin-pages/class-payment-edit-admin-page.php:725 -#: inc/admin-pages/class-product-edit-admin-page.php:502 +#: inc/admin-pages/class-product-edit-admin-page.php:500 msgid "E.g. 1" msgstr "" @@ -3665,7 +3665,7 @@ msgid "Refund, credit or fee amount." msgstr "" #: inc/admin-pages/class-payment-edit-admin-page.php:779 -#: inc/admin-pages/class-product-edit-admin-page.php:929 +#: inc/admin-pages/class-product-edit-admin-page.php:927 msgid "Is Taxable?" msgstr "" @@ -4039,33 +4039,33 @@ msgstr "" msgid "The billing period for recurring PWYW payments. Only applies when recurring is enabled." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:406 +#: inc/admin-pages/class-product-edit-admin-page.php:404 msgid "Day(s)" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:407 +#: inc/admin-pages/class-product-edit-admin-page.php:405 msgid "Week(s)" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:408 +#: inc/admin-pages/class-product-edit-admin-page.php:406 msgid "Month(s)" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:409 +#: inc/admin-pages/class-product-edit-admin-page.php:407 msgid "Year(s)" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:416 +#: inc/admin-pages/class-product-edit-admin-page.php:414 msgid "Is Recurring?" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:417 +#: inc/admin-pages/class-product-edit-admin-page.php:415 msgid "Check this if this product has a recurring charge." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:435 -#: inc/admin-pages/class-product-edit-admin-page.php:436 -#: inc/admin-pages/class-product-edit-admin-page.php:451 +#: inc/admin-pages/class-product-edit-admin-page.php:433 +#: inc/admin-pages/class-product-edit-admin-page.php:434 +#: inc/admin-pages/class-product-edit-admin-page.php:449 #: inc/list-tables/class-membership-list-table-widget.php:265 #: inc/list-tables/class-membership-list-table.php:180 #: inc/list-tables/class-product-list-table.php:256 @@ -4073,171 +4073,171 @@ msgstr "" msgid "Price" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:513 +#: inc/admin-pages/class-product-edit-admin-page.php:511 msgid "Offer Trial" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:514 +#: inc/admin-pages/class-product-edit-admin-page.php:512 msgid "Check if you want to add a trial period to this product." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:526 +#: inc/admin-pages/class-product-edit-admin-page.php:524 msgid "Trial" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:555 +#: inc/admin-pages/class-product-edit-admin-page.php:553 msgid "Add Setup Fee?" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:556 +#: inc/admin-pages/class-product-edit-admin-page.php:554 msgid "Check if you want to add a setup fee." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:575 +#: inc/admin-pages/class-product-edit-admin-page.php:573 #: inc/list-tables/class-product-list-table.php:257 msgid "Setup Fee" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:576 +#: inc/admin-pages/class-product-edit-admin-page.php:574 msgid "The setup fee will be added to the first charge, in addition to the regular price of the product." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:603 +#: inc/admin-pages/class-product-edit-admin-page.php:601 msgid "Use this option to manually enable or disable this product for new sign-ups." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:613 -#: inc/admin-pages/class-product-edit-admin-page.php:618 +#: inc/admin-pages/class-product-edit-admin-page.php:611 +#: inc/admin-pages/class-product-edit-admin-page.php:616 msgid "Product Image" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:619 +#: inc/admin-pages/class-product-edit-admin-page.php:617 msgid "This image is used on product list tables and other places." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:707 +#: inc/admin-pages/class-product-edit-admin-page.php:705 msgid "General product options such as product slug, type, etc." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:716 +#: inc/admin-pages/class-product-edit-admin-page.php:714 #: inc/list-tables/class-membership-line-item-list-table.php:115 msgid "Product Slug" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:717 +#: inc/admin-pages/class-product-edit-admin-page.php:715 msgid "e.g. premium" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:718 +#: inc/admin-pages/class-product-edit-admin-page.php:716 msgid "This serves as a id to the product in a number of different contexts." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:720 +#: inc/admin-pages/class-product-edit-admin-page.php:718 msgid "Lowercase alpha-numeric characters with dashes or underlines. No spaces allowed." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:729 -#: inc/admin-pages/class-product-edit-admin-page.php:730 +#: inc/admin-pages/class-product-edit-admin-page.php:727 +#: inc/admin-pages/class-product-edit-admin-page.php:728 msgid "Product Type" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:731 +#: inc/admin-pages/class-product-edit-admin-page.php:729 msgid "Different product types have different options." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:740 +#: inc/admin-pages/class-product-edit-admin-page.php:738 msgid "Customer Role" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:741 +#: inc/admin-pages/class-product-edit-admin-page.php:739 msgid "Select the role Ultimate Multisite should use when adding the user to their newly created site." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:758 +#: inc/admin-pages/class-product-edit-admin-page.php:756 msgid "Up & Downgrades" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:759 +#: inc/admin-pages/class-product-edit-admin-page.php:757 msgid "Settings related to upgrade and downgrade flows." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:765 +#: inc/admin-pages/class-product-edit-admin-page.php:763 msgid "Plan Group" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:766 +#: inc/admin-pages/class-product-edit-admin-page.php:764 msgid "Add related plans to the same group to have them show up as upgrade/downgrade paths." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:767 -#: inc/admin-pages/class-product-edit-admin-page.php:779 +#: inc/admin-pages/class-product-edit-admin-page.php:765 +#: inc/admin-pages/class-product-edit-admin-page.php:777 msgid "Type and press enter to search and/or add." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:770 +#: inc/admin-pages/class-product-edit-admin-page.php:768 msgid "Select Group" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:777 +#: inc/admin-pages/class-product-edit-admin-page.php:775 msgid "Product Order" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:778 +#: inc/admin-pages/class-product-edit-admin-page.php:776 msgid "Plans are shown in the order determined by this parameter, from the lowest to the highest." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:785 +#: inc/admin-pages/class-product-edit-admin-page.php:783 msgid "Offer Add-ons" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:786 +#: inc/admin-pages/class-product-edit-admin-page.php:784 msgid "Search for a package or service" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:787 -msgid "This products will be offered inside upgrade/downgrade forms as order bumps." +#: inc/admin-pages/class-product-edit-admin-page.php:785 +msgid "These products will be offered inside upgrade/downgrade forms as order bumps." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:809 +#: inc/admin-pages/class-product-edit-admin-page.php:807 msgid "Price Variations" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:810 +#: inc/admin-pages/class-product-edit-admin-page.php:808 msgid "Discounts for longer membership commitments." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:819 +#: inc/admin-pages/class-product-edit-admin-page.php:817 msgid "Enable Price Variations" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:820 +#: inc/admin-pages/class-product-edit-admin-page.php:818 msgid "Price Variations are an easy way to offer discounted prices for longer subscription commitments." msgstr "" #. translators: 1 is the price, 2 is the duration and 3 the duration unit -#: inc/admin-pages/class-product-edit-admin-page.php:829 +#: inc/admin-pages/class-product-edit-admin-page.php:827 #, php-format msgid "A discounted price of %1$s will be used when memberships are created with the recurrence of %2$s %3$s(s) instead of the regular period." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:852 +#: inc/admin-pages/class-product-edit-admin-page.php:850 #: inc/checkout/signup-fields/class-signup-field-period-selection.php:222 msgid "Duration" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:864 +#: inc/admin-pages/class-product-edit-admin-page.php:862 msgid "Period" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:888 +#: inc/admin-pages/class-product-edit-admin-page.php:886 msgid "New Price" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:901 +#: inc/admin-pages/class-product-edit-admin-page.php:899 msgid "Add new Price Variation" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:920 +#: inc/admin-pages/class-product-edit-admin-page.php:918 #: inc/list-tables/class-line-item-list-table.php:216 #: inc/tax/class-dashboard-taxes-tab.php:63 #: inc/tax/class-dashboard-taxes-tab.php:151 @@ -4247,102 +4247,102 @@ msgstr "" msgid "Taxes" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:921 +#: inc/admin-pages/class-product-edit-admin-page.php:919 msgid "Tax settings for your products." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:930 +#: inc/admin-pages/class-product-edit-admin-page.php:928 msgid "Enable this if you plan to collect taxes for this product." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:938 +#: inc/admin-pages/class-product-edit-admin-page.php:936 msgid "Tax Category" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:939 +#: inc/admin-pages/class-product-edit-admin-page.php:937 msgid "Select the product tax category." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:951 -#: inc/admin-pages/class-product-edit-admin-page.php:994 +#: inc/admin-pages/class-product-edit-admin-page.php:949 +#: inc/admin-pages/class-product-edit-admin-page.php:992 msgid "Site Templates" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:952 +#: inc/admin-pages/class-product-edit-admin-page.php:950 msgid "Limit which site templates are available for this particular template." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:963 +#: inc/admin-pages/class-product-edit-admin-page.php:961 msgid "Allow Site Templates" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:964 +#: inc/admin-pages/class-product-edit-admin-page.php:962 msgid "Toggle this option on to allow this plan to use Site Templates. If this option is disabled, sign-ups on this plan will get a default WordPress site." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:974 -#: inc/admin-pages/class-product-edit-admin-page.php:975 +#: inc/admin-pages/class-product-edit-admin-page.php:972 +#: inc/admin-pages/class-product-edit-admin-page.php:973 msgid "Site Template Selection Mode" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:976 +#: inc/admin-pages/class-product-edit-admin-page.php:974 msgid "Select the type of limitation you want to apply." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:977 +#: inc/admin-pages/class-product-edit-admin-page.php:975 msgid "\"Default\" will follow the settings of the checkout form: if you have a template selection field in there, all the templates selected will show up. If no field is present, then a default WordPress site will be created.

\"Assign Site Template\" forces new accounts with this plan to use a particular template site (this option removes the template selection field from the signup, if one exists).

Finally, \"Choose Available Site Templates\", overrides the templates selected on the checkout form with the templates selected here, while also giving you the chance of pre-select a template to be used as default." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:980 +#: inc/admin-pages/class-product-edit-admin-page.php:978 msgid "Default - Allow All Site Templates" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:981 +#: inc/admin-pages/class-product-edit-admin-page.php:979 msgid "Assign Site Template" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:982 +#: inc/admin-pages/class-product-edit-admin-page.php:980 msgid "Choose Available Site Templates" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:995 +#: inc/admin-pages/class-product-edit-admin-page.php:993 msgid "Select the Site Template to assign." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:995 +#: inc/admin-pages/class-product-edit-admin-page.php:993 msgid "Customize the access level of each Site Template below." msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:1037 -#: inc/admin-pages/class-product-edit-admin-page.php:1048 -#: inc/admin-pages/class-product-edit-admin-page.php:1085 +#: inc/admin-pages/class-product-edit-admin-page.php:1035 +#: inc/admin-pages/class-product-edit-admin-page.php:1046 +#: inc/admin-pages/class-product-edit-admin-page.php:1083 msgid "Edit Product" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:1037 -#: inc/admin-pages/class-product-edit-admin-page.php:1086 +#: inc/admin-pages/class-product-edit-admin-page.php:1035 +#: inc/admin-pages/class-product-edit-admin-page.php:1084 #: views/memberships/product-list.php:17 #: views/memberships/product-list.php:21 msgid "Add new Product" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:1066 +#: inc/admin-pages/class-product-edit-admin-page.php:1064 msgid "Click to copy Shareable Link" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:1087 +#: inc/admin-pages/class-product-edit-admin-page.php:1085 msgid "Product updated with success!" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:1088 +#: inc/admin-pages/class-product-edit-admin-page.php:1086 msgid "Enter Product Name" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:1090 +#: inc/admin-pages/class-product-edit-admin-page.php:1088 msgid "Save Product" msgstr "" -#: inc/admin-pages/class-product-edit-admin-page.php:1092 +#: inc/admin-pages/class-product-edit-admin-page.php:1090 msgid "Delete Product" msgstr "" @@ -5723,63 +5723,59 @@ msgstr "" msgid "The customer id sent does not correspond to a valid customer." msgstr "" -#: inc/apis/class-settings-endpoint.php:70 +#: inc/apis/class-settings-endpoint.php:73 msgid "The setting key to retrieve." msgstr "" -#: inc/apis/class-settings-endpoint.php:101 +#: inc/apis/class-settings-endpoint.php:104 msgid "The setting key to update." msgstr "" -#: inc/apis/class-settings-endpoint.php:107 +#: inc/apis/class-settings-endpoint.php:110 msgid "The new value for the setting." msgstr "" -#: inc/apis/class-settings-endpoint.php:158 +#: inc/apis/class-settings-endpoint.php:161 msgid "This setting is protected and cannot be retrieved via the API." msgstr "" #. translators: %s is the setting key -#: inc/apis/class-settings-endpoint.php:174 +#: inc/apis/class-settings-endpoint.php:177 #, php-format msgid "Setting \"%s\" not found." msgstr "" -#: inc/apis/class-settings-endpoint.php:214 +#: inc/apis/class-settings-endpoint.php:217 msgid "No valid settings provided. Please provide a \"settings\" object with key-value pairs." msgstr "" -#. translators: %s is the setting key -#: inc/apis/class-settings-endpoint.php:227 -#, php-format -msgid "Setting \"%s\" is protected and cannot be modified via the API." -msgstr "" - -#. translators: %s is the setting key -#: inc/apis/class-settings-endpoint.php:238 -#, php-format -msgid "Invalid setting key format: \"%s\"." +#: inc/apis/class-settings-endpoint.php:245 +msgid "No valid settings to update after filtering." msgstr "" -#: inc/apis/class-settings-endpoint.php:250 -msgid "No valid settings to update after filtering." +#: inc/apis/class-settings-endpoint.php:292 +msgid "The \"value\" parameter is required." msgstr "" -#: inc/apis/class-settings-endpoint.php:306 -msgid "This setting is protected and cannot be modified via the API." +#. translators: %s is the setting key +#: inc/apis/class-settings-endpoint.php:309 +#, php-format +msgid "Failed to update setting \"%s\"." msgstr "" -#: inc/apis/class-settings-endpoint.php:322 -msgid "The \"value\" parameter is required." +#. translators: %s is the setting key +#: inc/apis/class-settings-endpoint.php:348 +#, php-format +msgid "Setting \"%s\" is protected and cannot be modified via the API." msgstr "" #. translators: %s is the setting key -#: inc/apis/class-settings-endpoint.php:334 +#: inc/apis/class-settings-endpoint.php:363 #, php-format -msgid "Failed to update setting \"%s\"." +msgid "Invalid setting key format: \"%s\"." msgstr "" -#: inc/apis/class-settings-endpoint.php:360 +#: inc/apis/class-settings-endpoint.php:444 msgid "An object containing setting key-value pairs to update." msgstr "" @@ -7042,7 +7038,7 @@ msgstr "" #. translators: %s is the coupon code being used, all-caps. e.g. PROMO10OFF #: inc/checkout/class-cart.php:1369 #, php-format -msgid "The code %s do not exist or is no longer valid." +msgid "The code %s does not exist or is no longer valid." msgstr "" #. translators: two intervals @@ -7134,114 +7130,114 @@ msgstr "" msgid "Ultimate Multisite - New Site Page" msgstr "" -#: inc/checkout/class-checkout.php:752 -#: inc/checkout/class-checkout.php:760 -#: inc/checkout/class-checkout.php:2395 -#: inc/checkout/class-checkout.php:2401 +#: inc/checkout/class-checkout.php:754 +#: inc/checkout/class-checkout.php:762 +#: inc/checkout/class-checkout.php:2402 +#: inc/checkout/class-checkout.php:2408 msgid "Payment gateway not registered." msgstr "" -#: inc/checkout/class-checkout.php:1067 +#: inc/checkout/class-checkout.php:1069 msgid "The email address you entered is already in use." msgstr "" -#: inc/checkout/class-checkout.php:1117 +#: inc/checkout/class-checkout.php:1119 msgid "Something wrong happened while attempting to save the customer billing address" msgstr "" -#: inc/checkout/class-checkout.php:1674 +#: inc/checkout/class-checkout.php:1676 msgid "Invalid request" msgstr "" -#: inc/checkout/class-checkout.php:1686 +#: inc/checkout/class-checkout.php:1688 msgid "Too many requests. Please try again later." msgstr "" -#: inc/checkout/class-checkout.php:1728 +#: inc/checkout/class-checkout.php:1730 msgid "Please provide both username/email and password." msgstr "" -#: inc/checkout/class-checkout.php:1741 +#: inc/checkout/class-checkout.php:1743 msgid "Too many login attempts. Please try again in a few minutes." msgstr "" -#: inc/checkout/class-checkout.php:1770 +#: inc/checkout/class-checkout.php:1772 msgid "Invalid username or password." msgstr "" -#: inc/checkout/class-checkout.php:1788 +#: inc/checkout/class-checkout.php:1790 msgid "Login successful!" msgstr "" -#: inc/checkout/class-checkout.php:1811 +#: inc/checkout/class-checkout.php:1813 msgid "The item was added!" msgstr "" -#: inc/checkout/class-checkout.php:1812 +#: inc/checkout/class-checkout.php:1814 msgid "The Password entered is too weak." msgstr "" -#: inc/checkout/class-checkout.php:1813 +#: inc/checkout/class-checkout.php:1815 msgid "Password is required" msgstr "" -#: inc/checkout/class-checkout.php:1814 +#: inc/checkout/class-checkout.php:1816 msgid "Login failed. Please try again." msgstr "" -#: inc/checkout/class-checkout.php:1815 +#: inc/checkout/class-checkout.php:1817 msgid "Logging in..." msgstr "" -#: inc/checkout/class-checkout.php:1816 +#: inc/checkout/class-checkout.php:1818 #: views/checkout/partials/inline-login-prompt.php:17 msgid "Already have an account?" msgstr "" -#: inc/checkout/class-checkout.php:1817 +#: inc/checkout/class-checkout.php:1819 #: views/checkout/partials/inline-login-prompt.php:59 msgid "Sign in" msgstr "" -#: inc/checkout/class-checkout.php:1818 +#: inc/checkout/class-checkout.php:1820 #: views/checkout/partials/inline-login-prompt.php:51 msgid "Forgot password?" msgstr "" -#: inc/checkout/class-checkout.php:1819 +#: inc/checkout/class-checkout.php:1821 #: inc/list-tables/class-payment-list-table.php:111 #: views/dashboard-widgets/current-membership.php:315 msgid "Cancel" msgstr "" -#: inc/checkout/class-checkout.php:2167 +#: inc/checkout/class-checkout.php:2174 msgid "Password confirmation" msgstr "" -#: inc/checkout/class-checkout.php:2168 +#: inc/checkout/class-checkout.php:2175 msgid "Email confirmation" msgstr "" -#: inc/checkout/class-checkout.php:2169 +#: inc/checkout/class-checkout.php:2176 msgid "Template ID" msgstr "" -#: inc/checkout/class-checkout.php:2170 +#: inc/checkout/class-checkout.php:2177 msgid "Valid password" msgstr "" -#: inc/checkout/class-checkout.php:2172 +#: inc/checkout/class-checkout.php:2179 msgid "Payment Gateway" msgstr "" #. translators: %s payment id. -#: inc/checkout/class-checkout.php:2364 +#: inc/checkout/class-checkout.php:2371 #, php-format msgid "Payment (%s) not found." msgstr "" #. translators: %s is the membership ID -#: inc/checkout/class-checkout.php:2499 +#: inc/checkout/class-checkout.php:2506 #, php-format msgid "Checkout failed for customer %s: " msgstr "" @@ -8923,7 +8919,7 @@ msgstr "" #. translators: %s is a link to the privacy policy #: inc/class-settings.php:718 #, php-format -msgid "Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. Learn more." +msgid "Allow Ultimate Multisite to collect anonymous usage data and error reports to help us improve the plugin. We collect: PHP version, WordPress version, plugin version, network type (subdomain/subdirectory), aggregate counts (sites, memberships), active gateways, and error logs. We never collect personal data, customer information, or domain names. Learn more." msgstr "" #: inc/class-settings.php:735 @@ -9017,7 +9013,7 @@ msgid "Toggle this option to replace the WordPress logo on the sub-site login pa msgstr "" #: inc/class-settings.php:875 -msgid "Force Synchronous Site Publication " +msgid "Force Synchronous Site Publication" msgstr "" #: inc/class-settings.php:876 diff --git a/readme.txt b/readme.txt index 86c53f96..4c0790b0 100644 --- a/readme.txt +++ b/readme.txt @@ -240,10 +240,12 @@ We recommend running this in a staging environment before updating your producti == Changelog == -Version [2.4.10] - Released on 2026-XX-XX -- New: Settings API -- Fix: Problems with choosing country and state - +Version [2.4.11] - Released on 2026-XX-XX +- New: Settings API for remote settings management. +- New: Pay-What-You-Want (PWYW) pricing with per-product custom amounts and recurring options. +- New: Billing-period controls for discount codes and membership creation. +- New: Better error page for customers and admins. +- Fix: Problems with choosing country and state in checkout. Version [2.4.10] - Released on 2026-01-23 - New: Configurable minimum password strength setting with Medium, Strong, and Super Strong options. @@ -251,12 +253,10 @@ Version [2.4.10] - Released on 2026-01-23 - New: Real-time password requirement hints during checkout with translatable strings. - New: Themed password field styling with visibility toggle and color fallbacks for page builders (Elementor, Kadence, Beaver Builder). - New: Opt-in anonymous usage tracking to help improve the plugin. -- New: Better error page for customers and admins. - New: Rating reminder notice after 30 days of installation. - New: WooCommerce Subscriptions compatibility layer for site duplication. - Improved: JSON response handling for pending site creation in non-FastCGI environments. - Version [2.4.9] - Released on 2025-12-23 - New: Inline login prompt at checkout for existing users - returning customers can sign in directly without leaving the checkout flow. - New: GitHub Actions workflow for PR builds with WordPress Playground testing - enables one-click browser-based testing of pull requests. From c65339c3c13099a8bf44a0a15fce75fb190f5a6d Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 3 Feb 2026 12:42:51 -0700 Subject: [PATCH 24/32] Enforce a strong password --- assets/js/checkout.js | 1511 +++++++++++++++++++++-------------------- 1 file changed, 757 insertions(+), 754 deletions(-) diff --git a/assets/js/checkout.js b/assets/js/checkout.js index e77870af..53f8bcf3 100644 --- a/assets/js/checkout.js +++ b/assets/js/checkout.js @@ -1,1216 +1,1219 @@ -/* global Vue, moment, _, wu_checkout, wu_checkout_form, wu_create_cookie, wu_listen_to_cookie_change */ +/* global Vue, moment, _, wu_checkout, wu_checkout_form, wu_create_cookie, wu_listen_to_cookie_change, wu_initialize_tooltip */ (function ($, hooks, _) { - /* + /* * Remove the pre-flight parameter. */ - if (window.history.replaceState) { + if (window.history.replaceState) { - window.history.replaceState(null, null, wu_checkout.baseurl); + window.history.replaceState(null, null, wu_checkout.baseurl); - } // end if; + } // end if; - /* + /* * Sets default template. */ - hooks.addAction('wu_on_create_order', 'nextpress/wp-ultimo', function (checkout, data) { + hooks.addAction('wu_on_create_order', 'nextpress/wp-ultimo', function (checkout, data) { - if (typeof data.order.extra.template_id !== 'undefined' && data.order.extra.template_id) { + if (typeof data.order.extra.template_id !== 'undefined' && data.order.extra.template_id) { - checkout.template_id = data.order.extra.template_id; + checkout.template_id = data.order.extra.template_id; - } // end if; + } // end if; - }); + }); - /* + /* * Handle auto-submittable fields. * * Some fields are auto-submittable if they are the one relevant * field on a checkout step. */ - hooks.addAction('wu_checkout_loaded', 'nextpress/wp-ultimo', function (checkout) { + hooks.addAction('wu_checkout_loaded', 'nextpress/wp-ultimo', function (checkout) { - /* + /* * The checkout sets the auto submittable field as a global variable */ - if (typeof window.wu_auto_submittable_field !== 'undefined' && window.wu_auto_submittable_field) { + if (typeof window.wu_auto_submittable_field !== 'undefined' && window.wu_auto_submittable_field) { - const options = { - deep: true, - }; + const options = { + deep: true, + }; - checkout.$watch(window.wu_auto_submittable_field, function () { + checkout.$watch(window.wu_auto_submittable_field, function () { - jQuery(this.$el).submit(); + jQuery(this.$el).submit(); - }, options); + }, options); - } // end if; + } // end if; - }); + }); - /* + /* * Sets up the cookie listener for template selection. */ - hooks.addAction('wu_checkout_loaded', 'nextpress/wp-ultimo', function (checkout) { + hooks.addAction('wu_checkout_loaded', 'nextpress/wp-ultimo', function (checkout) { - /* + /* * Resets the template selection cookie. */ - wu_create_cookie('wu_template', ''); + wu_create_cookie('wu_template', ''); - /* + /* * Resets the selected products cookie. */ - wu_create_cookie('wu_selected_products', ''); - /* + wu_create_cookie('wu_selected_products', ''); + /* * Listens for changes and set the template if one is detected. */ - wu_listen_to_cookie_change('wu_template', function (value) { - if (value) { - checkout.template_id = value; - } - }); + wu_listen_to_cookie_change('wu_template', function (value) { + if (value) { + checkout.template_id = value; + } + }); - }); + }); - /** - * Allows for cross-sells - */ - $(document).on('click', '[href|="#wu-checkout-add"]', function (event) { + /** + * Allows for cross-sells + */ + $(document).on('click', '[href|="#wu-checkout-add"]', function (event) { - event.preventDefault(); + event.preventDefault(); - const el = $(this); + const el = $(this); - const product_slug = el.attr('href').split('#').pop().replace('wu-checkout-add-', ''); + const product_slug = el.attr('href').split('#').pop().replace('wu-checkout-add-', ''); - if (typeof wu_checkout_form !== 'undefined') { + if (typeof wu_checkout_form !== 'undefined') { - if (wu_checkout_form.products.indexOf(product_slug) === -1) { + if (wu_checkout_form.products.indexOf(product_slug) === -1) { - wu_checkout_form.add_product(product_slug); + wu_checkout_form.add_product(product_slug); - el.html(wu_checkout.i18n.added_to_order); + el.html(wu_checkout.i18n.added_to_order); - } // end if; + } // end if; - } // end if; + } // end if; - }); + }); - /** - * Reload page when history back button was pressed - */ - window.addEventListener('pageshow', function (event) { + /** + * Reload page when history back button was pressed + */ + window.addEventListener('pageshow', function (event) { - if (event.persisted && this.window.wu_checkout_form) { + if (event.persisted && this.window.wu_checkout_form) { - this.window.wu_checkout_form.unblock(); + this.window.wu_checkout_form.unblock(); - } // end if; + } // end if; - }); + }); - /** - * Setup - */ - $(document).ready(function () { + /** + * Setup + */ + $(document).ready(function () { - /* + /* * Prevent app creation when vue is not available. */ - if (typeof window.Vue === 'undefined') { - - return; - - } // end if; - - Object.defineProperty(Vue.prototype, '$moment', { value: moment }); - - const maybe_cast_to_int = function (value) { - - return isNaN(value) ? value : parseInt(value, 10); - - }; - - const initial_data = { - plan: maybe_cast_to_int(wu_checkout.plan), - errors: [], - order: wu_checkout.order, - products: _.map(wu_checkout.products, maybe_cast_to_int), - template_id: wu_checkout.template_id, - template_category: '', - gateway: wu_checkout.gateway, - request_billing_address: wu_checkout.request_billing_address, - country: wu_checkout.country, - state: '', - city: '', - site_url: wu_checkout.site_url, - site_domain: wu_checkout.site_domain, - is_subdomain: wu_checkout.is_subdomain, - discount_code: wu_checkout.discount_code, - toggle_discount_code: 0, - payment_method: '', - username: '', - email_address: '', - payment_id: wu_checkout.payment_id, - membership_id: wu_checkout.membership_id, - cart_type: 'new', - auto_renew: 1, - duration: wu_checkout.duration, - duration_unit: wu_checkout.duration_unit, - prevent_submission: false, - valid_password: true, - stored_templates: {}, - state_list: [], - city_list: [], - labels: {}, - show_login_prompt: false, - login_prompt_field: '', - checking_user_exists: false, - logging_in: false, - login_error: '', - inline_login_password: '', - custom_amounts: wu_checkout.custom_amounts || {}, - pwyw_recurring: wu_checkout.pwyw_recurring || {}, - }; - - hooks.applyFilters('wu_before_form_init', initial_data); - - if (!jQuery('#wu_form').length) { - - return; - - } // end if; - - /** - * ColorPicker Component - */ - Vue.component('colorPicker', { - props: ['value'], - template: '', - mounted() { + if (typeof window.Vue === 'undefined') { + + return; + + } // end if; + + Object.defineProperty(Vue.prototype, '$moment', { value: moment }); + + const maybe_cast_to_int = function (value) { + + return isNaN(value) ? value : parseInt(value, 10); + + }; + + const initial_data = { + plan: maybe_cast_to_int(wu_checkout.plan), + errors: [], + order: wu_checkout.order, + products: _.map(wu_checkout.products, maybe_cast_to_int), + template_id: wu_checkout.template_id, + template_category: '', + gateway: wu_checkout.gateway, + request_billing_address: wu_checkout.request_billing_address, + country: wu_checkout.country, + state: '', + city: '', + site_url: wu_checkout.site_url, + site_domain: wu_checkout.site_domain, + is_subdomain: wu_checkout.is_subdomain, + discount_code: wu_checkout.discount_code, + toggle_discount_code: 0, + payment_method: '', + username: '', + email_address: '', + payment_id: wu_checkout.payment_id, + membership_id: wu_checkout.membership_id, + cart_type: 'new', + auto_renew: 1, + duration: wu_checkout.duration, + duration_unit: wu_checkout.duration_unit, + prevent_submission: false, + valid_password: true, + stored_templates: {}, + state_list: [], + city_list: [], + labels: {}, + show_login_prompt: false, + login_prompt_field: '', + checking_user_exists: false, + logging_in: false, + login_error: '', + inline_login_password: '', + custom_amounts: wu_checkout.custom_amounts || {}, + pwyw_recurring: wu_checkout.pwyw_recurring || {}, + }; + + hooks.applyFilters('wu_before_form_init', initial_data); + + if (! jQuery('#wu_form').length) { + + return; + + } // end if; + + /** + * ColorPicker Component + */ + Vue.component('colorPicker', { + props: [ 'value' ], + template: '', + mounted() { + + const vm = this; - const vm = this; + $(this.$el) + .val(this.value) + // WordPress color picker + .wpColorPicker({ + width: 200, + defaultColor: this.value, + change(event, ui) { - $(this.$el) - .val(this.value) - // WordPress color picker - .wpColorPicker({ - width: 200, - defaultColor: this.value, - change(event, ui) { + // emit change event on color change using mouse + vm.$emit('input', ui.color.toString()); - // emit change event on color change using mouse - vm.$emit('input', ui.color.toString()); + }, + }); - }, - }); + }, + watch: { + value(value) { - }, - watch: { - value(value) { + // update value + $(this.$el).wpColorPicker('color', value); - // update value - $(this.$el).wpColorPicker('color', value); + }, + }, + destroyed() { - }, - }, - destroyed() { + $(this.$el).off().wpColorPicker('destroy'); // (!) Not tested - $(this.$el).off().wpColorPicker('destroy'); // (!) Not tested + }, + }); - }, - }); + /** + * Declare the dynamic content for Vue. + */ + const dynamic = { + functional: true, + template: '#dynamic', + props: [ 'template' ], + render(h, context) { - /** - * Declare the dynamic content for Vue. - */ - const dynamic = { - functional: true, - template: '#dynamic', - props: ['template'], - render(h, context) { - - const template = context.props.template; + const template = context.props.template; - const component = template ? { template } : '
nbsp;
'; + const component = template ? { template } : '
nbsp;
'; - return h(component); + return h(component); - }, - }; + }, + }; - // eslint-disable-next-line no-unused-vars - window.wu_checkout_form = new Vue({ - el: '#wu_form', - data: initial_data, - directives: { - init: { - bind(el, binding, vnode) { + // eslint-disable-next-line no-unused-vars + window.wu_checkout_form = new Vue({ + el: '#wu_form', + data: initial_data, + directives: { + init: { + bind(el, binding, vnode) { - vnode.context[binding.arg] = binding.value; + vnode.context[ binding.arg ] = binding.value; - }, - }, - }, - components: { - dynamic, - }, - computed: { - hooks() { + }, + }, + }, + components: { + dynamic, + }, + computed: { + hooks() { - return wp.hooks; + return wp.hooks; - }, - unique_products() { + }, + unique_products() { - return _.uniq(this.products, false, (item) => parseInt(item, 10)); + return _.uniq(this.products, false, (item) => parseInt(item, 10)); - }, - }, - methods: { - debounce(fn) { + }, + }, + methods: { + debounce(fn) { - return _.debounce(fn, 200, true); + return _.debounce(fn, 200, true); - }, - open_url(url, target = '_blank') { + }, + open_url(url, target = '_blank') { - window.open(url, target); + window.open(url, target); - }, - get_template(template, data) { + }, + get_template(template, data) { - if (typeof data.id === 'undefined') { + if (typeof data.id === 'undefined') { - data.id = 'default'; + data.id = 'default'; - } // end if; + } // end if; - const template_name = template + '/' + data.id; + const template_name = template + '/' + data.id; - if (typeof this.stored_templates[template_name] !== 'undefined') { + if (typeof this.stored_templates[ template_name ] !== 'undefined') { - return this.stored_templates[template_name]; + return this.stored_templates[ template_name ]; - } // end if; + } // end if; - const template_data = this.hooks.applyFilters('wu_before_template_fetch', { - duration: this.duration, - duration_unit: this.duration_unit, - products: this.products, - ...data, - }, this); + const template_data = this.hooks.applyFilters('wu_before_template_fetch', { + duration: this.duration, + duration_unit: this.duration_unit, + products: this.products, + ...data, + }, this); - this.fetch_template(template, template_data); + this.fetch_template(template, template_data); - return '
' + wu_checkout.i18n.loading + '
'; + return '
' + wu_checkout.i18n.loading + '
'; - }, - reset_templates(to_clear) { + }, + reset_templates(to_clear) { - if (typeof to_clear === 'undefined') { + if (typeof to_clear === 'undefined') { - this.stored_templates = {}; + this.stored_templates = {}; - return; + return; - } + } - const new_list = {}; + const new_list = {}; - _.forEach(this.stored_templates, function (item, key) { + _.forEach(this.stored_templates, function (item, key) { - const type = key.toString().substr(0, key.toString().indexOf('/')); + const type = key.toString().substr(0, key.toString().indexOf('/')); - if (_.contains(to_clear, type) === false) { + if (_.contains(to_clear, type) === false) { - new_list[key] = item; + new_list[ key ] = item; - } // end if; + } // end if; - }); + }); - this.stored_templates = new_list; + this.stored_templates = new_list; - }, - fetch_template(template, data) { + }, + fetch_template(template, data) { - const that = this; + const that = this; - if (typeof data.id === 'undefined') { + if (typeof data.id === 'undefined') { - data.id = 'default'; + data.id = 'default'; - } // end if; + } // end if; - this.request('wu_render_field_template', { - template, - attributes: data, - }, function (results) { + this.request('wu_render_field_template', { + template, + attributes: data, + }, function (results) { - const template_name = template + '/' + data.id; + const template_name = template + '/' + data.id; - if (results.success) { + if (results.success) { - Vue.set(that.stored_templates, template_name, results.data.html); + Vue.set(that.stored_templates, template_name, results.data.html); - } else { + } else { - Vue.set(that.stored_templates, template_name, '
' + results.data[0].message + '
'); + Vue.set(that.stored_templates, template_name, '
' + results.data[ 0 ].message + '
'); - } // end if; + } // end if; - }); + }); - }, - go_back() { + }, + go_back() { - this.block(); + this.block(); - window.history.back(); + window.history.back(); - }, - set_prevent_submission(value) { + }, + set_prevent_submission(value) { - this.$nextTick(function () { + this.$nextTick(function () { - this.prevent_submission = value; + this.prevent_submission = value; - }); + }); - }, - remove_product(product_id, product_slug) { + }, + remove_product(product_id, product_slug) { - this.products = _.filter(this.products, function (item) { + this.products = _.filter(this.products, function (item) { - // eslint-disable-next-line eqeqeq - return item != product_id && item != product_slug; + // eslint-disable-next-line eqeqeq + return item != product_id && item != product_slug; - }); + }); - }, - add_plan(product_id) { + }, + add_plan(product_id) { - if (this.plan) { + if (this.plan) { - this.remove_product(this.plan); + this.remove_product(this.plan); - } // end if; + } // end if; - this.plan = product_id; + this.plan = product_id; - this.add_product(product_id); + this.add_product(product_id); - }, - add_product(product_id) { + }, + add_product(product_id) { - this.products.push(product_id); + this.products.push(product_id); - }, - has_product(product_id) { + }, + has_product(product_id) { - return this.products.indexOf(product_id) > -1 || this.products.indexOf(parseInt(product_id, 10)) > -1; + return this.products.indexOf(product_id) > -1 || this.products.indexOf(parseInt(product_id, 10)) > -1; - }, - set_custom_amount(product_id, amount) { + }, + set_custom_amount(product_id, amount) { - Vue.set(this.custom_amounts, product_id, parseFloat(amount) || 0); + Vue.set(this.custom_amounts, product_id, parseFloat(amount) || 0); - this.create_order(); + this.create_order(); - }, - get_custom_amount(product_id) { + }, + get_custom_amount(product_id) { - return this.custom_amounts[product_id] || null; + return this.custom_amounts[ product_id ] || null; - }, - set_pwyw_recurring(product_id, is_recurring) { + }, + set_pwyw_recurring(product_id, is_recurring) { - Vue.set(this.pwyw_recurring, product_id, Boolean(is_recurring)); + Vue.set(this.pwyw_recurring, product_id, Boolean(is_recurring)); - this.create_order(); + this.create_order(); - }, - get_pwyw_recurring(product_id) { + }, + get_pwyw_recurring(product_id) { - return this.pwyw_recurring[product_id] || false; + return this.pwyw_recurring[ product_id ] || false; - }, - wu_format_money(value) { + }, + wu_format_money(value) { - return window.wu_format_money(value); + return window.wu_format_money(value); - }, - filter_for_request(data, request_type = '') { + }, + filter_for_request(data, request_type = '') { - const filter_list = this.hooks.doAction('wu_filter_for_request', [ - 'stored_templates', - ], data, request_type); + const filter_list = this.hooks.doAction('wu_filter_for_request', [ + 'stored_templates', + ], data, request_type); - const filtered_list = _.omit(data, filter_list); + const filtered_list = _.omit(data, filter_list); - return filtered_list; + return filtered_list; - }, - create_order() { + }, + create_order() { - /* + /* * Bail if there is no order summary to update. */ - if (!jQuery('#wu-order-summary-content').length) { + if (! jQuery('#wu-order-summary-content').length) { - return; + return; - } // end if; + } // end if; - this.block(); + this.block(); - this.order = false; + this.order = false; - const that = this; + const that = this; - const _request = this.debounce(this.request); + const _request = this.debounce(this.request); - const data = { ...this.$data }; + const data = { ...this.$data }; - delete data.stored_templates; - delete data.state_list; - delete data.city_list; - delete data.labels; + delete data.stored_templates; + delete data.state_list; + delete data.city_list; + delete data.labels; - _request('wu_create_order', this.filter_for_request(data, 'wu_create_order'), function (results) { + _request('wu_create_order', this.filter_for_request(data, 'wu_create_order'), function (results) { - that.order = results.data.order; + that.order = results.data.order; - that.state_list = results.data.states; + that.state_list = results.data.states; - that.city_list = results.data.cities; + that.city_list = results.data.cities; - that.labels = results.data.labels; + that.labels = results.data.labels; - that.cart_type = results.data.order.type; + that.cart_type = results.data.order.type; - that.errors = results.data.order.errors; + that.errors = results.data.order.errors; - that.hooks.doAction('wu_on_create_order', that, results.data); + that.hooks.doAction('wu_on_create_order', that, results.data); - if (results.data.order.url) { + if (results.data.order.url) { - try { + try { - // history.pushState({}, null, wu_checkout.baseurl + results.data.order.url); + // history.pushState({}, null, wu_checkout.baseurl + results.data.order.url); - } catch (err) { + } catch (err) { - // eslint-disable-next-line no-console - console.warn('Browser does not support pushState.', err); + // eslint-disable-next-line no-console + console.warn('Browser does not support pushState.', err); - } // end try; + } // end try; - } // ed if; + } // ed if; - that.unblock(); + that.unblock(); - }, this.handle_errors); + }, this.handle_errors); - }, - get_errors() { + }, + get_errors() { - const result = this.errors.map(function (e) { + const result = this.errors.map(function (e) { - return e.message; + return e.message; - }); + }); - return result.length > 0 ? result : false; + return result.length > 0 ? result : false; - }, - get_error(field) { + }, + get_error(field) { - const result = this.errors.filter(function (e) { + const result = this.errors.filter(function (e) { - return e.code === field; + return e.code === field; - }); + }); - return result.length > 0 ? result[0] : false; + return result.length > 0 ? result[ 0 ] : false; - }, - form_success(results) { + }, + form_success(results) { - if (!_.isEmpty(results.data)) { + if (! _.isEmpty(results.data)) { - this.hooks.doAction('wu_on_form_success', this, results.data); + this.hooks.doAction('wu_on_form_success', this, results.data); - const fields = results.data.gateway.data; + const fields = results.data.gateway.data; - fields.payment_id = results.data.payment_id; + fields.payment_id = results.data.payment_id; - fields.membership_id = results.data.membership_id; + fields.membership_id = results.data.membership_id; - fields.cart_type = results.data.cart_type; + fields.cart_type = results.data.cart_type; - // Append the hidden fields - jQuery.each(Object.assign({}, fields), function (index, value) { + // Append the hidden fields + jQuery.each(Object.assign({}, fields), function (index, value) { - const hidden = document.createElement('input'); + const hidden = document.createElement('input'); - hidden.type = 'hidden'; + hidden.type = 'hidden'; - hidden.name = index; + hidden.name = index; - hidden.value = value; + hidden.value = value; - jQuery('#wu_form').append(hidden); + jQuery('#wu_form').append(hidden); - }); + }); - } // end if; + } // end if; - }, - validate_form() { + }, + validate_form() { - this.errors = []; + this.errors = []; - const form_data_obj = jQuery('#wu_form').serializeArray().reduce(function (json, { name, value }) { + const form_data_obj = jQuery('#wu_form').serializeArray().reduce(function (json, { name, value }) { - // Get products from this - if (name !== 'products[]') { + // Get products from this + if (name !== 'products[]') { - json[name] = value; + json[ name ] = value; - } + } - return json; + return json; - }, {}); + }, {}); - const form_data = jQuery.param({ - ...form_data_obj, - products: this.products, - membership_id: this.membership_id, - payment_id: this.payment_id, - auto_renew: this.auto_renew, - cart_type: this.type, - valid_password: this.valid_password, - duration: this.duration, - duration_unit: this.duration_unit, - }); + const form_data = jQuery.param({ + ...form_data_obj, + products: this.products, + membership_id: this.membership_id, + payment_id: this.payment_id, + auto_renew: this.auto_renew, + cart_type: this.type, + valid_password: this.valid_password, + duration: this.duration, + duration_unit: this.duration_unit, + }); - const that = this; + const that = this; - this.request('wu_validate_form', form_data, function (results) { + this.request('wu_validate_form', form_data, function (results) { - if (!that.valid_password) { + if (! that.valid_password) { - that.errors.push({ - code: 'password', - message: wu_checkout.i18n.weak_password, - }); + that.errors.push({ + code: 'password', + message: wu_checkout.i18n.weak_password, + }); - } // end if; + } // end if; - if (results.success === false) { + if (results.success === false) { - that.errors = [].concat(that.errors, results.data); + that.errors = [].concat(that.errors, results.data); - that.unblock(); + that.unblock(); - return; + return; - } // end if; + } // end if; - if (!that.errors.length) { + if (! that.errors.length) { - that.form_success(results); + that.form_success(results); - if (that.prevent_submission === false) { + if (that.prevent_submission === false) { - that.resubmit(); + that.resubmit(); - } // end if; + } // end if; - } else { + } else { - that.unblock(); + that.unblock(); - } // end if; + } // end if; - }, this.handle_errors); + }, this.handle_errors); - }, - resubmit() { + }, + resubmit() { - jQuery('#wu_form').get(0).submit(); + jQuery('#wu_form').get(0).submit(); - }, - handle_errors(errors) { + }, + handle_errors(errors) { - this.unblock(); + this.unblock(); - // eslint-disable-next-line no-console - console.error(errors); + // eslint-disable-next-line no-console + console.error(errors); - }, - on_submit(event) { + }, + on_submit(event) { - event.preventDefault(); + event.preventDefault(); - }, - on_change_product(new_value, old_value) { + }, + on_change_product(new_value, old_value) { - window.wu_create_cookie('wu_selected_products', new_value.join(','), 0.5) // Save it for 12 hours max. + window.wu_create_cookie('wu_selected_products', new_value.join(','), 0.5) // Save it for 12 hours max. - this.reset_templates(['template-selection']); + this.reset_templates([ 'template-selection' ]); - hooks.doAction('wu_on_change_product', new_value, old_value, this); + hooks.doAction('wu_on_change_product', new_value, old_value, this); - this.create_order(); + this.create_order(); - }, - on_change_gateway(new_value, old_value) { + }, + on_change_gateway(new_value, old_value) { - hooks.doAction('wu_on_change_gateway', new_value, old_value, this); + hooks.doAction('wu_on_change_gateway', new_value, old_value, this); - }, - on_change_country(new_value, old_value) { + }, + on_change_country(new_value, old_value) { - hooks.doAction('wu_on_change_country', new_value, old_value, this); + hooks.doAction('wu_on_change_country', new_value, old_value, this); - this.create_order(); + this.create_order(); - }, - on_change_state(new_value, old_value) { + }, + on_change_state(new_value, old_value) { - hooks.doAction('wu_on_change_state', new_value, old_value, this); + hooks.doAction('wu_on_change_state', new_value, old_value, this); - this.create_order(); + this.create_order(); - }, - on_change_city(new_value, old_value) { + }, + on_change_city(new_value, old_value) { - hooks.doAction('wu_on_change_city', new_value, old_value, this); + hooks.doAction('wu_on_change_city', new_value, old_value, this); - this.create_order(); + this.create_order(); - }, - on_change_duration(new_value, old_value) { + }, + on_change_duration(new_value, old_value) { - this.reset_templates(); + this.reset_templates(); - hooks.doAction('wu_on_change_duration', new_value, old_value, this); + hooks.doAction('wu_on_change_duration', new_value, old_value, this); - this.create_order(); + this.create_order(); - }, - on_change_duration_unit(new_value, old_value) { + }, + on_change_duration_unit(new_value, old_value) { - this.reset_templates(); + this.reset_templates(); - hooks.doAction('wu_on_change_duration_unit', new_value, old_value, this); + hooks.doAction('wu_on_change_duration_unit', new_value, old_value, this); - this.create_order(); + this.create_order(); - }, - on_change_discount_code(new_value, old_value) { + }, + on_change_discount_code(new_value, old_value) { - hooks.doAction('wu_on_change_discount_code', new_value, old_value, this); + hooks.doAction('wu_on_change_discount_code', new_value, old_value, this); - this.create_order(); + this.create_order(); - }, - block() { + }, + block() { - /* + /* * Get the first bg color from a parent. */ - const bg_color = jQuery(this.$el).parents().filter(function () { + const bg_color = jQuery(this.$el).parents().filter(function () { - return $(this).css('backgroundColor') !== 'rgba(0, 0, 0, 0)'; + return $(this).css('backgroundColor') !== 'rgba(0, 0, 0, 0)'; - }).first().css('backgroundColor'); + }).first().css('backgroundColor'); - jQuery(this.$el).wu_block({ - message: '
', - overlayCSS: { - backgroundColor: bg_color ? bg_color : '#ffffff', - opacity: 0.6, - }, - css: { - padding: 0, - margin: 0, - width: '50%', - fontSize: '14px !important', - top: '40%', - left: '35%', - textAlign: 'center', - color: '#000', - border: 'none', - backgroundColor: 'none', - cursor: 'wait', - }, - }); + jQuery(this.$el).wu_block({ + message: '
', + overlayCSS: { + backgroundColor: bg_color ? bg_color : '#ffffff', + opacity: 0.6, + }, + css: { + padding: 0, + margin: 0, + width: '50%', + fontSize: '14px !important', + top: '40%', + left: '35%', + textAlign: 'center', + color: '#000', + border: 'none', + backgroundColor: 'none', + cursor: 'wait', + }, + }); - }, - unblock() { + }, + unblock() { - jQuery(this.$el).wu_unblock(); + jQuery(this.$el).wu_unblock(); - }, - request(action, data, success_handler, error_handler) { + }, + request(action, data, success_handler, error_handler) { - const actual_ajax_url = (action === 'wu_validate_form' || action === 'wu_create_order' || action === 'wu_render_field_template' || action === 'wu_check_user_exists' || action === 'wu_inline_login') ? wu_checkout.late_ajaxurl : wu_checkout.ajaxurl; + const actual_ajax_url = (action === 'wu_validate_form' || action === 'wu_create_order' || action === 'wu_render_field_template' || action === 'wu_check_user_exists' || action === 'wu_inline_login') ? wu_checkout.late_ajaxurl : wu_checkout.ajaxurl; - jQuery.ajax({ - method: 'POST', - url: actual_ajax_url + '&action=' + action, - data, - success: success_handler, - error: error_handler, - }); + jQuery.ajax({ + method: 'POST', + url: actual_ajax_url + '&action=' + action, + data, + success: success_handler, + error: error_handler, + }); - }, - init_password_strength() { + }, + init_password_strength() { - const that = this; - const pass1_el = jQuery('#field-password'); + const that = this; + const pass1_el = jQuery('#field-password'); - if (!pass1_el.length) { + if (! pass1_el.length) { - return; + return; - } // end if; + } // end if; - // Use the shared WU_PasswordStrength utility - if (typeof window.WU_PasswordStrength !== 'undefined') { + // Use the shared WU_PasswordStrength utility + if (typeof window.WU_PasswordStrength !== 'undefined') { - this.password_strength_checker = new window.WU_PasswordStrength({ - pass1: pass1_el, - result: jQuery('#pass-strength-result'), - minStrength: 3, - onValidityChange: function(isValid) { + // Set valid_password to false initially since password field exists and needs validation + this.valid_password = false; - that.valid_password = isValid; + this.password_strength_checker = new window.WU_PasswordStrength({ + pass1: pass1_el, + result: jQuery('#pass-strength-result'), + minStrength: 3, + onValidityChange(isValid) { - } - }); + that.valid_password = isValid; - } // end if; + } + }); - }, - check_user_exists_debounced: _.debounce(function(field_type, value) { + } // end if; - this.check_user_exists(field_type, value); + }, + check_user_exists_debounced: _.debounce(function(field_type, value) { - }, 500), - check_user_exists(field_type, value) { + this.check_user_exists(field_type, value); - // Don't check if value is too short - if (!value || value.length < 3) { + }, 500), + check_user_exists(field_type, value) { - this.show_login_prompt = false; + // Don't check if value is too short + if (! value || value.length < 3) { - return; + this.show_login_prompt = false; - } + return; - this.checking_user_exists = true; - this.login_error = ''; + } - const that = this; + this.checking_user_exists = true; + this.login_error = ''; - this.request('wu_check_user_exists', { - field_type: field_type, - value: value, - _wpnonce: jQuery('[name="_wpnonce"]').val() - }, function(results) { + const that = this; - that.checking_user_exists = false; + this.request('wu_check_user_exists', { + field_type, + value, + _wpnonce: jQuery('[name="_wpnonce"]').val() + }, function(results) { - if (results.success && results.data.exists) { + that.checking_user_exists = false; - that.show_login_prompt = true; - that.login_prompt_field = field_type; + if (results.success && results.data.exists) { - } else { + that.show_login_prompt = true; + that.login_prompt_field = field_type; - that.show_login_prompt = false; + } else { - } + that.show_login_prompt = false; - }, function(error) { + } - that.checking_user_exists = false; - that.show_login_prompt = false; + }, function(error) { - }); + that.checking_user_exists = false; + that.show_login_prompt = false; - }, - handle_inline_login(event) { + }); - console.log('handle_inline_login called', event); + }, + handle_inline_login(event) { - // Prevent any default behavior or form submission - if (event) { + // Prevent any default behavior or form submission + if (event) { - event.preventDefault(); - event.stopPropagation(); - event.stopImmediatePropagation(); + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); - } + } - if (!this.inline_login_password) { + if (! this.inline_login_password) { - this.login_error = wu_checkout.i18n.password_required || 'Password is required'; + this.login_error = wu_checkout.i18n.password_required || 'Password is required'; - return false; + return false; - } + } - this.logging_in = true; - this.login_error = ''; + this.logging_in = true; + this.login_error = ''; - const that = this; - const username_or_email = this.login_prompt_field === 'email' - ? this.email_address || '' - : this.username || ''; + const that = this; + const username_or_email = this.login_prompt_field === 'email' + ? this.email_address || '' + : this.username || ''; - this.request('wu_inline_login', { - username_or_email: username_or_email, - password: this.inline_login_password, - _wpnonce: jQuery('[name="_wpnonce"]').val() - }, function(results) { + this.request('wu_inline_login', { + username_or_email, + password: this.inline_login_password, + _wpnonce: jQuery('[name="_wpnonce"]').val() + }, function(results) { - that.logging_in = false; + that.logging_in = false; - if (results.success) { + if (results.success) { - // Login successful - reload page to show logged-in state - window.location.reload(); + // Login successful - reload page to show logged-in state + window.location.reload(); - } + } - }, function(error) { + }, function(error) { - that.logging_in = false; + that.logging_in = false; - if (error.responseJSON && error.responseJSON.data && error.responseJSON.data.message) { + if (error.responseJSON && error.responseJSON.data && error.responseJSON.data.message) { - that.login_error = error.responseJSON.data.message; + that.login_error = error.responseJSON.data.message; - } else { + } else { - that.login_error = wu_checkout.i18n.login_failed || 'Login failed. Please try again.'; + that.login_error = wu_checkout.i18n.login_failed || 'Login failed. Please try again.'; - } + } - }); + }); - return false; + return false; - }, - dismiss_login_prompt() { + }, + dismiss_login_prompt() { - this.show_login_prompt = false; - this.inline_login_password = ''; - this.login_error = ''; + this.show_login_prompt = false; + this.inline_login_password = ''; + this.login_error = ''; - }, - setup_inline_login_handlers() { + }, + setup_inline_login_handlers() { - const that = this; + const that = this; - // Setup handlers for both email and username field types - ['email', 'username'].forEach(function(fieldType) { + // Setup handlers for both email and username field types + [ 'email', 'username' ].forEach(function(fieldType) { - const passwordField = document.getElementById('wu-inline-login-password-' + fieldType); - const submitButton = document.getElementById('wu-inline-login-submit-' + fieldType); - const dismissButton = document.getElementById('wu-dismiss-login-prompt-' + fieldType); - const errorDiv = document.getElementById('wu-login-error-' + fieldType); - const loginPromptContainer = document.getElementById('wu-inline-login-prompt-' + fieldType); + const passwordField = document.getElementById('wu-inline-login-password-' + fieldType); + const submitButton = document.getElementById('wu-inline-login-submit-' + fieldType); - if (!passwordField || !submitButton) return; + if (! passwordField || ! submitButton) { + return; + } - // Remove any existing listeners to avoid duplicates - const newSubmitButton = submitButton.cloneNode(true); - submitButton.parentNode.replaceChild(newSubmitButton, submitButton); + const dismissButton = document.getElementById('wu-dismiss-login-prompt-' + fieldType); + const errorDiv = document.getElementById('wu-login-error-' + fieldType); + const loginPromptContainer = document.getElementById('wu-inline-login-prompt-' + fieldType); - const newPasswordField = passwordField.cloneNode(true); - passwordField.parentNode.replaceChild(newPasswordField, passwordField); - function handleError(error) { + // Remove any existing listeners to avoid duplicates + const newSubmitButton = submitButton.cloneNode(true); + submitButton.parentNode.replaceChild(newSubmitButton, submitButton); - newSubmitButton.disabled = false; - newSubmitButton.textContent = wu_checkout.i18n.sign_in || 'Sign in'; + const newPasswordField = passwordField.cloneNode(true); + passwordField.parentNode.replaceChild(newPasswordField, passwordField); + function handleError(error) { - if (error.data && error.data.message) { + newSubmitButton.disabled = false; + newSubmitButton.textContent = wu_checkout.i18n.sign_in || 'Sign in'; - errorDiv.textContent = error.data.message; + if (error.data && error.data.message) { - } else { + errorDiv.textContent = error.data.message; - errorDiv.textContent = wu_checkout.i18n.login_failed || 'Login failed. Please try again.'; + } else { - } + errorDiv.textContent = wu_checkout.i18n.login_failed || 'Login failed. Please try again.'; - errorDiv.style.display = 'block'; + } + errorDiv.style.display = 'block'; - } + } - function handleLogin(e) { + function handleLogin(e) { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); - const password = newPasswordField.value; + const password = newPasswordField.value; - if (!password) { + if (! password) { - errorDiv.textContent = wu_checkout.i18n.password_required || 'Password is required'; - errorDiv.style.display = 'block'; + errorDiv.textContent = wu_checkout.i18n.password_required || 'Password is required'; + errorDiv.style.display = 'block'; - return false; + return false; - } + } - newSubmitButton.disabled = true; - newSubmitButton.innerHTML = '' + (wu_checkout.i18n.logging_in || 'Logging in...'); - errorDiv.style.display = 'none'; + newSubmitButton.disabled = true; + newSubmitButton.innerHTML = '' + (wu_checkout.i18n.logging_in || 'Logging in...'); + errorDiv.style.display = 'none'; - const username_or_email = fieldType === 'email' ? that.email_address : that.username; + const username_or_email = fieldType === 'email' ? that.email_address : that.username; - jQuery.ajax({ - method: 'POST', - url: wu_checkout.late_ajaxurl + '&action=wu_inline_login', - data: { - username_or_email: username_or_email, - password: password, - _wpnonce: jQuery('[name="_wpnonce"]').val() - }, - success: function(results) { + jQuery.ajax({ + method: 'POST', + url: wu_checkout.late_ajaxurl + '&action=wu_inline_login', + data: { + username_or_email, + password, + _wpnonce: jQuery('[name="_wpnonce"]').val() + }, + success(results) { - if (results.success) { + if (results.success) { - window.location.reload(); + window.location.reload(); - } else { - handleError(results); - } + } else { + handleError(results); + } - }, - error: handleError - }); + }, + error: handleError + }); - return false; + return false; - } + } - // Stop all events from bubbling out of the login prompt - if (loginPromptContainer) { + // Stop all events from bubbling out of the login prompt + if (loginPromptContainer) { - loginPromptContainer.addEventListener('click', function(e) { + loginPromptContainer.addEventListener('click', function(e) { - e.stopPropagation(); + e.stopPropagation(); - }); + }); - loginPromptContainer.addEventListener('keydown', function(e) { + loginPromptContainer.addEventListener('keydown', function(e) { - e.stopPropagation(); + e.stopPropagation(); - }); + }); - loginPromptContainer.addEventListener('keyup', function(e) { + loginPromptContainer.addEventListener('keyup', function(e) { - e.stopPropagation(); + e.stopPropagation(); - }); + }); - } + } - newSubmitButton.addEventListener('click', handleLogin); + newSubmitButton.addEventListener('click', handleLogin); - newPasswordField.addEventListener('keydown', function(e) { + newPasswordField.addEventListener('keydown', function(e) { - if (e.key === 'Enter') { + if (e.key === 'Enter') { - handleLogin(e); + handleLogin(e); - } + } - }); + }); - if (dismissButton) { + if (dismissButton) { - dismissButton.addEventListener('click', function(e) { + dismissButton.addEventListener('click', function(e) { - e.preventDefault(); - e.stopPropagation(); - that.show_login_prompt = false; - that.inline_login_password = ''; - newPasswordField.value = ''; + e.preventDefault(); + e.stopPropagation(); + that.show_login_prompt = false; + that.inline_login_password = ''; + newPasswordField.value = ''; - }); + }); - } + } - }); + }); - }, - }, - updated() { + }, + }, + updated() { - this.$nextTick(function () { + this.$nextTick(function () { - hooks.doAction('wu_on_form_updated', this); + hooks.doAction('wu_on_form_updated', this); - wu_initialize_tooltip(); + wu_initialize_tooltip(); - // Setup inline login handlers if prompt is visible - this.setup_inline_login_handlers(); + // Setup inline login handlers if prompt is visible + this.setup_inline_login_handlers(); - }); + }); - }, - mounted() { + }, + mounted() { - const that = this; + const that = this; - jQuery(this.$el).on('click', function (e) { + jQuery(this.$el).on('click', function (e) { - $(this).data('submited_via', $(e.target)); + $(this).data('submited_via', $(e.target)); - }); + }); - jQuery(this.$el).on('submit', async function (e) { + jQuery(this.$el).on('submit', async function (e) { - e.preventDefault(); + e.preventDefault(); - /** - * Handle button submission. - */ - const submit_el = jQuery(this).data('submited_via'); + /** + * Handle button submission. + */ + const submit_el = jQuery(this).data('submited_via'); - if (submit_el) { + if (submit_el) { - const new_input = jQuery(''); + const new_input = jQuery(''); - new_input.attr('type', 'hidden'); + new_input.attr('type', 'hidden'); - new_input.attr('name', submit_el.attr('name')); + new_input.attr('name', submit_el.attr('name')); - new_input.attr('value', submit_el.val()); + new_input.attr('value', submit_el.val()); - jQuery(this).append(new_input); + jQuery(this).append(new_input); - } // end if; + } // end if; - that.block(); + that.block(); - try { + try { - const promises = []; + const promises = []; - // Here we use filter to return possible promises to await - await Promise.all(hooks.applyFilters("wu_before_form_submitted", promises, that, that.gateway)); + // Here we use filter to return possible promises to await + await Promise.all(hooks.applyFilters("wu_before_form_submitted", promises, that, that.gateway)); - } catch (error) { + } catch (error) { - that.errors = []; + that.errors = []; - that.errors.push({ - code: 'before-submit-error', - message: error.message, - }); + that.errors.push({ + code: 'before-submit-error', + message: error.message, + }); - that.unblock(); + that.unblock(); - that.handle_errors(error); + that.handle_errors(error); - return; + return; - } // end try; + } // end try; - that.validate_form(); + that.validate_form(); - hooks.doAction('wu_on_form_submitted', that, that.gateway); + hooks.doAction('wu_on_form_submitted', that, that.gateway); - }); + }); - this.create_order(); + this.create_order(); - hooks.doAction('wu_checkout_loaded', this); + hooks.doAction('wu_checkout_loaded', this); - hooks.doAction('wu_on_change_gateway', this.gateway, this.gateway); + hooks.doAction('wu_on_change_gateway', this.gateway, this.gateway); - // Initialize password strength checker using the shared utility - this.init_password_strength(); + // Initialize password strength checker using the shared utility + this.init_password_strength(); - wu_initialize_tooltip(); + wu_initialize_tooltip(); - }, - watch: { - products(new_value, old_value) { + }, + watch: { + products(new_value, old_value) { - this.on_change_product(new_value, old_value); + this.on_change_product(new_value, old_value); - }, - toggle_discount_code(new_value) { + }, + toggle_discount_code(new_value) { - if (!new_value) { + if (! new_value) { - this.discount_code = ''; + this.discount_code = ''; - } // end if; + } // end if; - }, - discount_code(new_value, old_value) { + }, + discount_code(new_value, old_value) { - this.on_change_discount_code(new_value, old_value); + this.on_change_discount_code(new_value, old_value); - }, - gateway(new_value, old_value) { + }, + gateway(new_value, old_value) { - this.on_change_gateway(new_value, old_value); + this.on_change_gateway(new_value, old_value); - }, - country(new_value, old_value) { + }, + country(new_value, old_value) { - this.state = ''; + this.state = ''; - this.on_change_country(new_value, old_value); + this.on_change_country(new_value, old_value); - }, - state(new_value, old_value) { + }, + state(new_value, old_value) { - this.city = ''; + this.city = ''; - this.on_change_state(new_value, old_value); + this.on_change_state(new_value, old_value); - }, - city(new_value, old_value) { + }, + city(new_value, old_value) { - this.on_change_city(new_value, old_value); + this.on_change_city(new_value, old_value); - }, - duration(new_value, old_value) { + }, + duration(new_value, old_value) { - this.on_change_duration(new_value, old_value); + this.on_change_duration(new_value, old_value); - }, - duration_unit(new_value, old_value) { + }, + duration_unit(new_value, old_value) { - this.on_change_duration_unit(new_value, old_value); + this.on_change_duration_unit(new_value, old_value); - }, - }, - }); + }, + }, + }); - }); + }); }(jQuery, wp.hooks, _)); From e95460bd221b1bbfec3f856ebf9769dc48c5c4c7 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 3 Feb 2026 15:17:28 -0700 Subject: [PATCH 25/32] Restructure E2E tests with programmatic setup and working checkout flows Replace fragile UI-based wizard setup with WP-CLI/PHP fixture approach: - Add PHP fixtures for DB tables, products, checkout form, trial products, and verification scripts - Create 000-setup, 010-manual-checkout-flow, 020-free-trial-flow specs - Fix wpCli command (npx wp-env instead of npm run env) and add wpCliFile - Remove broken checkout commands and old specs that used vague selectors - Remove domain-mapping-roles and plugin specs (incompatible with wp-env) - Fix wizard.spec.js: handle variable button text on installer steps, add missing recommended-plugins step, reset custom login page on setup - Remove WooCommerce addon from core .wp-env.json test plugins - Add env:fix-perms script for Docker permission issues All 15 tests across 7 specs pass reliably on consecutive runs. Co-Authored-By: Claude Opus 4.5 --- .wp-env.json | 3 +- package.json | 3 +- .../cypress/fixtures/setup-checkout-form.php | 44 ++ tests/e2e/cypress/fixtures/setup-product.php | 15 + tests/e2e/cypress/fixtures/setup-tables.php | 15 + .../cypress/fixtures/setup-trial-product.php | 44 ++ .../verify-manual-checkout-results.php | 29 + .../cypress/fixtures/verify-trial-results.php | 25 + .../e2e/cypress/integration/000-setup.spec.js | 65 +++ .../010-manual-checkout-flow.spec.js | 98 ++++ .../integration/020-free-trial-flow.spec.js | 97 ++++ .../integration/checkout-confirmation.spec.js | 373 ------------- .../integration/checkout-registration.spec.js | 422 --------------- .../integration/checkout-scenarios.spec.js | 421 --------------- .../integration/checkout-validation.spec.js | 417 -------------- .../integration/domain-mapping-roles.md | 137 ----- .../integration/domain-mapping-roles.spec.js | 406 -------------- tests/e2e/cypress/integration/plugin.spec.js | 23 - .../integration/setup-wizard-complete.spec.js | 508 ------------------ tests/e2e/cypress/integration/wizard.spec.js | 30 +- .../e2e/cypress/support/commands/checkout.js | 409 -------------- tests/e2e/cypress/support/commands/index.js | 34 +- 22 files changed, 494 insertions(+), 3124 deletions(-) create mode 100644 tests/e2e/cypress/fixtures/setup-checkout-form.php create mode 100644 tests/e2e/cypress/fixtures/setup-product.php create mode 100644 tests/e2e/cypress/fixtures/setup-tables.php create mode 100644 tests/e2e/cypress/fixtures/setup-trial-product.php create mode 100644 tests/e2e/cypress/fixtures/verify-manual-checkout-results.php create mode 100644 tests/e2e/cypress/fixtures/verify-trial-results.php create mode 100644 tests/e2e/cypress/integration/000-setup.spec.js create mode 100644 tests/e2e/cypress/integration/010-manual-checkout-flow.spec.js create mode 100644 tests/e2e/cypress/integration/020-free-trial-flow.spec.js delete mode 100644 tests/e2e/cypress/integration/checkout-confirmation.spec.js delete mode 100644 tests/e2e/cypress/integration/checkout-registration.spec.js delete mode 100644 tests/e2e/cypress/integration/checkout-scenarios.spec.js delete mode 100644 tests/e2e/cypress/integration/checkout-validation.spec.js delete mode 100644 tests/e2e/cypress/integration/domain-mapping-roles.md delete mode 100644 tests/e2e/cypress/integration/domain-mapping-roles.spec.js delete mode 100644 tests/e2e/cypress/integration/plugin.spec.js delete mode 100644 tests/e2e/cypress/integration/setup-wizard-complete.spec.js delete mode 100644 tests/e2e/cypress/support/commands/checkout.js diff --git a/.wp-env.json b/.wp-env.json index 0c543e10..5e599888 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -23,8 +23,7 @@ "tests": { "multisite": true, "plugins": [ - ".", - "../addons/ultimate-multisite-woocommerce" + "." ], "mappings": { "wp-content/mu-plugins/email-smtp-test.php": "./mu-plugins/email-smtp-test/email-smtp-test.php", diff --git a/package.json b/package.json index 77f3c5e6..e05c4c0d 100644 --- a/package.json +++ b/package.json @@ -74,7 +74,8 @@ "clean:cache": "rm -f .phpunit.result.cache && rm -rf node_modules/.cache", "translate": "php scripts/translate.php", "translate:force": "php scripts/translate.php --force", - "env:start": "wp-env start", + "env:fix-perms": "docker run --rm -v \"$HOME/.wp-env:/wp-env\" alpine chown -R \"$(id -u):$(id -g)\" /wp-env || true", + "env:start": "npm run env:fix-perms && wp-env start", "env:stop": "wp-env stop", "env:clean": "wp-env clean all", "env:destroy": "wp-env destroy all", diff --git a/tests/e2e/cypress/fixtures/setup-checkout-form.php b/tests/e2e/cypress/fixtures/setup-checkout-form.php new file mode 100644 index 00000000..10aa59e1 --- /dev/null +++ b/tests/e2e/cypress/fixtures/setup-checkout-form.php @@ -0,0 +1,44 @@ + 'main-form', 'number' => 1 ] ); + +if ( $existing ) { + $form = $existing[0]; + $page_id = wu_get_setting( 'default_registration_page', 0 ); + echo 'form:' . $form->get_id() . ',page:' . $page_id; + return; +} + +$form_data = [ + 'name' => 'Registration Form', + 'slug' => 'main-form', + 'settings' => [], +]; + +$form = wu_create_checkout_form( $form_data ); + +if ( is_wp_error( $form ) ) { + echo 'error:' . $form->get_error_message(); + return; +} + +$form->use_template( 'single-step' ); +$form->save(); + +$page_id = wp_insert_post( + [ + 'post_name' => 'register', + 'post_title' => 'Register', + 'post_content' => '[wu_checkout slug="main-form"]', + 'post_status' => 'publish', + 'post_type' => 'page', + 'post_author' => 1, + ] +); + +wu_save_setting( 'default_registration_page', $page_id ); + +echo 'form:' . $form->get_id() . ',page:' . $page_id; diff --git a/tests/e2e/cypress/fixtures/setup-product.php b/tests/e2e/cypress/fixtures/setup-product.php new file mode 100644 index 00000000..8ad1d085 --- /dev/null +++ b/tests/e2e/cypress/fixtures/setup-product.php @@ -0,0 +1,15 @@ +set_name( 'Test Plan' ); +$product->set_slug( 'test-plan' ); +$product->set_amount( 29.99 ); +$product->set_duration( 1 ); +$product->set_duration_unit( 'month' ); +$product->set_type( 'plan' ); +$product->set_active( true ); +$product->save(); + +echo $product->get_id(); diff --git a/tests/e2e/cypress/fixtures/setup-tables.php b/tests/e2e/cypress/fixtures/setup-tables.php new file mode 100644 index 00000000..857b7784 --- /dev/null +++ b/tests/e2e/cypress/fixtures/setup-tables.php @@ -0,0 +1,15 @@ +init(); + +if ( ! $loader->is_installed() ) { + $installer = WP_Ultimo\Installers\Core_Installer::get_instance(); + $installer->_install_database_tables(); +} + +update_network_option( null, WP_Ultimo::NETWORK_OPTION_SETUP_FINISHED, time() ); + +echo $loader->is_installed() ? 'installed' : 'failed'; diff --git a/tests/e2e/cypress/fixtures/setup-trial-product.php b/tests/e2e/cypress/fixtures/setup-trial-product.php new file mode 100644 index 00000000..9efaf2a9 --- /dev/null +++ b/tests/e2e/cypress/fixtures/setup-trial-product.php @@ -0,0 +1,44 @@ +set_name( 'Trial Plan' ); +$product->set_slug( 'trial-plan' ); +$product->set_amount( 19.99 ); +$product->set_duration( 1 ); +$product->set_duration_unit( 'month' ); +$product->set_trial_duration( 14 ); +$product->set_trial_duration_unit( 'day' ); +$product->set_type( 'plan' ); +$product->set_active( true ); +$product->save(); + +$product_id = $product->get_id(); + +// Add the trial product to the checkout form's pricing table. +$form = WP_Ultimo\Models\Checkout_Form::query( [ 'number' => 1 ] ); + +if ( $form ) { + $form = $form[0]; + $settings = $form->get_settings(); + + foreach ( $settings as &$step ) { + if ( ! isset( $step['fields'] ) ) { + continue; + } + + foreach ( $step['fields'] as &$field ) { + if ( isset( $field['id'] ) && 'pricing_table' === $field['id'] ) { + $existing = $field['pricing_table_products'] ?? ''; + $field['pricing_table_products'] = $existing ? $existing . ',' . $product_id : (string) $product_id; + } + } + } + + $form->set_settings( $settings ); + $form->save(); +} + +echo $product_id; diff --git a/tests/e2e/cypress/fixtures/verify-manual-checkout-results.php b/tests/e2e/cypress/fixtures/verify-manual-checkout-results.php new file mode 100644 index 00000000..ae98b523 --- /dev/null +++ b/tests/e2e/cypress/fixtures/verify-manual-checkout-results.php @@ -0,0 +1,29 @@ + 1, 'orderby' => 'id', 'order' => 'DESC' ] ); +$um_payment_status = $payments ? $payments[0]->get_status() : 'no-payments'; +$um_payment_gateway = $payments ? $payments[0]->get_gateway() : 'none'; +$um_payment_total = $payments ? (float) $payments[0]->get_total() : 0; + +// UM membership +$memberships = WP_Ultimo\Models\Membership::query( [ 'number' => 1, 'orderby' => 'id', 'order' => 'DESC' ] ); +$um_membership_status = $memberships ? $memberships[0]->get_status() : 'no-memberships'; + +// UM sites +$sites = WP_Ultimo\Models\Site::query( [ 'type__in' => [ 'customer_owned' ] ] ); +$um_site_count = count( $sites ); +$um_site_type = $sites ? $sites[0]->get_type() : 'no-sites'; + +echo json_encode( [ + 'um_payment_status' => $um_payment_status, + 'um_payment_gateway' => $um_payment_gateway, + 'um_payment_total' => $um_payment_total, + 'um_membership_status' => $um_membership_status, + 'um_site_count' => $um_site_count, + 'um_site_type' => $um_site_type, +] ); diff --git a/tests/e2e/cypress/fixtures/verify-trial-results.php b/tests/e2e/cypress/fixtures/verify-trial-results.php new file mode 100644 index 00000000..4725e8c9 --- /dev/null +++ b/tests/e2e/cypress/fixtures/verify-trial-results.php @@ -0,0 +1,25 @@ + 1, 'orderby' => 'id', 'order' => 'DESC' ] ); +$um_payment_status = $payments ? $payments[0]->get_status() : 'no-payments'; + +// UM membership +$memberships = WP_Ultimo\Models\Membership::query( [ 'number' => 1, 'orderby' => 'id', 'order' => 'DESC' ] ); +$um_membership_status = $memberships ? $memberships[0]->get_status() : 'no-memberships'; +$um_membership_trial_end = $memberships ? (string) $memberships[0]->get_date_trial_end() : ''; + +// UM sites +$sites = WP_Ultimo\Models\Site::query( [ 'type__in' => [ 'customer_owned' ] ] ); +$um_site_type = $sites ? $sites[0]->get_type() : 'no-sites'; + +echo json_encode( [ + 'um_payment_status' => $um_payment_status, + 'um_membership_status' => $um_membership_status, + 'um_membership_trial_end' => $um_membership_trial_end, + 'um_site_type' => $um_site_type, +] ); diff --git a/tests/e2e/cypress/integration/000-setup.spec.js b/tests/e2e/cypress/integration/000-setup.spec.js new file mode 100644 index 00000000..1189109b --- /dev/null +++ b/tests/e2e/cypress/integration/000-setup.spec.js @@ -0,0 +1,65 @@ +describe("Ultimate Multisite Setup", () => { + before(() => { + // Disable custom login page if a previous wizard run enabled it, + // otherwise /wp-login.php redirects to /login/ and loginByForm breaks. + cy.wpCli( + 'eval "if (function_exists(\'wu_save_setting\')) { wu_save_setting(\'enable_custom_login_page\', 0); }"', + { failOnNonZeroExit: false } + ); + + cy.loginByForm( + Cypress.env("admin").username, + Cypress.env("admin").password + ); + }); + + it("Should install Ultimate Multisite database tables and mark setup complete", () => { + cy.wpCliFile("tests/e2e/cypress/fixtures/setup-tables.php").then( + (result) => { + expect(result.stdout).to.contain("installed"); + } + ); + }); + + it("Should create a test product/plan", () => { + cy.wpCliFile("tests/e2e/cypress/fixtures/setup-product.php", { + failOnNonZeroExit: false, + }).then((result) => { + const productId = result.stdout.trim(); + cy.log(`Created test product with ID: ${productId}`); + }); + }); + + it("Should create a checkout form and registration page", () => { + cy.wpCliFile( + "tests/e2e/cypress/fixtures/setup-checkout-form.php" + ).then((result) => { + expect(result.stdout).to.contain("form:"); + expect(result.stdout).to.not.contain("error:"); + }); + }); + + it("Should enable the manual gateway in Ultimate Multisite", () => { + cy.wpCli( + "eval \"wu_save_setting('active_gateways', ['manual']);\"" + ); + + cy.wpCli( + "eval \"echo json_encode(wu_get_setting('active_gateways', []));\"" + ).then((result) => { + expect(result.stdout).to.contain("manual"); + }); + }); + + it("Should disable email verification and enable sync site publish", () => { + cy.wpCli( + "eval \"wu_save_setting('enable_email_verification', 'never'); wu_save_setting('force_publish_sites_sync', true);\"" + ); + + cy.wpCli( + "eval \"echo wu_get_setting('enable_email_verification', 'always');\"" + ).then((result) => { + expect(result.stdout).to.contain("never"); + }); + }); +}); diff --git a/tests/e2e/cypress/integration/010-manual-checkout-flow.spec.js b/tests/e2e/cypress/integration/010-manual-checkout-flow.spec.js new file mode 100644 index 00000000..109b9a07 --- /dev/null +++ b/tests/e2e/cypress/integration/010-manual-checkout-flow.spec.js @@ -0,0 +1,98 @@ +describe("Manual Gateway Checkout Flow", () => { + const timestamp = Date.now(); + const customerData = { + username: `manualcust${timestamp}`, + email: `manualcust${timestamp}@test.com`, + password: "TestPassword123!", + }; + const siteData = { + title: "Manual Test Site", + path: `manualsite${timestamp}`, + }; + + it("Should complete the UM checkout form with manual gateway", { + retries: 0, + }, () => { + cy.clearCookies(); + cy.visit("/register", { failOnStatusCode: false }); + + // Wait for checkout form to render + cy.get("#field-email_address", { timeout: 30000 }).should( + "be.visible" + ); + cy.wait(3000); + + // Select the plan + cy.get('#wrapper-field-pricing_table label[id^="wu-product-"]', { + timeout: 15000, + }) + .first() + .click(); + + cy.wait(3000); + + // Fill account details + cy.get("#field-email_address").clear().type(customerData.email); + cy.get("#field-username").should("be.visible").clear().type(customerData.username); + cy.get("#field-password").should("be.visible").clear().type(customerData.password); + + cy.get("body").then(($body) => { + if ($body.find("#field-password_conf").length > 0) { + cy.get("#field-password_conf").clear().type(customerData.password); + } + }); + + // Fill site details + cy.get("#field-site_title").should("be.visible").clear().type(siteData.title); + cy.get("#field-site_url").should("be.visible").clear().type(siteData.path); + + // Select manual gateway if visible radio + cy.get("body").then(($body) => { + const radioSelector = 'input[type="radio"][name="gateway"][value="manual"]'; + if ($body.find(radioSelector).length > 0) { + cy.get(radioSelector).check({ force: true }); + } + }); + + // Fill billing address if present + cy.get("body").then(($body) => { + if ($body.find("#field-billing_country").length > 0) { + cy.get("#field-billing_country").select("US"); + } else if ($body.find('[name="billing_address[billing_country]"]').length > 0) { + cy.get('[name="billing_address[billing_country]"]').select("US"); + } + + if ($body.find("#field-billing_zip_code").length > 0) { + cy.get("#field-billing_zip_code").clear().type("94105"); + } else if ($body.find('[name="billing_address[billing_zip_code]"]').length > 0) { + cy.get('[name="billing_address[billing_zip_code]"]').clear().type("94105"); + } + }); + + // Submit the UM checkout form + cy.get( + '#wrapper-field-checkout button[type="submit"], button.wu-checkout-submit, #field-checkout, button[type="submit"]', + { timeout: 10000 } + ) + .filter(":visible") + .last() + .click(); + + // Should redirect to status=done + cy.url({ timeout: 60000 }).should("include", "status=done"); + }); + + it("Should verify checkout state via WP-CLI", () => { + cy.wpCliFile("tests/e2e/cypress/fixtures/verify-manual-checkout-results.php").then( + (result) => { + const data = JSON.parse(result.stdout.trim()); + cy.log(`Results: ${JSON.stringify(data)}`); + + // Manual gateway: payment should be pending + expect(data.um_payment_status).to.equal("pending"); + expect(data.um_payment_gateway).to.equal("manual"); + expect(data.um_membership_status).to.equal("pending"); + } + ); + }); +}); diff --git a/tests/e2e/cypress/integration/020-free-trial-flow.spec.js b/tests/e2e/cypress/integration/020-free-trial-flow.spec.js new file mode 100644 index 00000000..d932b1b6 --- /dev/null +++ b/tests/e2e/cypress/integration/020-free-trial-flow.spec.js @@ -0,0 +1,97 @@ +describe("Free Trial Checkout Flow", () => { + const timestamp = Date.now(); + const customerData = { + username: `trialcust${timestamp}`, + email: `trialcust${timestamp}@test.com`, + password: "TestPassword123!", + }; + const siteData = { + title: "Trial Test Site", + path: `trialsite${timestamp}`, + }; + + before(() => { + cy.loginByForm( + Cypress.env("admin").username, + Cypress.env("admin").password + ); + + // Enable trial without payment method + cy.wpCli( + "eval \"wu_save_setting('allow_trial_without_payment_method', true);\"" + ); + + // Create the trial product + cy.wpCliFile("tests/e2e/cypress/fixtures/setup-trial-product.php", { + failOnNonZeroExit: false, + }).then((result) => { + const productId = result.stdout.trim(); + cy.log(`Created trial product with ID: ${productId}`); + }); + }); + + it("Should complete free trial checkout without payment", { + retries: 0, + }, () => { + cy.clearCookies(); + cy.visit("/register", { failOnStatusCode: false }); + + // Wait for checkout form to render + cy.get("#field-email_address", { timeout: 30000 }).should( + "be.visible" + ); + cy.wait(3000); + + // Select the trial plan by name + cy.get('#wrapper-field-pricing_table label[id^="wu-product-"]', { + timeout: 15000, + }) + .contains("Trial Plan") + .click(); + + cy.wait(3000); + + // Fill account details + cy.get("#field-email_address").clear().type(customerData.email); + cy.get("#field-username").should("be.visible").clear().type(customerData.username); + cy.get("#field-password").should("be.visible").clear().type(customerData.password); + + cy.get("body").then(($body) => { + if ($body.find("#field-password_conf").length > 0) { + cy.get("#field-password_conf").clear().type(customerData.password); + } + }); + + // Fill site details + cy.get("#field-site_title").should("be.visible").clear().type(siteData.title); + cy.get("#field-site_url").should("be.visible").clear().type(siteData.path); + + // No gateway or billing fields should be required for free trial + + // Submit the checkout form + cy.get( + '#wrapper-field-checkout button[type="submit"], button.wu-checkout-submit, #field-checkout, button[type="submit"]', + { timeout: 10000 } + ) + .filter(":visible") + .last() + .click(); + + // Should redirect to status=done without any payment page + cy.url({ timeout: 60000 }).should("include", "status=done"); + }); + + it("Should verify trial membership state via WP-CLI", () => { + cy.wpCliFile( + "tests/e2e/cypress/fixtures/verify-trial-results.php" + ).then((result) => { + const data = JSON.parse(result.stdout.trim()); + cy.log(`Trial results: ${JSON.stringify(data)}`); + + expect(data.um_membership_status).to.equal("trialing"); + expect(data.um_membership_trial_end).to.not.equal(""); + expect(data.um_payment_status).to.equal("completed"); + expect(data.um_site_type).to.equal("customer_owned"); + }); + }); +}); diff --git a/tests/e2e/cypress/integration/checkout-confirmation.spec.js b/tests/e2e/cypress/integration/checkout-confirmation.spec.js deleted file mode 100644 index 6970948e..00000000 --- a/tests/e2e/cypress/integration/checkout-confirmation.spec.js +++ /dev/null @@ -1,373 +0,0 @@ -/** - * E2E tests for checkout confirmation and post-registration flow - * - * This test suite covers the confirmation page, email verification, - * and post-registration user experience. - */ - -describe("Checkout Confirmation & Post-Registration", () => { - const testCustomer = { - username: `confirmuser_${Date.now()}`, - email: `confirmuser_${Date.now()}@example.com`, - password: 'ConfirmPass123!', - firstName: 'Sarah', - lastName: 'Wilson' - }; - - const testSite = { - title: 'Confirmation Test Site', - path: `confirmsite_${Date.now()}` - }; - - describe("Successful Registration Confirmation", () => { - before(() => { - // Complete a full registration to test confirmation - cy.visitCheckoutForm('registration'); - cy.selectPricingPlan(0); - cy.fillAccountDetails(testCustomer); - cy.proceedToNextStep(); - cy.fillSiteDetails(testSite); - cy.proceedToNextStep(); - - // Handle payment/completion - cy.get('body').then($body => { - if ($body.find('[name*="billing"]').length > 0) { - cy.fillBillingAddress(); - cy.selectPaymentGateway('manual'); - } - }); - - cy.completeCheckout(); - }); - - it("Should display confirmation page with correct information", () => { - // Verify we're on confirmation page - cy.url({ timeout: 30000 }).should('satisfy', url => - url.includes('/confirmation') || - url.includes('/thank') || - url.includes('/success') || - url.includes('/complete') - ); - - // Verify page title/heading - cy.get('h1, h2, .wu-title, [data-testid="page-title"]') - .should('be.visible') - .and('contain.text', /success|complete|welcome|thank|congratulations/i); - - // Verify success message - cy.get('[data-testid="success-message"], .wu-success, .notice-success, [class*="success"]') - .should('be.visible') - .and('contain.text', /success|complete|registered|created/i); - }); - - it("Should display customer information correctly", () => { - // Check customer email - cy.get('[data-testid="customer-email"], .customer-email, .user-email') - .should('contain.text', testCustomer.email); - - // Check username if displayed - cy.get('body').then($body => { - if ($body.find('[data-testid="customer-username"], .customer-username').length > 0) { - cy.get('[data-testid="customer-username"], .customer-username') - .should('contain.text', testCustomer.username); - } - }); - - // Check full name if displayed - cy.get('body').then($body => { - if ($body.find('[data-testid="customer-name"], .customer-name').length > 0) { - cy.get('[data-testid="customer-name"], .customer-name') - .should('contain.text', testCustomer.firstName) - .or('contain.text', testCustomer.lastName); - } - }); - }); - - it("Should display site information correctly", () => { - // Check site title - cy.get('[data-testid="site-title"], .site-title, .site-name') - .should('contain.text', testSite.title); - - // Check site URL - cy.get('[data-testid="site-url"], .site-url, .site-address') - .should('contain.text', testSite.path); - - // Check site status - cy.get('[data-testid="site-status"], .site-status') - .should('contain.text', /active|ready|live|created/i); - }); - - it("Should provide navigation options", () => { - // Check for dashboard link - cy.get('body').then($body => { - if ($body.find('[data-testid="dashboard-link"], .dashboard-link, a:contains("Dashboard")').length > 0) { - cy.get('[data-testid="dashboard-link"], .dashboard-link, a:contains("Dashboard")') - .should('be.visible') - .and('have.attr', 'href') - .and('contain', '/wp-admin'); - } - }); - - // Check for site visit link - cy.get('body').then($body => { - if ($body.find('[data-testid="visit-site"], .visit-site, a:contains("Visit")').length > 0) { - cy.get('[data-testid="visit-site"], .visit-site, a:contains("Visit")') - .should('be.visible') - .and('have.attr', 'href'); - } - }); - - // Check for login link - cy.get('body').then($body => { - if ($body.find('[data-testid="login-link"], .login-link, a:contains("Login")').length > 0) { - cy.get('[data-testid="login-link"], .login-link, a:contains("Login")') - .should('be.visible') - .and('have.attr', 'href') - .and('contain', 'login'); - } - }); - }); - }); - - describe("Email Verification Process", () => { - it("Should display email verification notice if required", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="email-verification"], .email-verification, :contains("verify")').length > 0) { - cy.get('[data-testid="email-verification"], .email-verification') - .should('be.visible') - .and('contain.text', /verify.*email|check.*email|activation/i); - - // Should show email address - cy.get('[data-testid="verification-email"], .verification-email') - .should('contain.text', testCustomer.email); - - // Should have resend option - cy.get('body').then($resendBody => { - if ($resendBody.find('[data-testid="resend-verification"], .resend-verification').length > 0) { - cy.get('[data-testid="resend-verification"], .resend-verification') - .should('be.visible') - .and('contain.text', /resend|send.*again/i); - } - }); - } - }); - }); - - it("Should handle email verification link clicks", () => { - // Test resend functionality if available - cy.get('body').then($body => { - if ($body.find('[data-testid="resend-verification"], .resend-verification').length > 0) { - cy.get('[data-testid="resend-verification"], .resend-verification').click(); - - // Should show confirmation message - cy.get('[data-testid="resend-success"], .resend-success, [class*="success"]') - .should('be.visible') - .and('contain.text', /sent|resent|check.*email/i); - } - }); - }); - }); - - describe("Site Access and Functionality", () => { - it("Should allow access to site dashboard", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="dashboard-link"], .dashboard-link').length > 0) { - cy.get('[data-testid="dashboard-link"], .dashboard-link').then($link => { - const href = $link.attr('href'); - if (href) { - // Visit the dashboard link - cy.visit(href); - - // Should be on a dashboard page - cy.url().should('contain', '/wp-admin'); - - // Should show dashboard elements - cy.get('#wpadminbar, .wp-admin, #adminmenu').should('exist'); - } - }); - } else { - cy.log('No dashboard link found on confirmation page'); - } - }); - }); - - it("Should allow access to frontend site", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="visit-site"], .visit-site').length > 0) { - cy.get('[data-testid="visit-site"], .visit-site').then($link => { - const href = $link.attr('href'); - if (href) { - // Visit the site - cy.visit(href); - - // Should show site title somewhere - cy.get('title, h1, .site-title, .site-name') - .should('contain.text', testSite.title); - } - }); - } else { - // Try to construct site URL manually - cy.visit(`//${testSite.path}.localhost:8889`); - cy.get('title, h1, .site-title').should('contain.text', testSite.title); - } - }); - }); - }); - - describe("Payment Confirmation", () => { - it("Should display payment information for paid plans", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="payment-info"], .payment-info, .order-summary').length > 0) { - // Check payment status - cy.get('[data-testid="payment-status"], .payment-status') - .should('contain.text', /paid|complete|pending|manual/i); - - // Check payment method - cy.get('[data-testid="payment-method"], .payment-method') - .should('be.visible'); - - // Check amount if displayed - cy.get('body').then($amountBody => { - if ($amountBody.find('[data-testid="payment-amount"], .payment-amount').length > 0) { - cy.get('[data-testid="payment-amount"], .payment-amount') - .should('match', /\$[\d.,]+/); - } - }); - } - }); - }); - - it("Should show next payment date for recurring plans", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="next-payment"], .next-payment, :contains("next payment")').length > 0) { - cy.get('[data-testid="next-payment"], .next-payment') - .should('be.visible') - .and('contain.text', /next.*payment|renewal/i); - } - }); - }); - }); - - describe("Plan and Limitation Information", () => { - it("Should display selected plan details", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="plan-info"], .plan-info, .membership-info').length > 0) { - cy.get('[data-testid="plan-info"], .plan-info, .membership-info') - .should('be.visible'); - - // Check plan name - cy.get('[data-testid="plan-name"], .plan-name') - .should('be.visible'); - - // Check limitations if displayed - cy.get('body').then($limitBody => { - if ($limitBody.find('[data-testid="plan-limits"], .plan-limits').length > 0) { - cy.get('[data-testid="plan-limits"], .plan-limits') - .should('be.visible'); - } - }); - } - }); - }); - }); - - describe("Next Steps and Onboarding", () => { - it("Should provide getting started information", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="getting-started"], .getting-started, :contains("getting started")').length > 0) { - cy.get('[data-testid="getting-started"], .getting-started') - .should('be.visible') - .and('contain.text', /getting.*started|next.*steps|what.*next/i); - } - }); - }); - - it("Should show support or help information", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="support-info"], .support-info, :contains("support")').length > 0) { - cy.get('[data-testid="support-info"], .support-info') - .should('be.visible'); - - // Check for support links - cy.get('[data-testid="support-link"], .support-link, a:contains("support")') - .should('have.attr', 'href'); - } - }); - }); - }); - - describe("Error Handling on Confirmation Page", () => { - it("Should handle missing registration data gracefully", () => { - // Visit confirmation page directly without registration - cy.visit('/confirmation'); - - // Should either redirect or show appropriate error - cy.url().then(url => { - if (url.includes('confirmation')) { - // If we stay on confirmation page, should show error or empty state - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible') - .or(() => { - cy.get('[data-testid="no-data"], .no-data, :contains("no registration")') - .should('be.visible'); - }); - } else { - // Should redirect to appropriate page (registration, login, etc.) - cy.url().should('satisfy', redirectUrl => - redirectUrl.includes('/checkout') || - redirectUrl.includes('/login') || - redirectUrl.includes('/register') - ); - } - }); - }); - }); - - describe("Social Sharing and Notifications", () => { - it("Should provide social sharing options if available", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="social-share"], .social-share').length > 0) { - cy.get('[data-testid="social-share"], .social-share') - .should('be.visible'); - - // Check for common social platforms - const socialPlatforms = ['facebook', 'twitter', 'linkedin']; - socialPlatforms.forEach(platform => { - cy.get(`[data-testid="${platform}-share"], .${platform}-share`).then($social => { - if ($social.length > 0) { - cy.wrap($social).should('be.visible'); - } - }); - }); - } - }); - }); - }); - - describe("Accessibility and SEO", () => { - it("Should have proper accessibility attributes", () => { - // Check for proper heading hierarchy - cy.get('h1').should('exist').and('have.length', 1); - - // Check for proper form labels if any forms exist - cy.get('input').each($input => { - const id = $input.attr('id'); - if (id) { - cy.get(`label[for="${id}"]`).should('exist'); - } - }); - - // Check for alt text on images - cy.get('img').each($img => { - cy.wrap($img).should('have.attr', 'alt'); - }); - }); - - it("Should have proper meta tags and page title", () => { - cy.title().should('contain.text', /success|complete|confirmation/i); - - // Check for meta description - cy.get('meta[name="description"]').should('exist'); - }); - }); -}); \ No newline at end of file diff --git a/tests/e2e/cypress/integration/checkout-registration.spec.js b/tests/e2e/cypress/integration/checkout-registration.spec.js deleted file mode 100644 index 6e25ba9d..00000000 --- a/tests/e2e/cypress/integration/checkout-registration.spec.js +++ /dev/null @@ -1,422 +0,0 @@ -/** - * E2E tests for the complete checkout registration flow - * - * This test suite covers the happy path for new user registration - * including all typical checkout steps: product selection, user details, - * site details, payment processing, and confirmation. - */ - -describe("Checkout Registration Flow", () => { - // Test data for consistent usage across tests - const testCustomer = { - username: `testuser_${Date.now()}`, - email: `testuser_${Date.now()}@example.com`, - password: 'TestPassword123!', - firstName: 'John', - lastName: 'Doe' - }; - - const testSite = { - title: 'Test Site', - path: `testsite_${Date.now()}` - }; - - beforeEach(() => { - // Visit the registration/checkout page - // Note: This assumes there's a checkout form with slug 'registration' - cy.visit('/checkout/registration'); - }); - - it("Should complete the full registration checkout flow successfully", () => { - // Step 1: Plan/Product Selection - cy.log("Starting Step 1: Plan Selection"); - - cy.get('[data-testid="pricing-table"], .wu-pricing-table, [id*="pricing"], [class*="plan"]') - .should('be.visible') - .first() - .within(() => { - cy.get('button, .wu-button, [type="submit"]').first().click(); - }); - - // Verify we moved to the next step - cy.get('[data-testid="checkout-step"], .wu-step, [class*="step"]') - .should('contain.text', '2') - .or('contain.text', 'Account') - .or('contain.text', 'Details'); - - // Step 2: Account/User Details - cy.log("Starting Step 2: Account Details"); - - // Fill in username - cy.get('#username, [name="username"], [data-testid="username"]') - .should('be.visible') - .clear() - .type(testCustomer.username); - - // Fill in email - cy.get('#email, [name="email"], [data-testid="email"]') - .should('be.visible') - .clear() - .type(testCustomer.email); - - // Fill in password - cy.get('#password, [name="password"], [data-testid="password"]') - .should('be.visible') - .clear() - .type(testCustomer.password); - - // Fill in confirm password if it exists - cy.get('body').then(($body) => { - if ($body.find('#password_confirmation, [name="password_confirmation"], [data-testid="password-confirm"]').length > 0) { - cy.get('#password_confirmation, [name="password_confirmation"], [data-testid="password-confirm"]') - .clear() - .type(testCustomer.password); - } - }); - - // Fill in first name if it exists - cy.get('body').then(($body) => { - if ($body.find('#first_name, [name="first_name"], [data-testid="first-name"]').length > 0) { - cy.get('#first_name, [name="first_name"], [data-testid="first-name"]') - .clear() - .type(testCustomer.firstName); - } - }); - - // Fill in last name if it exists - cy.get('body').then(($body) => { - if ($body.find('#last_name, [name="last_name"], [data-testid="last-name"]').length > 0) { - cy.get('#last_name, [name="last_name"], [data-testid="last-name"]') - .clear() - .type(testCustomer.lastName); - } - }); - - // Continue to next step - cy.get('[data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/continue|next|proceed/i) - .should('not.be.disabled') - .click(); - - // Step 3: Site Details - cy.log("Starting Step 3: Site Details"); - - // Verify we're on the site details step - cy.get('[data-testid="checkout-step"], .wu-step, [class*="step"]') - .should('contain.text', '3') - .or('contain.text', 'Site') - .or('contain.text', 'Domain'); - - // Fill in site title - cy.get('#site_title, [name="site_title"], [data-testid="site-title"]') - .should('be.visible') - .clear() - .type(testSite.title); - - // Fill in site path/URL - cy.get('#site_url, [name="site_url"], [data-testid="site-url"], [name="blogname"]') - .should('be.visible') - .clear() - .type(testSite.path); - - // Select template if template selection exists - cy.get('body').then(($body) => { - if ($body.find('[data-testid="template-selection"], .wu-template-selection, [class*="template"]').length > 0) { - cy.get('[data-testid="template-selection"], .wu-template-selection, [class*="template"]') - .first() - .click(); - } - }); - - // Continue to payment step - cy.get('[data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/continue|next|proceed/i) - .should('not.be.disabled') - .click(); - - // Step 4: Payment Details (if not free) - cy.log("Starting Step 4: Payment Processing"); - - // Check if this is a free plan or paid plan - cy.get('body').then(($body) => { - const hasFreeIndicator = $body.find('[data-testid="free-plan"], .wu-free-plan, [class*="free"]').length > 0; - const hasPaymentFields = $body.find('[data-testid="payment-form"], .wu-payment-form, [name*="card"], [name*="billing"]').length > 0; - - if (hasFreeIndicator || !hasPaymentFields) { - cy.log("Free plan detected - skipping payment details"); - - // For free plans, just click continue/complete - cy.get('[data-testid="complete-btn"], [data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/complete|finish|continue|create/i) - .should('not.be.disabled') - .click(); - - } else { - cy.log("Paid plan detected - filling payment details"); - - // Fill in billing address if required - cy.get('body').then(($billingBody) => { - if ($billingBody.find('[name="billing_address"], [data-testid="billing-address"]').length > 0) { - cy.get('[name="billing_address[address_line_1]"], [name="billing_address_line_1"]') - .type('123 Test Street'); - cy.get('[name="billing_address[city]"], [name="billing_city"]') - .type('Test City'); - cy.get('[name="billing_address[state]"], [name="billing_state"]') - .type('CA'); - cy.get('[name="billing_address[zip_code]"], [name="billing_zip"]') - .type('12345'); - } - }); - - // Select Manual Payment gateway (most reliable for testing) - cy.get('[data-testid="gateway-manual"], [value="manual"], [data-gateway="manual"]') - .should('be.visible') - .click(); - - // Complete the payment - cy.get('[data-testid="complete-btn"], .wu-button, button[type="submit"]') - .contains(/complete|finish|pay/i) - .should('not.be.disabled') - .click(); - } - }); - - // Step 5: Confirmation/Thank You - cy.log("Step 5: Verifying Registration Completion"); - - // Wait for redirect to confirmation page or success message - cy.url({ timeout: 30000 }).should('contain', '/confirmation') - .or('contain', '/thank') - .or('contain', '/success'); - - // Verify success message - cy.get('[data-testid="success-message"], .wu-success, .notice-success, [class*="success"]') - .should('be.visible') - .and('contain.text', /success|complete|welcome|thank/i); - - // Verify customer details are displayed - cy.get('[data-testid="customer-info"], .wu-customer-info') - .should('contain.text', testCustomer.email); - - // Verify site information is displayed - cy.get('[data-testid="site-info"], .wu-site-info') - .should('contain.text', testSite.title); - }); - - it("Should validate required fields on each step", () => { - cy.log("Testing field validation across checkout steps"); - - // Step 1: Try to proceed without selecting a plan - cy.get('[data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/continue|next|proceed/i) - .click(); - - // Should show validation error or still be on the same step - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible') - .or(() => { - cy.get('[data-testid="checkout-step"], .wu-step, [class*="step"]') - .should('contain.text', '1'); - }); - - // Select a plan to proceed - cy.get('[data-testid="pricing-table"], .wu-pricing-table, [id*="pricing"], [class*="plan"]') - .should('be.visible') - .first() - .within(() => { - cy.get('button, .wu-button, [type="submit"]').first().click(); - }); - - // Step 2: Try to proceed with empty required fields - cy.get('[data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/continue|next|proceed/i) - .click(); - - // Should show validation errors - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - - // Verify specific field errors - cy.get('#username, [name="username"]').then(($username) => { - if ($username.length > 0) { - cy.wrap($username).should('have.attr', 'required') - .or('have.class', 'error') - .or('have.class', 'invalid'); - } - }); - - cy.get('#email, [name="email"]').then(($email) => { - if ($email.length > 0) { - cy.wrap($email).should('have.attr', 'required') - .or('have.class', 'error') - .or('have.class', 'invalid'); - } - }); - }); - - it("Should handle email validation correctly", () => { - cy.log("Testing email field validation"); - - // Navigate to account step - cy.get('[data-testid="pricing-table"], .wu-pricing-table, [id*="pricing"], [class*="plan"]') - .should('be.visible') - .first() - .within(() => { - cy.get('button, .wu-button, [type="submit"]').first().click(); - }); - - // Test invalid email formats - const invalidEmails = ['invalid', 'invalid@', '@invalid.com', 'invalid.com']; - - invalidEmails.forEach((invalidEmail) => { - cy.get('#email, [name="email"], [data-testid="email"]') - .should('be.visible') - .clear() - .type(invalidEmail); - - cy.get('[data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/continue|next|proceed/i) - .click(); - - // Should show validation error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - }); - - // Test valid email - cy.get('#email, [name="email"], [data-testid="email"]') - .clear() - .type(testCustomer.email); - - // Fill other required fields - cy.get('#username, [name="username"], [data-testid="username"]') - .clear() - .type(testCustomer.username); - - cy.get('#password, [name="password"], [data-testid="password"]') - .clear() - .type(testCustomer.password); - - // Should be able to proceed - cy.get('[data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/continue|next|proceed/i) - .should('not.be.disabled') - .click(); - - // Should advance to next step - cy.get('[data-testid="checkout-step"], .wu-step, [class*="step"]') - .should('contain.text', '3') - .or('contain.text', 'Site') - .or('contain.text', 'Domain'); - }); - - it("Should handle username availability checking", () => { - cy.log("Testing username availability"); - - // Navigate to account step - cy.get('[data-testid="pricing-table"], .wu-pricing-table, [id*="pricing"], [class*="plan"]') - .should('be.visible') - .first() - .within(() => { - cy.get('button, .wu-button, [type="submit"]').first().click(); - }); - - // Test with admin username (should be taken) - cy.get('#username, [name="username"], [data-testid="username"]') - .should('be.visible') - .clear() - .type('admin'); - - cy.get('#email, [name="email"], [data-testid="email"]') - .clear() - .type(testCustomer.email); - - cy.get('#password, [name="password"], [data-testid="password"]') - .clear() - .type(testCustomer.password); - - cy.get('[data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/continue|next|proceed/i) - .click(); - - // Should show username taken error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible') - .and('contain.text', /username|taken|exists/i); - - // Use unique username - cy.get('#username, [name="username"], [data-testid="username"]') - .clear() - .type(testCustomer.username); - - cy.get('[data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/continue|next|proceed/i) - .click(); - - // Should advance to next step - cy.get('[data-testid="checkout-step"], .wu-step, [class*="step"]') - .should('contain.text', '3') - .or('contain.text', 'Site') - .or('contain.text', 'Domain'); - }); - - it("Should validate site URL availability", () => { - cy.log("Testing site URL validation"); - - // Navigate through to site details step - cy.get('[data-testid="pricing-table"], .wu-pricing-table, [id*="pricing"], [class*="plan"]') - .first() - .within(() => { - cy.get('button, .wu-button, [type="submit"]').first().click(); - }); - - // Fill account details - cy.get('#username, [name="username"]').type(testCustomer.username); - cy.get('#email, [name="email"]').type(testCustomer.email); - cy.get('#password, [name="password"]').type(testCustomer.password); - - cy.get('[data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/continue|next|proceed/i) - .click(); - - // Test invalid site URLs - const invalidSiteUrls = ['', ' ', 'site with spaces', 'UPPERCASE', 'site-with-special!']; - - invalidSiteUrls.forEach((invalidUrl) => { - if (invalidUrl.trim()) { - cy.get('#site_url, [name="site_url"], [data-testid="site-url"], [name="blogname"]') - .clear() - .type(invalidUrl); - - cy.get('[data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/continue|next|proceed/i) - .click(); - - // Should show validation error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - } - }); - - // Test valid site URL - cy.get('#site_title, [name="site_title"]').type(testSite.title); - cy.get('#site_url, [name="site_url"], [data-testid="site-url"], [name="blogname"]') - .clear() - .type(testSite.path); - - cy.get('[data-testid="continue-btn"], .wu-button, button[type="submit"]') - .contains(/continue|next|proceed/i) - .click(); - - // Should advance to payment/completion step - cy.url().should('not.contain', 'step=site') - .and('not.contain', 'step=domain'); - }); - - // Cleanup: Remove test data after tests (if needed) - after(() => { - cy.log("Cleanup: Test data should be cleaned up by WordPress/plugin automatically"); - // Note: In a real scenario, you might want to clean up test users/sites - // This could be done via WP-CLI commands or API calls - }); -}); \ No newline at end of file diff --git a/tests/e2e/cypress/integration/checkout-scenarios.spec.js b/tests/e2e/cypress/integration/checkout-scenarios.spec.js deleted file mode 100644 index 8469482e..00000000 --- a/tests/e2e/cypress/integration/checkout-scenarios.spec.js +++ /dev/null @@ -1,421 +0,0 @@ -/** - * E2E tests for different checkout scenarios and edge cases - * - * This test suite covers various checkout scenarios including - * different plan types, payment methods, and special cases. - */ - -describe("Checkout Scenarios", () => { - const testData = { - customers: { - basic: { - username: `basicuser_${Date.now()}`, - email: `basicuser_${Date.now()}@example.com`, - password: 'BasicPass123!', - firstName: 'Jane', - lastName: 'Smith' - }, - premium: { - username: `premiumuser_${Date.now()}`, - email: `premiumuser_${Date.now()}@example.com`, - password: 'PremiumPass123!', - firstName: 'John', - lastName: 'Premium' - } - }, - sites: { - basic: { - title: 'Basic Test Site', - path: `basicsite_${Date.now()}` - }, - premium: { - title: 'Premium Business Site', - path: `premiumsite_${Date.now()}` - } - } - }; - - describe("Free Plan Registration", () => { - it("Should complete registration with free plan", () => { - cy.visitCheckoutForm('registration'); - - // Look for and select free plan - cy.get('body').then($body => { - // Try to find free plan indicators - const freePlanSelectors = [ - '[data-testid*="free"]', - '[class*="free"]', - '[data-price="0"]', - ':contains("Free")', - ':contains("$0")' - ]; - - let freePlanFound = false; - - freePlanSelectors.forEach(selector => { - if (!freePlanFound && $body.find(selector).length > 0) { - cy.get(selector).first().within(() => { - cy.get('button, .wu-button, [type="submit"]').click(); - }); - freePlanFound = true; - } - }); - - if (!freePlanFound) { - // Fallback to first plan (might be free) - cy.selectPricingPlan(0); - } - }); - - // Fill account details - cy.fillAccountDetails(testData.customers.basic); - cy.proceedToNextStep(); - - // Fill site details - cy.fillSiteDetails(testData.sites.basic); - cy.selectSiteTemplate(0); - cy.proceedToNextStep(); - - // Complete registration (should skip payment for free plan) - cy.completeCheckout(); - - // Verify success - cy.verifyCheckoutSuccess({ - email: testData.customers.basic.email, - siteTitle: testData.sites.basic.title - }); - }); - }); - - describe("Paid Plan Registration", () => { - it("Should complete registration with paid plan using manual payment", () => { - cy.visitCheckoutForm('registration'); - - // Select a paid plan (try to find one that's not free) - cy.get('body').then($body => { - const paidPlanSelectors = [ - '[data-testid*="paid"]', - '[class*="premium"]', - '[class*="pro"]', - ':contains("$") [data-testid*="plan"]', - ':not(:contains("Free")) [data-testid*="plan"]' - ]; - - let paidPlanFound = false; - - paidPlanSelectors.forEach(selector => { - if (!paidPlanFound && $body.find(selector).length > 0) { - cy.get(selector).first().within(() => { - cy.get('button, .wu-button, [type="submit"]').click(); - }); - paidPlanFound = true; - } - }); - - if (!paidPlanFound) { - // Fallback to second plan if available - cy.get('[data-testid="pricing-table"], .wu-pricing-table, [id*="pricing"], [class*="plan"]') - .then($plans => { - if ($plans.length > 1) { - cy.selectPricingPlan(1); - } else { - cy.selectPricingPlan(0); - } - }); - } - }); - - // Fill account details - cy.fillAccountDetails(testData.customers.premium); - cy.proceedToNextStep(); - - // Fill site details - cy.fillSiteDetails(testData.sites.premium); - cy.selectSiteTemplate(0); - cy.proceedToNextStep(); - - // Handle payment - cy.fillBillingAddress({ - address: '456 Premium Street', - city: 'Business City', - state: 'NY', - zipCode: '54321' - }); - - cy.selectPaymentGateway('manual'); - cy.completeCheckout(); - - // Verify success - cy.verifyCheckoutSuccess({ - email: testData.customers.premium.email, - siteTitle: testData.sites.premium.title - }); - }); - }); - - describe("Multi-step Navigation", () => { - beforeEach(() => { - cy.visitCheckoutForm('registration'); - cy.selectPricingPlan(0); - }); - - it("Should handle browser back/forward navigation", () => { - // Fill account details and proceed - cy.fillAccountDetails(testData.customers.basic); - cy.proceedToNextStep(); - - // Verify we're on step 3 (site details) - cy.assertCheckoutStep('3'); - - // Go back using browser navigation - cy.go('back'); - - // Should be back on step 2 with form data preserved - cy.assertCheckoutStep('2'); - cy.get('#username, [name="username"]') - .should('have.value', testData.customers.basic.username); - - // Go forward again - cy.go('forward'); - - // Should be on step 3 again - cy.assertCheckoutStep('3'); - }); - - it("Should allow step navigation via step indicators", () => { - // Fill account details - cy.fillAccountDetails(testData.customers.basic); - cy.proceedToNextStep(); - - // Check if step navigation is available - cy.get('body').then($body => { - const hasStepNavigation = $body.find('[data-testid="step-nav"], .wu-step-nav, [class*="step-nav"]').length > 0; - - if (hasStepNavigation) { - // Try to navigate back to step 2 - cy.get('[data-testid="step-2"], [data-step="2"], .step-2').click(); - - // Should be back on account details step - cy.assertCheckoutStep('2'); - cy.get('#username, [name="username"]') - .should('have.value', testData.customers.basic.username); - } else { - cy.log('Step navigation not available'); - } - }); - }); - }); - - describe("Template Selection Scenarios", () => { - beforeEach(() => { - cy.visitCheckoutForm('registration'); - cy.selectPricingPlan(0); - cy.fillAccountDetails(testData.customers.basic); - cy.proceedToNextStep(); - }); - - it("Should handle sites with template selection", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="template-selection"], .wu-template-selection').length > 0) { - // Test selecting different templates - cy.get('[data-testid="template-selection"], .wu-template-selection') - .should('have.length.at.least', 1); - - // Select first template - cy.selectSiteTemplate(0); - - // Fill site details - cy.fillSiteDetails(testData.sites.basic); - cy.proceedToNextStep(); - - // Should proceed to next step - cy.url().should('not.contain', 'template'); - - } else { - cy.log('No template selection available'); - cy.fillSiteDetails(testData.sites.basic); - cy.proceedToNextStep(); - } - }); - }); - - it("Should handle blank/custom template option", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="template-blank"], [data-template="blank"], :contains("Blank")').length > 0) { - cy.get('[data-testid="template-blank"], [data-template="blank"], :contains("Blank")').click(); - - cy.fillSiteDetails(testData.sites.basic); - cy.proceedToNextStep(); - - // Should proceed successfully - cy.url().should('not.contain', 'template'); - } - }); - }); - }); - - describe("Payment Gateway Scenarios", () => { - beforeEach(() => { - cy.visitCheckoutForm('registration'); - cy.selectPricingPlan(0); - cy.fillAccountDetails(testData.customers.basic); - cy.proceedToNextStep(); - cy.fillSiteDetails(testData.sites.basic); - cy.proceedToNextStep(); - }); - - it("Should handle manual payment gateway", () => { - cy.get('body').then($body => { - if ($body.find('[data-gateway="manual"], [value="manual"]').length > 0) { - cy.selectPaymentGateway('manual'); - cy.completeCheckout(); - - // Should complete successfully - cy.verifyCheckoutSuccess({ - email: testData.customers.basic.email, - siteTitle: testData.sites.basic.title - }); - } - }); - }); - - it("Should handle free gateway for zero-cost orders", () => { - cy.get('body').then($body => { - const hasFreeGateway = $body.find('[data-gateway="free"], [value="free"]').length > 0; - const isFreeOrder = $body.find(':contains("$0"), :contains("Free"), [data-price="0"]').length > 0; - - if (hasFreeGateway && isFreeOrder) { - cy.selectPaymentGateway('free'); - cy.completeCheckout(); - - cy.verifyCheckoutSuccess({ - email: testData.customers.basic.email, - siteTitle: testData.sites.basic.title - }); - } - }); - }); - }); - - describe("Discount Code Scenarios", () => { - beforeEach(() => { - cy.visitCheckoutForm('registration'); - cy.selectPricingPlan(0); - }); - - it("Should handle discount code application", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="discount-code"], [name*="discount"], [name*="coupon"]').length > 0) { - // Try applying a discount code - cy.get('[data-testid="discount-code"], [name*="discount"], [name*="coupon"]') - .type('TESTCODE'); - - cy.get('[data-testid="apply-discount"], [data-testid="apply-coupon"], button:contains("Apply")').click(); - - // Should show either success or error message - cy.get('[data-testid="discount-message"], .discount-message, .coupon-message') - .should('be.visible'); - - // Continue with checkout - cy.fillAccountDetails(testData.customers.basic); - cy.proceedToNextStep(); - cy.fillSiteDetails(testData.sites.basic); - cy.proceedToNextStep(); - cy.completeCheckout(); - - cy.verifyCheckoutSuccess({ - email: testData.customers.basic.email, - siteTitle: testData.sites.basic.title - }); - } else { - cy.log('No discount code field found'); - } - }); - }); - }); - - describe("Error Recovery Scenarios", () => { - it("Should handle session timeout gracefully", () => { - cy.visitCheckoutForm('registration'); - cy.selectPricingPlan(0); - cy.fillAccountDetails(testData.customers.basic); - - // Simulate session timeout by clearing cookies - cy.clearCookies(); - - cy.proceedToNextStep(); - - // Should either redirect to login or show session error - cy.url().then(url => { - if (url.includes('login')) { - cy.log('Redirected to login as expected'); - } else { - cy.get('[data-testid="error"], .wu-error, .error') - .should('be.visible') - .and('contain.text', /session|expired|login/i); - } - }); - }); - - it("Should handle network errors during submission", () => { - cy.visitCheckoutForm('registration'); - cy.selectPricingPlan(0); - cy.fillAccountDetails(testData.customers.basic); - cy.proceedToNextStep(); - cy.fillSiteDetails(testData.sites.basic); - cy.proceedToNextStep(); - - // Intercept checkout request to simulate network error - cy.intercept('POST', '**/checkout**', { forceNetworkError: true }).as('checkoutError'); - - cy.completeCheckout(); - - // Should handle error gracefully - cy.wait('@checkoutError'); - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible') - .and('contain.text', /network|connection|try.*again/i); - }); - }); - - describe("Mobile Responsiveness", () => { - beforeEach(() => { - cy.viewport('iphone-x'); - cy.visitCheckoutForm('registration'); - }); - - it("Should complete checkout on mobile device", () => { - cy.selectPricingPlan(0); - cy.fillAccountDetails(testData.customers.basic); - cy.proceedToNextStep(); - cy.fillSiteDetails(testData.sites.basic); - cy.proceedToNextStep(); - cy.completeCheckout(); - - cy.verifyCheckoutSuccess({ - email: testData.customers.basic.email, - siteTitle: testData.sites.basic.title - }); - }); - - it("Should handle mobile form interactions", () => { - // Test that form fields are accessible on mobile - cy.get('#username, [name="username"]').should('be.visible'); - cy.get('#email, [name="email"]').should('be.visible'); - - // Test mobile-specific interactions - cy.selectPricingPlan(0); - - cy.get('#username, [name="username"]').type(testData.customers.basic.username); - cy.get('#email, [name="email"]').type(testData.customers.basic.email); - - // Should not have horizontal scroll - cy.window().its('scrollX').should('equal', 0); - }); - }); - - afterEach(() => { - // Reset viewport for subsequent tests - cy.viewport(1000, 600); - }); -}); \ No newline at end of file diff --git a/tests/e2e/cypress/integration/checkout-validation.spec.js b/tests/e2e/cypress/integration/checkout-validation.spec.js deleted file mode 100644 index 95538d28..00000000 --- a/tests/e2e/cypress/integration/checkout-validation.spec.js +++ /dev/null @@ -1,417 +0,0 @@ -/** - * E2E tests for checkout form validation - * - * This test suite focuses specifically on field validation, - * error handling, and form validation across the checkout flow. - */ - -describe("Checkout Form Validation", () => { - const testData = { - validCustomer: { - username: `testuser_${Date.now()}`, - email: `testuser_${Date.now()}@example.com`, - password: 'ValidPassword123!', - firstName: 'John', - lastName: 'Doe' - }, - validSite: { - title: 'Valid Test Site', - path: `validsite_${Date.now()}` - } - }; - - beforeEach(() => { - cy.visitCheckoutForm('registration'); - }); - - describe("Product Selection Validation", () => { - it("Should require plan selection before proceeding", () => { - // Try to proceed without selecting any plan - cy.proceedToNextStep(); - - // Should show error or remain on same step - cy.hasValidationErrors().then(hasErrors => { - if (!hasErrors) { - // If no explicit error, should still be on step 1 - cy.assertCheckoutStep('1'); - } else { - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible') - .and('contain.text', /select|choose|plan|product/i); - } - }); - }); - - it("Should allow proceeding after plan selection", () => { - cy.selectPricingPlan(0); - - // Should advance to next step - cy.assertCheckoutStep('2'); - }); - }); - - describe("Account Details Validation", () => { - beforeEach(() => { - // Navigate to account details step - cy.selectPricingPlan(0); - }); - - it("Should validate required fields", () => { - // Try to proceed with empty fields - cy.proceedToNextStep(); - - // Should show validation errors - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - - // Check that required fields are marked as invalid - cy.get('#username, [name="username"]').then($username => { - if ($username.length > 0) { - cy.wrap($username).should('satisfy', $el => - $el.attr('required') !== undefined || - $el.hasClass('error') || - $el.hasClass('invalid') || - $el.get(0).checkValidity() === false - ); - } - }); - - cy.get('#email, [name="email"]').then($email => { - if ($email.length > 0) { - cy.wrap($email).should('satisfy', $el => - $el.attr('required') !== undefined || - $el.hasClass('error') || - $el.hasClass('invalid') || - $el.get(0).checkValidity() === false - ); - } - }); - }); - - it("Should validate email format", () => { - const invalidEmails = [ - 'invalid-email', - 'invalid@', - '@invalid.com', - 'invalid.email', - 'spaces @email.com', - 'email@', - 'email@.com' - ]; - - invalidEmails.forEach(invalidEmail => { - cy.get('#email, [name="email"], [data-testid="email"]') - .clear() - .type(invalidEmail); - - cy.get('#username, [name="username"]').type(testData.validCustomer.username); - cy.get('#password, [name="password"]').type(testData.validCustomer.password); - - cy.proceedToNextStep(); - - // Should show validation error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - - // Clear fields for next iteration - cy.get('#email, [name="email"]').clear(); - cy.get('#username, [name="username"]').clear(); - cy.get('#password, [name="password"]').clear(); - }); - }); - - it("Should validate username format and availability", () => { - // Test invalid username formats - const invalidUsernames = [ - '', // empty - 'ab', // too short - 'user name', // spaces - 'user@name', // special characters - 'UPPERCASE', // case sensitivity - '123numericstart' - ]; - - invalidUsernames.forEach(invalidUsername => { - if (invalidUsername.trim() !== '') { - cy.get('#username, [name="username"], [data-testid="username"]') - .clear() - .type(invalidUsername); - - cy.get('#email, [name="email"]').type(testData.validCustomer.email); - cy.get('#password, [name="password"]').type(testData.validCustomer.password); - - cy.proceedToNextStep(); - - // Should show validation error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - - // Clear fields - cy.get('#username, [name="username"]').clear(); - cy.get('#email, [name="email"]').clear(); - cy.get('#password, [name="password"]').clear(); - } - }); - - // Test existing username (admin should exist) - cy.get('#username, [name="username"]').clear().type('admin'); - cy.get('#email, [name="email"]').clear().type(testData.validCustomer.email); - cy.get('#password, [name="password"]').clear().type(testData.validCustomer.password); - - cy.proceedToNextStep(); - - // Should show username taken error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible') - .and('contain.text', /username.*taken|username.*exists|already.*use/i); - }); - - it("Should validate password requirements", () => { - const weakPasswords = [ - '', // empty - '123', // too short - 'password', // too simple - '12345678' // numeric only - ]; - - weakPasswords.forEach(weakPassword => { - if (weakPassword.trim() !== '') { - cy.get('#password, [name="password"], [data-testid="password"]') - .clear() - .type(weakPassword); - - cy.get('#username, [name="username"]').type(testData.validCustomer.username); - cy.get('#email, [name="email"]').type(testData.validCustomer.email); - - cy.proceedToNextStep(); - - // Should show password validation error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - - // Clear fields - cy.get('#password, [name="password"]').clear(); - cy.get('#username, [name="username"]').clear(); - cy.get('#email, [name="email"]').clear(); - } - }); - }); - - it("Should validate password confirmation match", () => { - cy.get('body').then($body => { - // Only test if password confirmation field exists - if ($body.find('#password_confirmation, [name="password_confirmation"]').length > 0) { - cy.get('#username, [name="username"]').type(testData.validCustomer.username); - cy.get('#email, [name="email"]').type(testData.validCustomer.email); - cy.get('#password, [name="password"]').type(testData.validCustomer.password); - cy.get('#password_confirmation, [name="password_confirmation"]').type('DifferentPassword123!'); - - cy.proceedToNextStep(); - - // Should show password mismatch error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible') - .and('contain.text', /password.*match|confirm.*password/i); - } - }); - }); - }); - - describe("Site Details Validation", () => { - beforeEach(() => { - // Navigate to site details step - cy.selectPricingPlan(0); - cy.fillAccountDetails(testData.validCustomer); - cy.proceedToNextStep(); - }); - - it("Should validate required site fields", () => { - // Try to proceed with empty site fields - cy.proceedToNextStep(); - - // Should show validation errors - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - }); - - it("Should validate site URL format", () => { - const invalidSiteUrls = [ - '', // empty - ' ', // only spaces - 'site with spaces', - 'UPPERCASE-SITE', - 'site-with-special!@#', - 'site..double-dots', - '-starting-dash', - 'ending-dash-', - '123numericstart' - ]; - - invalidSiteUrls.forEach(invalidUrl => { - if (invalidUrl.trim()) { - cy.get('#site_url, [name="site_url"], [data-testid="site-url"], [name="blogname"]') - .clear() - .type(invalidUrl); - - cy.get('#site_title, [name="site_title"]').type(testData.validSite.title); - - cy.proceedToNextStep(); - - // Should show validation error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - - // Clear fields - cy.get('#site_url, [name="site_url"], [data-testid="site-url"], [name="blogname"]').clear(); - cy.get('#site_title, [name="site_title"]').clear(); - } - }); - }); - - it("Should validate site title requirements", () => { - const invalidTitles = [ - '', // empty - ' ', // only spaces - 'A', // too short - 'X'.repeat(256) // too long - ]; - - invalidTitles.forEach(invalidTitle => { - cy.get('#site_title, [name="site_title"], [data-testid="site-title"]') - .clear() - .type(invalidTitle); - - cy.get('#site_url, [name="site_url"]').type(testData.validSite.path); - - cy.proceedToNextStep(); - - // Should show validation error for empty/invalid titles - if (invalidTitle.trim() === '' || invalidTitle.length > 255) { - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - } - - // Clear fields - cy.get('#site_title, [name="site_title"]').clear(); - cy.get('#site_url, [name="site_url"]').clear(); - }); - }); - - it("Should check site URL availability", () => { - // Test with existing site URL (main site should exist) - cy.get('#site_url, [name="site_url"], [data-testid="site-url"], [name="blogname"]') - .clear() - .type('main'); // or 'blog' or 'www' - common existing paths - - cy.get('#site_title, [name="site_title"]').type(testData.validSite.title); - - cy.proceedToNextStep(); - - // Should show site URL taken error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible') - .and('contain.text', /site.*taken|url.*exists|already.*use|not.*available/i); - }); - }); - - describe("Payment Validation", () => { - beforeEach(() => { - // Navigate to payment step - cy.selectPricingPlan(0); - cy.fillAccountDetails(testData.validCustomer); - cy.proceedToNextStep(); - cy.fillSiteDetails(testData.validSite); - cy.proceedToNextStep(); - }); - - it("Should handle free plan checkout", () => { - cy.get('body').then($body => { - if ($body.find('[data-testid="free-plan"], .wu-free-plan, [class*="free"]').length > 0) { - // For free plans, should be able to complete directly - cy.completeCheckout(); - - // Should proceed to confirmation - cy.verifyCheckoutSuccess({ - email: testData.validCustomer.email, - siteTitle: testData.validSite.title - }); - } else { - cy.log('Not a free plan, skipping free plan test'); - } - }); - }); - - it("Should validate billing information for paid plans", () => { - cy.get('body').then($body => { - if ($body.find('[name*="billing"], [data-testid*="billing"]').length > 0) { - // Try to proceed without billing info - cy.completeCheckout(); - - // Should show validation error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - - // Fill billing info and try again - cy.fillBillingAddress(); - cy.selectPaymentGateway('manual'); - cy.completeCheckout(); - - // Should proceed successfully - cy.verifyCheckoutSuccess({ - email: testData.validCustomer.email, - siteTitle: testData.validSite.title - }); - } else { - cy.log('No billing fields found, likely free plan'); - } - }); - }); - - it("Should require payment gateway selection", () => { - cy.get('body').then($body => { - const hasPaymentGateways = $body.find('[name*="gateway"], [data-testid*="gateway"]').length > 0; - - if (hasPaymentGateways) { - // Try to complete without selecting gateway - cy.completeCheckout(); - - // Should show validation error - cy.get('[data-testid="error"], .wu-error, .error, [class*="error"]') - .should('be.visible'); - - // Select payment gateway and try again - cy.selectPaymentGateway('manual'); - cy.completeCheckout(); - - // Should proceed successfully - cy.verifyCheckoutSuccess({ - email: testData.validCustomer.email, - siteTitle: testData.validSite.title - }); - } - }); - }); - }); - - describe("Cross-field Validation", () => { - it("Should handle email uniqueness across users", () => { - // This would require testing with existing user email - // Skip if no existing users or implement user creation first - cy.log('Email uniqueness validation would require existing test data'); - }); - - it("Should maintain form state during validation errors", () => { - cy.selectPricingPlan(0); - - // Fill partial form data - cy.get('#username, [name="username"]').type(testData.validCustomer.username); - cy.get('#email, [name="email"]').type('invalid-email'); - - // Try to proceed - cy.proceedToNextStep(); - - // Should show error but maintain username field value - cy.get('#username, [name="username"]').should('have.value', testData.validCustomer.username); - }); - }); -}); \ No newline at end of file diff --git a/tests/e2e/cypress/integration/domain-mapping-roles.md b/tests/e2e/cypress/integration/domain-mapping-roles.md deleted file mode 100644 index 1cc9a4ce..00000000 --- a/tests/e2e/cypress/integration/domain-mapping-roles.md +++ /dev/null @@ -1,137 +0,0 @@ -# Domain Mapping Roles E2E Test - -## Overview - -This test suite verifies the fix for user role loading on domain-mapped sites (commit: `adf85bb`). - -## The Problem Being Tested - -When a WordPress Multisite site is accessed via a custom/mapped domain, user roles may not be properly loaded if domain mapping occurs early (via sunrise.php). This causes issues with: - -- Plugins that check user roles (e.g., If Menu, WooCommerce) -- User capability checks (`current_user_can()`) -- Role-based UI elements (admin menus, etc.) - -## The Fix - -The fix adds a `refresh_user_roles_for_mapped_domain()` method to the `Domain_Mapping` class that: - -1. Hooks into `set_current_user` action -2. Detects when a domain-mapped site is being accessed -3. Calls `$user->for_site($current_blog_id)` to refresh roles for the correct blog context - -## Test Coverage - -The test suite includes 6 comprehensive tests: - -### 1. Baseline Test - Original Subdomain -Verifies that roles work correctly on the original subdomain (establishes baseline). - -### 2. Critical Test - Mapped Domain -**This is the main test for the fix.** Verifies that user roles are correctly loaded when accessing a site via a mapped domain. - -### 3. Plugin Compatibility Test -Simulates how plugins check user roles and ensures the fix works for plugin integrations. - -### 4. Multiple Users Test -Verifies the fix works correctly for multiple users with different roles on the same mapped domain. - -### 5. Role Change Test -Ensures that role changes are reflected when accessing via mapped domain. - -### 6. Inactive Mapping Test -Verifies that inactive domain mappings don't interfere with role loading. - -## Requirements - -### Environment Setup - -1. **WordPress Multisite**: Test requires a multisite installation -2. **Ultimate Multisite Plugin**: Must be active with domain mapping enabled -3. **Test Environment**: Uses `@wordpress/env` or similar - -### Domain Configuration - -**IMPORTANT**: For domain mapping tests to work properly, you need to configure domain resolution. There are several approaches: - -#### Option 1: Hosts File (Recommended for local testing) -Add entries to `/etc/hosts`: -``` -127.0.0.1 test-123456.example.com -127.0.0.1 test-123457.example.com -``` - -#### Option 2: DNS Wildcard (For CI/CD) -Configure wildcard DNS for `*.example.com` pointing to test server. - -#### Option 3: Test Framework Configuration -Some test environments support custom host headers without DNS/hosts file changes. - -### Running the Tests - -```bash -# Open Cypress UI (recommended for debugging) -npm run cy:open:test - -# Run tests headlessly -npm run cy:run:test - -# Run only domain mapping tests -npx cypress run --spec "tests/e2e/cypress/integration/domain-mapping-roles.spec.js" -``` - -## Known Limitations - -1. **Domain Resolution**: Tests may need environment-specific configuration for domain resolution -2. **Timing**: Some tests may need additional wait times depending on server performance -3. **Cleanup**: Test cleanup depends on wp-cli being available in the test environment - -## Troubleshooting - -### Test Fails: "Domain not accessible" -- Check that domain resolution is configured (hosts file or DNS) -- Verify web server accepts the test domains -- Check Ultimate Multisite domain mapping is enabled - -### Test Fails: "User roles empty" -- This indicates the fix may not be working correctly -- Check that the `refresh_user_roles_for_mapped_domain()` method is being called -- Add debug logging to verify the hook is firing - -### Test Fails: "Site creation failed" -- Ensure wp-cli is available in test environment -- Check WordPress multisite is properly configured -- Verify database permissions - -## Debugging - -To debug test failures: - -1. **Run with Cypress UI**: `npm run cy:open:test` -2. **Check Screenshots**: Failed tests capture screenshots in `tests/e2e/cypress/screenshots/` -3. **Check Videos**: Test recordings are in `tests/e2e/cypress/videos/` -4. **Add cy.log()**: Insert additional logging in the test -5. **Use cy.pause()**: Add breakpoints in the test - -## Success Criteria - -All 6 tests should pass, demonstrating that: -- ✓ User roles load correctly on original subdomains (baseline) -- ✓ User roles load correctly on mapped domains (the fix) -- ✓ Plugins can check user roles on mapped domains -- ✓ Multiple users with different roles work correctly -- ✓ Role changes are reflected on mapped domains -- ✓ Inactive mappings don't interfere - -## Related Code - -- **Fix**: `inc/class-domain-mapping.php:refresh_user_roles_for_mapped_domain()` -- **Commit**: `adf85bb` - "fix roles with custom domain" -- **Issue**: User roles not loaded when accessing via custom domain - -## Future Improvements - -- Add tests for more complex role scenarios (custom capabilities) -- Test with actual plugins (If Menu, WooCommerce) -- Add performance tests for role loading -- Test with multiple concurrent users diff --git a/tests/e2e/cypress/integration/domain-mapping-roles.spec.js b/tests/e2e/cypress/integration/domain-mapping-roles.spec.js deleted file mode 100644 index 0112f68f..00000000 --- a/tests/e2e/cypress/integration/domain-mapping-roles.spec.js +++ /dev/null @@ -1,406 +0,0 @@ -/** - * E2E tests for domain mapping with user roles - * - * This test suite verifies that user roles are correctly loaded when accessing - * a site via a custom/mapped domain. This is critical for plugins that check - * user capabilities (like If Menu, WooCommerce, etc.). - * - * Background: - * When domain mapping occurs early (via sunrise.php), WordPress may cache - * user role data before the correct blog context is established. The fix - * in inc/class-domain-mapping.php ensures that user roles are properly - * refreshed for the mapped blog context. - * - * @see inc/class-domain-mapping.php:refresh_user_roles_for_mapped_domain() - */ - -describe("Domain Mapping - User Roles", () => { - // Test data - let testSite = { - id: null, - title: 'Domain Mapped Site', - path: `mapped_${Date.now()}`, - domain: `test-${Date.now()}.example.com` - }; - - let testUser = { - id: null, - username: `domainuser_${Date.now()}`, - email: `domainuser_${Date.now()}@example.com`, - password: 'TestPassword123!', - role: 'editor' - }; - - before(() => { - cy.log("Setting up test environment for domain mapping"); - - // Ensure admin is logged in - cy.loginByApi( - Cypress.env("admin").username, - Cypress.env("admin").password - ); - }); - - /** - * Test 1: Verify user roles are loaded on original domain (baseline) - * - * This test establishes the baseline behavior - when accessing a site - * via its original subdomain, user roles should be properly loaded. - */ - it("Should load user roles correctly on original subdomain (baseline)", () => { - cy.log("Creating test site and user"); - - // Create a new site - cy.createTestSite(testSite.path, testSite.title).then((siteId) => { - testSite.id = siteId; - cy.log(`Created site with ID: ${siteId}`); - - // Create a test user on this site with editor role - cy.createTestUser( - testUser.username, - testUser.email, - testUser.password, - testUser.role, - siteId - ).then((userId) => { - testUser.id = userId; - cy.log(`Created user with ID: ${userId} and role: ${testUser.role}`); - }); - }); - - // Login as the test user - cy.loginByApi(testUser.username, testUser.password); - - // Switch to the test site - cy.switchToSite(testSite.id); - - // Visit the site admin - cy.visit(`/wp-admin/`); - - // Verify user is logged in and has access to admin - cy.get('#wpadminbar').should('be.visible'); - - // Check that user has editor capabilities - // Editors should see Posts menu - cy.get('#menu-posts').should('be.visible'); - - // Editors should NOT see Users menu (admin only) - cy.get('#menu-users').should('not.exist'); - - // Verify user role via API/wp-admin - cy.wpCli(`user get ${testUser.id} --field=roles --format=json`, { - failOnNonZeroExit: false - }).then((result) => { - if (result.code === 0) { - const roles = JSON.parse(result.stdout); - expect(roles).to.include(testUser.role); - } - }); - - cy.log("✓ Baseline test passed - roles work on original subdomain"); - }); - - /** - * Test 2: Verify user roles are loaded on mapped domain - * - * This is the critical test that verifies the fix. When accessing a site - * via a custom/mapped domain, user roles should still be properly loaded. - */ - it("Should load user roles correctly when accessing via mapped domain", () => { - cy.log("Setting up domain mapping for the test site"); - - // Login as admin to set up domain mapping - cy.loginByApi( - Cypress.env("admin").username, - Cypress.env("admin").password - ); - - // Add a custom domain mapping to the test site - cy.addDomainMapping(testSite.id, testSite.domain, true).then(() => { - cy.log(`Added domain mapping: ${testSite.domain} → Site ${testSite.id}`); - }); - - // Now login as the test user - cy.loginByApi(testUser.username, testUser.password); - - // CRITICAL: Access the site via the mapped domain - // This simulates a real-world scenario where users access sites via custom domains - cy.visitMappedDomain(testSite.domain, '/wp-admin/'); - - // Verify user is logged in - cy.get('#wpadminbar', { timeout: 10000 }).should('be.visible'); - - // MAIN TEST: Verify user roles are properly loaded on mapped domain - // The fix ensures that refresh_user_roles_for_mapped_domain() is called - // and user capabilities are correctly initialized - - // Check that user still has editor capabilities - cy.get('#menu-posts').should('be.visible'); - - // Editors should still NOT see Users menu - cy.get('#menu-users').should('not.exist'); - - // Verify via JavaScript that user object has correct roles - cy.window().then((win) => { - // Access WordPress user data if available - if (win.wp && win.wp.data && win.wp.data.select) { - const currentUser = win.wp.data.select('core').getCurrentUser(); - if (currentUser && currentUser.roles) { - expect(currentUser.roles).to.include(testUser.role); - cy.log(`✓ User roles verified via JS: ${currentUser.roles.join(', ')}`); - } - } - }); - - // Test a role-dependent action - creating a post (editors can do this) - cy.visit('/wp-admin/post-new.php'); - cy.get('#title', { timeout: 10000 }).should('be.visible'); - cy.log("✓ User can access post editor (editor capability confirmed)"); - - // Try to access Users page (should be blocked for editor role) - cy.visit('/wp-admin/users.php', { failOnStatusCode: false }); - - // Should see "You do not have permission" message or redirect - cy.get('body').then(($body) => { - const bodyText = $body.text(); - const hasPermissionError = bodyText.includes('permission') || - bodyText.includes('not allowed') || - bodyText.includes('sufficient'); - - if (hasPermissionError) { - cy.log("✓ User correctly denied access to Users page (not an admin)"); - } else { - // Might have redirected away from users.php - cy.url().should('not.contain', 'users.php'); - cy.log("✓ User redirected away from Users page (not an admin)"); - } - }); - - cy.log("✓ CRITICAL TEST PASSED - User roles work correctly on mapped domain!"); - }); - - /** - * Test 3: Verify role-based plugin functionality on mapped domain - * - * This test simulates how plugins like "If Menu" check user roles. - * The fix ensures that plugins checking $current_user->roles get correct data. - */ - it("Should allow plugins to check user roles correctly on mapped domain", () => { - cy.log("Testing role-based functionality (simulating plugins like If Menu)"); - - // Login as test user - cy.loginByApi(testUser.username, testUser.password); - - // Access via mapped domain - cy.visitMappedDomain(testSite.domain, '/wp-admin/'); - - // Execute JavaScript to simulate how plugins check user roles - cy.window().then((win) => { - // Create a custom command to check roles (simulating a plugin) - win.testRoleCheck = function() { - // This simulates what plugins like If Menu do - if (typeof wpApiSettings !== 'undefined' && wpApiSettings.nonce) { - return fetch('/wp-json/wp/v2/users/me', { - headers: { - 'X-WP-Nonce': wpApiSettings.nonce - }, - credentials: 'same-origin' - }) - .then(response => response.json()) - .then(user => user.roles); - } - return Promise.resolve([]); - }; - }); - - // Execute the role check - cy.window().then((win) => { - return cy.wrap(win.testRoleCheck()).then((roles) => { - if (roles && roles.length > 0) { - expect(roles).to.include(testUser.role); - cy.log(`✓ Plugin role check successful: ${roles.join(', ')}`); - } else { - cy.log("⚠ Could not verify via REST API (may not be available)"); - } - }); - }); - - // Test via current_user_can() equivalent - // Check if user can edit posts (editor capability) - cy.visit('/wp-admin/edit.php'); - cy.get('.page-title-action', { timeout: 10000 }).should('contain', 'Add New'); - cy.log("✓ User has edit_posts capability (editor role confirmed)"); - - // Verify user cannot manage options (admin-only capability) - cy.visit('/wp-admin/options-general.php', { failOnStatusCode: false }); - cy.get('body').then(($body) => { - const bodyText = $body.text(); - const hasPermissionError = bodyText.includes('permission') || - bodyText.includes('not allowed'); - expect(hasPermissionError).to.be.true; - cy.log("✓ User correctly denied manage_options capability (not admin)"); - }); - }); - - /** - * Test 4: Verify multiple users with different roles on mapped domain - * - * This test ensures the fix works for multiple users with different roles. - */ - it("Should handle multiple users with different roles on mapped domain", () => { - cy.log("Testing multiple users with different roles"); - - // Create a subscriber user - const subscriberUser = { - username: `subscriber_${Date.now()}`, - email: `subscriber_${Date.now()}@example.com`, - password: 'TestPassword123!', - role: 'subscriber' - }; - - cy.loginByApi( - Cypress.env("admin").username, - Cypress.env("admin").password - ); - - cy.createTestUser( - subscriberUser.username, - subscriberUser.email, - subscriberUser.password, - subscriberUser.role, - testSite.id - ); - - // Test subscriber on mapped domain - cy.loginByApi(subscriberUser.username, subscriberUser.password); - cy.visitMappedDomain(testSite.domain, '/wp-admin/'); - - // Subscribers should have very limited access - cy.get('#wpadminbar').should('be.visible'); - - // Subscribers should NOT see Posts menu - cy.get('#menu-posts').should('not.exist'); - - // Subscribers should only see Profile - cy.get('#menu-users a[href*="profile.php"]').should('be.visible'); - - cy.log("✓ Subscriber role correctly enforced on mapped domain"); - - // Now test the editor again to ensure roles are not mixed up - cy.loginByApi(testUser.username, testUser.password); - cy.visitMappedDomain(testSite.domain, '/wp-admin/'); - - // Editor should still have Posts access - cy.get('#menu-posts').should('be.visible'); - cy.log("✓ Editor role still correct after subscriber login"); - }); - - /** - * Test 5: Verify role changes are reflected on mapped domain - * - * This ensures that when a user's role changes, the change is - * correctly reflected when accessing via mapped domain. - */ - it("Should reflect role changes when accessing via mapped domain", () => { - cy.log("Testing role changes on mapped domain"); - - // Login as admin - cy.loginByApi( - Cypress.env("admin").username, - Cypress.env("admin").password - ); - - // Change test user's role from editor to author - cy.wpCli(`user set-role ${testUser.id} author --url=${testSite.path}`); - - // Login as test user - cy.loginByApi(testUser.username, testUser.password); - - // Access via mapped domain - cy.visitMappedDomain(testSite.domain, '/wp-admin/'); - - // Authors should see Posts menu - cy.get('#menu-posts').should('be.visible'); - - // Authors should NOT see others' posts in edit list - cy.visit('/wp-admin/edit.php'); - - // Verify author capabilities - cy.get('.page-title-action').should('contain', 'Add New'); - cy.log("✓ Author role correctly applied on mapped domain after role change"); - - // Change back to editor for cleanup - cy.loginByApi( - Cypress.env("admin").username, - Cypress.env("admin").password - ); - cy.wpCli(`user set-role ${testUser.id} editor --url=${testSite.path}`); - }); - - /** - * Test 6: Verify inactive domain mappings don't affect roles - * - * This ensures that inactive mappings don't interfere with role loading. - */ - it("Should not interfere with roles when domain mapping is inactive", () => { - cy.log("Testing with inactive domain mapping"); - - // Login as admin - cy.loginByApi( - Cypress.env("admin").username, - Cypress.env("admin").password - ); - - // Deactivate the domain mapping - cy.deactivateDomainMapping(testSite.id, testSite.domain); - - // Login as test user - cy.loginByApi(testUser.username, testUser.password); - - // Access via original subdomain (not mapped domain) - cy.switchToSite(testSite.id); - cy.visit('/wp-admin/'); - - // Roles should still work correctly - cy.get('#menu-posts').should('be.visible'); - cy.log("✓ Roles work correctly when domain mapping is inactive"); - - // Reactivate for cleanup - cy.loginByApi( - Cypress.env("admin").username, - Cypress.env("admin").password - ); - cy.addDomainMapping(testSite.id, testSite.domain, true); - }); - - /** - * Cleanup after all tests - */ - after(() => { - cy.log("Cleaning up test data"); - - cy.loginByApi( - Cypress.env("admin").username, - Cypress.env("admin").password - ); - - // Clean up domain mapping - if (testSite.id && testSite.domain) { - cy.deleteDomainMapping(testSite.id, testSite.domain).then(() => { - cy.log(`Deleted domain mapping: ${testSite.domain}`); - }); - } - - // Clean up test users - if (testUser.id) { - cy.wpCli(`user delete ${testUser.id} --yes`, { failOnNonZeroExit: false }); - cy.log(`Deleted test user: ${testUser.username}`); - } - - // Clean up test site - if (testSite.id) { - cy.wpCli(`site delete ${testSite.id} --yes`, { failOnNonZeroExit: false }); - cy.log(`Deleted test site: ${testSite.path}`); - } - }); -}); diff --git a/tests/e2e/cypress/integration/plugin.spec.js b/tests/e2e/cypress/integration/plugin.spec.js deleted file mode 100644 index cf8c76d8..00000000 --- a/tests/e2e/cypress/integration/plugin.spec.js +++ /dev/null @@ -1,23 +0,0 @@ -describe("Plugin", () => { - beforeEach(() => { - cy.loginByForm( - Cypress.env("admin").username, - Cypress.env("admin").password - ); - }); - - it("Should be able to deactivate the plugin", () => { - cy.visit("/wp-admin/network/plugins.php"); - cy.location("pathname").should("equal", "/wp-admin/network/plugins.php"); - cy.get("#deactivate-ultimate-multisite").scrollIntoView().should("be.visible").click(); - cy.get("#activate-ultimate-multisite").scrollIntoView().should("be.visible"); - }); - - it("Should be able to activate the plugin", () => { - cy.visit("/wp-admin/network/plugins.php"); - cy.location("pathname").should("equal", "/wp-admin/network/plugins.php"); - cy.get("#activate-ultimate-multisite").scrollIntoView().should("be.visible").click(); - cy.location("pathname").should("eq", "/wp-admin/network/admin.php"); - cy.location("search").should("include", "page=wp-ultimo-setup"); - }); -}); diff --git a/tests/e2e/cypress/integration/setup-wizard-complete.spec.js b/tests/e2e/cypress/integration/setup-wizard-complete.spec.js deleted file mode 100644 index 56cd6d33..00000000 --- a/tests/e2e/cypress/integration/setup-wizard-complete.spec.js +++ /dev/null @@ -1,508 +0,0 @@ -/** - * E2E tests for completing the WP Multisite Ultimate setup wizard - * - * This test suite ensures the setup wizard is completed properly, - * which creates the necessary checkout forms and pages required - * for the checkout flow tests. - * - * ⚠️ IMPORTANT: This test should run before any checkout tests - * as it sets up the plugin for use. - */ - -describe("Setup Wizard Completion", () => { - const setupData = { - company: { - name: 'Test Company', - email: 'admin@testcompany.com', - website: 'https://testcompany.com' - }, - settings: { - currency: 'USD', - defaultPlan: 'Basic Plan', - enableRegistration: true - } - }; - - before(() => { - // Enable detailed logging - cy.log('🔧 Starting Setup Wizard E2E Test'); - - // Check environment - cy.log('Environment check:', Cypress.env()); - - // Login as admin before starting setup - const adminUsername = Cypress.env("admin") && Cypress.env("admin").username || 'admin'; - const adminPassword = Cypress.env("admin") && Cypress.env("admin").password || 'password'; - - cy.log(`Attempting login with username: ${adminUsername}`); - - cy.loginByApi(adminUsername, adminPassword).then(() => { - cy.log('✅ Login successful'); - }).catch((error) => { - cy.log('❌ Login failed:', error); - throw error; - }); - }); - - describe("Complete Setup Wizard Flow", () => { - it("Should navigate to setup wizard if not completed", () => { - cy.log('🔍 Checking for setup wizard requirement'); - - // First, let's verify WordPress is working - cy.visit('/wp-admin/network/', { - failOnStatusCode: false, - timeout: 30000 - }); - - cy.url().then(url => { - cy.log('Current URL:', url); - }); - - // Take a screenshot to see what we're dealing with - cy.screenshot('network-admin-initial'); - - cy.get('body').then($body => { - const bodyText = $body.text(); - cy.log('Page content preview:', bodyText.substring(0, 200)); - - // Check for WordPress admin indicators - if ($body.find('#wpadminbar, #adminmenu, .wp-admin').length === 0) { - cy.log('❌ Not on WordPress admin page'); - throw new Error('Expected WordPress admin interface not found'); - } - - // Look for setup wizard indicators - const setupIndicators = [ - '[href*="wp-ultimo-setup"]', - '.wu-setup-wizard', - ':contains("Setup Wizard")', - ':contains("Setup WP Ultimo")', - '[data-testid="setup-wizard"]' - ]; - - let foundSetup = false; - setupIndicators.forEach(selector => { - if ($body.find(selector).length > 0) { - cy.log(`✅ Setup wizard indicator found: ${selector}`); - foundSetup = true; - } - }); - - if (foundSetup) { - cy.log('📋 Setup wizard found - navigating to setup'); - - // Try multiple selectors to navigate to setup - cy.get('[href*="wp-ultimo-setup"], .wu-setup-wizard, a:contains("Setup")') - .first() - .should('be.visible') - .click(); - - } else { - cy.log('🔗 No setup wizard link found, trying direct URL'); - // Try direct URL - cy.visit('/wp-admin/network/admin.php?page=wp-ultimo-setup', { - failOnStatusCode: false - }); - } - }); - - // Verify we're on setup wizard page - cy.url({ timeout: 10000 }).should('contain', 'wp-ultimo-setup'); - cy.screenshot('setup-wizard-page'); - cy.log('✅ Successfully navigated to setup wizard'); - }); - - it("Should complete the Welcome step", () => { - cy.log("🎯 Starting Welcome Step"); - - // Verify we're on welcome step - cy.url({ timeout: 10000 }).should('contain', 'wp-ultimo-setup'); - cy.screenshot('welcome-step-start'); - - cy.get('body').then($body => { - const pageText = $body.text(); - cy.log('Welcome page content preview:', pageText.substring(0, 300)); - - // Look for welcome indicators - const hasWelcome = /welcome|setup|getting.*started/i.test(pageText); - if (!hasWelcome) { - cy.log('⚠️ No welcome indicators found, but continuing...'); - } - }); - - // Look for and click get started button with multiple fallbacks - const startButtonSelectors = [ - 'button:contains("Get Started")', - 'a:contains("Get Started")', - '[data-testid="get-started"]', - '.wu-button:contains("Start")', - 'input[value*="Get Started"]', - 'button:contains("Begin")', - 'a:contains("Continue")' - ]; - - let buttonFound = false; - startButtonSelectors.forEach(selector => { - cy.get('body').then($body => { - if (!buttonFound && $body.find(selector).length > 0) { - cy.log(`✅ Found start button with selector: ${selector}`); - cy.get(selector).first().should('be.visible').click(); - buttonFound = true; - } - }); - }); - - if (!buttonFound) { - cy.log('❌ No start button found, trying clickPrimaryBtnByTxt fallback'); - cy.clickPrimaryBtnByTxt("Get Started"); - } - - // Wait and verify navigation - cy.wait(2000); - cy.url({ timeout: 15000 }).should('contain', 'step=checks'); - cy.screenshot('welcome-step-completed'); - cy.log('✅ Welcome step completed successfully'); - }); - - it("Should complete the System Checks step", () => { - cy.log("Completing System Checks Step"); - - // Wait for checks to complete - cy.get('.wu-setup-check, .setup-check, [class*="check"]', { timeout: 10000 }) - .should('be.visible'); - - // Look for any failed checks - cy.get('body').then($body => { - const hasFailures = $body.find('.wu-check-fail, .check-fail, [class*="fail"]').length > 0; - - if (hasFailures) { - cy.log('Warning: Some system checks failed, but proceeding'); - } else { - cy.log('All system checks passed'); - } - }); - - // Proceed to next step - cy.clickPrimaryBtnByTxt("Go to the Next Step"); - - // Should move to installation step - cy.assertPageUrl({ - pathname: "/wp-admin/network/admin.php", - page: "wp-ultimo-setup", - step: "installation" - }); - }); - - it("Should complete the Installation step", () => { - cy.log("Completing Installation Step"); - - // Verify installation content - cy.get('body').should('contain.text', /install|database|table/i); - - // Click install button - cy.clickPrimaryBtnByTxt("Install"); - - // Wait for installation to complete - cy.get('.wu-progress, .progress, [class*="progress"]', { timeout: 30000 }) - .should('be.visible'); - - // Wait for installation success - cy.get('.wu-success, .success, [class*="success"]', { timeout: 30000 }) - .should('be.visible'); - - // Should move to company details step - cy.assertPageUrl({ - pathname: "/wp-admin/network/admin.php", - page: "wp-ultimo-setup", - step: "your-company" - }); - }); - - it("Should complete the Company Details step", () => { - cy.log("Completing Company Details Step"); - - // Fill company information - cy.get('body').then($body => { - // Company name - if ($body.find('#company_name, [name="company_name"], [data-testid="company-name"]').length > 0) { - cy.get('#company_name, [name="company_name"], [data-testid="company-name"]') - .clear() - .type(setupData.company.name); - } - - // Company email - if ($body.find('#company_email, [name="company_email"], [data-testid="company-email"]').length > 0) { - cy.get('#company_email, [name="company_email"], [data-testid="company-email"]') - .clear() - .type(setupData.company.email); - } - - // Company website - if ($body.find('#company_website, [name="company_website"], [data-testid="company-website"]').length > 0) { - cy.get('#company_website, [name="company_website"], [data-testid="company-website"]') - .clear() - .type(setupData.company.website); - } - - // Currency selection - if ($body.find('#currency, [name="currency"], [data-testid="currency"]').length > 0) { - cy.get('#currency, [name="currency"], [data-testid="currency"]') - .select(setupData.settings.currency); - } - }); - - // Continue to next step - cy.clickPrimaryBtnByTxt("Continue"); - - // Should move to defaults step - cy.assertPageUrl({ - pathname: "/wp-admin/network/admin.php", - page: "wp-ultimo-setup", - step: "defaults" - }); - }); - - it("Should complete the Defaults step and create sample data", () => { - cy.log("Completing Defaults Step"); - - // This step typically creates sample plans, checkout forms, etc. - cy.get('body').should('contain.text', /default|sample|plan|product/i); - - // Look for sample data creation options - cy.get('body').then($body => { - // Enable sample data creation if option exists - if ($body.find('[name="create_sample_data"], [data-testid="create-sample"], input[type="checkbox"]').length > 0) { - cy.get('[name="create_sample_data"], [data-testid="create-sample"], input[type="checkbox"]') - .check(); - } - - // Enable checkout forms creation if option exists - if ($body.find('[name="create_checkout_forms"], [data-testid="create-checkout"], input[type="checkbox"]').length > 0) { - cy.get('[name="create_checkout_forms"], [data-testid="create-checkout"], input[type="checkbox"]') - .check(); - } - - // Enable sample plans if option exists - if ($body.find('[name="create_sample_plans"], [data-testid="create-plans"], input[type="checkbox"]').length > 0) { - cy.get('[name="create_sample_plans"], [data-testid="create-plans"], input[type="checkbox"]') - .check(); - } - }); - - // Install defaults - cy.clickPrimaryBtnByTxt("Install"); - - // Wait for installation to complete - cy.get('.wu-progress, .progress, [class*="progress"]', { timeout: 30000 }) - .should('be.visible'); - - // Wait for completion - cy.get('.wu-success, .success, [class*="success"]', { timeout: 30000 }) - .should('be.visible'); - - // Should move to completion step - cy.url({ timeout: 10000 }).should('satisfy', url => - url.includes('step=done') || - url.includes('step=complete') || - url.includes('step=finish') - ); - }); - - it("Should complete the final step and redirect to dashboard", () => { - cy.log("Completing Final Step"); - - // Should show completion message - cy.get('body').should('contain.text', /complete|done|ready|congratulations|success/i); - - // Click finish button - cy.clickPrimaryBtnByTxt("Thanks!"); - - // Should redirect to main dashboard - cy.assertPageUrl({ - pathname: "/wp-admin/network/index.php" - }); - - // Verify we're on the network dashboard - cy.get('body').should('contain.text', /dashboard|network|admin/i); - }); - }); - - describe("Verify Setup Completion", () => { - it("Should have created necessary database tables", () => { - cy.log("Verifying database tables were created"); - - // Use WP-CLI to check for WP Multisite Ultimate tables - cy.wpCli("db query 'SHOW TABLES LIKE \"%wu_%\"'").then(result => { - // Should have multiple WP Multisite Ultimate tables - expect(result.stdout).to.contain('wu_'); - }); - }); - - it("Should have created sample plans", () => { - cy.log("Verifying sample plans were created"); - - // Navigate to plans page - cy.visit('/wp-admin/network/admin.php?page=wp-ultimo-products'); - - // Should show plans list - cy.get('body').should('contain.text', /plan|product/i); - - // Should have at least one plan - cy.get('.wp-list-table tbody tr, .wu-list-table tbody tr').should('have.length.at.least', 1); - }); - - it("Should have created default checkout forms", () => { - cy.log("Verifying checkout forms were created"); - - // Navigate to checkout forms page - cy.visit('/wp-admin/network/admin.php?page=wp-ultimo-checkout-forms'); - - // Should show checkout forms list - cy.get('body').should('contain.text', /checkout.*form|registration.*form/i); - - // Should have at least one checkout form - cy.get('.wp-list-table tbody tr, .wu-list-table tbody tr').should('have.length.at.least', 1); - }); - - it("Should have created necessary pages", () => { - cy.log("Verifying necessary pages were created"); - - // Check for registration page - cy.visit('/wp-admin/network/admin.php?page=wp-ultimo-sites'); - - // Navigate to main site pages - cy.visit('/wp-admin/edit.php?post_type=page'); - - // Look for checkout/registration related pages - cy.get('body').then($body => { - const hasCheckoutPages = $body.find('a:contains("Checkout"), a:contains("Registration"), a:contains("Sign Up")').length > 0; - - if (hasCheckoutPages) { - cy.log('Checkout pages found'); - } else { - cy.log('Note: Checkout pages may be created automatically on first access'); - } - }); - }); - - it("Should have configured payment gateways", () => { - cy.log("Verifying payment gateways configuration"); - - // Navigate to payment settings - cy.visit('/wp-admin/network/admin.php?page=wp-ultimo-settings&tab=payments'); - - // Should show payment gateway settings - cy.get('body').should('contain.text', /payment|gateway|stripe|paypal/i); - - // Manual gateway should be enabled by default - cy.get('body').should('contain.text', /manual.*payment|manual.*gateway/i); - }); - - it("Should allow access to main plugin features", () => { - cy.log("Verifying main plugin features are accessible"); - - // Test main menu items - const menuItems = [ - { url: '/wp-admin/network/admin.php?page=wp-ultimo-dashboard', text: 'dashboard' }, - { url: '/wp-admin/network/admin.php?page=wp-ultimo-products', text: 'product' }, - { url: '/wp-admin/network/admin.php?page=wp-ultimo-customers', text: 'customer' }, - { url: '/wp-admin/network/admin.php?page=wp-ultimo-sites', text: 'site' } - ]; - - menuItems.forEach(item => { - cy.visit(item.url); - cy.get('body').should('contain.text', new RegExp(item.text, 'i')); - }); - }); - }); - - describe("Create Test Checkout Form for E2E Tests", () => { - it("Should create a test checkout form for e2e testing", () => { - cy.log("Creating test checkout form for e2e tests"); - - // Navigate to checkout forms - cy.visit('/wp-admin/network/admin.php?page=wp-ultimo-checkout-forms'); - - // Check if a registration form already exists - cy.get('body').then($body => { - const hasRegistrationForm = $body.find('td:contains("registration"), td:contains("Registration")').length > 0; - - if (!hasRegistrationForm) { - // Create new checkout form - cy.get('a:contains("Add New"), .page-title-action').click(); - - // Fill form details - cy.get('#title, [name="name"], [data-testid="form-name"]') - .type('Registration Form'); - - cy.get('#slug, [name="slug"], [data-testid="form-slug"]') - .clear() - .type('registration'); - - // Save form - cy.get('#publish, [type="submit"], .wu-button-primary').click(); - - // Should show success message - cy.get('.notice-success, .wu-success').should('be.visible'); - - cy.log('Test checkout form created'); - } else { - cy.log('Registration form already exists'); - } - }); - }); - - it("Should verify checkout form is accessible on frontend", () => { - cy.log("Verifying checkout form frontend access"); - - // Try to access checkout form - cy.visit('/checkout/registration', { failOnStatusCode: false }); - - cy.get('body').then($body => { - // Check if checkout form loads or if we need to create pages - const hasCheckoutContent = $body.find('.wu-checkout, .checkout-form, form').length > 0; - - if (hasCheckoutContent) { - cy.log('Checkout form is accessible'); - } else { - cy.log('Checkout form may need additional setup'); - // This is normal - checkout forms may need additional configuration - } - }); - }); - }); - - describe("Setup Wizard Skip/Reset", () => { - it("Should mark setup as completed to prevent re-running", () => { - cy.log("Marking setup wizard as completed"); - - // Navigate to settings to verify setup completion - cy.visit('/wp-admin/network/admin.php?page=wp-ultimo-settings'); - - // Should not redirect to setup wizard - cy.url().should('contain', 'wp-ultimo-settings'); - cy.url().should('not.contain', 'wp-ultimo-setup'); - - cy.log('Setup wizard is marked as completed'); - }); - - it("Should provide reset option for testing (if available)", () => { - cy.log("Checking for setup reset option"); - - cy.visit('/wp-admin/network/admin.php?page=wp-ultimo-settings&tab=advanced'); - - cy.get('body').then($body => { - if ($body.find(':contains("Reset Setup"), :contains("Re-run Setup")').length > 0) { - cy.log('Setup reset option is available for future testing'); - } else { - cy.log('No setup reset option found (this is normal)'); - } - }); - }); - }); - - after(() => { - cy.log("Setup wizard completion tests finished"); - cy.log("Checkout flow tests can now be run"); - }); -}); \ No newline at end of file diff --git a/tests/e2e/cypress/integration/wizard.spec.js b/tests/e2e/cypress/integration/wizard.spec.js index 947c4458..de46be89 100644 --- a/tests/e2e/cypress/integration/wizard.spec.js +++ b/tests/e2e/cypress/integration/wizard.spec.js @@ -3,6 +3,12 @@ const page_name = "wp-ultimo-setup"; describe("Wizard", () => { before(() => { cy.loginByApi(Cypress.env("admin").username, Cypress.env("admin").password); + + // Clear setup-finished flag so the wizard is accessible + cy.wpCli( + 'eval "delete_network_option(null, WP_Ultimo::NETWORK_OPTION_SETUP_FINISHED);"' + ); + cy.visit(`/wp-admin/network/admin.php?page=${page_name}`); }); @@ -28,13 +34,17 @@ describe("Wizard", () => { /** * Steps: Installation + * Button text varies ("Install" vs "Go to the Next Step") depending on + * whether DB tables were already created by a prior spec. */ cy.assertPageUrl({ pathname: "/wp-admin/network/admin.php", page: page_name, step: "installation", }); - cy.clickPrimaryBtnByTxt("Install"); + cy.get('button[data-testid="button-primary"]') + .should("not.be.disabled") + .click(); /** * Steps: Your Company @@ -47,18 +57,32 @@ describe("Wizard", () => { cy.clickPrimaryBtnByTxt("Continue"); /** - * Steps: Defaults + * Steps: Default Content + * Creates template site, example products, checkout form, emails, login page. + * Items already created by prior specs are skipped automatically. */ cy.assertPageUrl({ pathname: "/wp-admin/network/admin.php", page: page_name, step: "defaults", }); - cy.clickPrimaryBtnByTxt("Install"); + cy.get('button[data-testid="button-primary"]') + .should("not.be.disabled") + .click(); + + /** + * Steps: Recommended Plugins + * May download plugins from wordpress.org via AJAX; allow extra time. + */ + cy.url({ timeout: 120000 }).should("include", "step=recommended-plugins"); + cy.get('button[data-testid="button-primary"]') + .should("not.be.disabled") + .click(); /** * Steps: Done */ + cy.url({ timeout: 120000 }).should("include", "step=done"); cy.clickPrimaryBtnByTxt("Thanks!"); cy.assertPageUrl({ pathname: "/wp-admin/network/index.php", diff --git a/tests/e2e/cypress/support/commands/checkout.js b/tests/e2e/cypress/support/commands/checkout.js deleted file mode 100644 index 9ed7f2e2..00000000 --- a/tests/e2e/cypress/support/commands/checkout.js +++ /dev/null @@ -1,409 +0,0 @@ -/** - * Cypress custom commands for checkout flow testing - */ - -/** - * Navigate to a specific checkout form - * @param {string} formSlug - The checkout form slug - * @param {object} options - Additional options - */ -Cypress.Commands.add("visitCheckoutForm", (formSlug = 'registration', options = {}) => { - const url = `/checkout/${formSlug}`; - cy.visit(url, options); -}); - -/** - * Select a pricing plan/product from the pricing table - * @param {number} planIndex - Index of the plan to select (0-based) - * @param {string} planSelector - Custom selector for pricing plans - */ -Cypress.Commands.add("selectPricingPlan", (planIndex = 0, planSelector = null) => { - const selector = planSelector || '[data-testid="pricing-table"], .wu-pricing-table, [id*="pricing"], [class*="plan"]'; - - cy.get(selector) - .should('be.visible') - .eq(planIndex) - .within(() => { - cy.get('button, .wu-button, [type="submit"], a[href*="checkout"]') - .first() - .click(); - }); -}); - -/** - * Fill checkout account details - * @param {object} customerData - Customer information - */ -Cypress.Commands.add("fillAccountDetails", (customerData) => { - const { - username, - email, - password, - firstName = '', - lastName = '' - } = customerData; - - // Username field - cy.get('#username, [name="username"], [data-testid="username"]') - .should('be.visible') - .clear() - .type(username); - - // Email field - cy.get('#email, [name="email"], [data-testid="email"]') - .should('be.visible') - .clear() - .type(email); - - // Password field - cy.get('#password, [name="password"], [data-testid="password"]') - .should('be.visible') - .clear() - .type(password); - - // Password confirmation (if exists) - cy.get('body').then(($body) => { - if ($body.find('#password_confirmation, [name="password_confirmation"], [data-testid="password-confirm"]').length > 0) { - cy.get('#password_confirmation, [name="password_confirmation"], [data-testid="password-confirm"]') - .clear() - .type(password); - } - }); - - // First name (if exists) - if (firstName) { - cy.get('body').then(($body) => { - if ($body.find('#first_name, [name="first_name"], [data-testid="first-name"]').length > 0) { - cy.get('#first_name, [name="first_name"], [data-testid="first-name"]') - .clear() - .type(firstName); - } - }); - } - - // Last name (if exists) - if (lastName) { - cy.get('body').then(($body) => { - if ($body.find('#last_name, [name="last_name"], [data-testid="last-name"]').length > 0) { - cy.get('#last_name, [name="last_name"], [data-testid="last-name"]') - .clear() - .type(lastName); - } - }); - } -}); - -/** - * Fill site details - * @param {object} siteData - Site information - */ -Cypress.Commands.add("fillSiteDetails", (siteData) => { - const { title, path } = siteData; - - // Site title - cy.get('#site_title, [name="site_title"], [data-testid="site-title"]') - .should('be.visible') - .clear() - .type(title); - - // Site URL/path - cy.get('#site_url, [name="site_url"], [data-testid="site-url"], [name="blogname"]') - .should('be.visible') - .clear() - .type(path); -}); - -/** - * Select a site template (if template selection is available) - * @param {number} templateIndex - Index of template to select (0-based) - */ -Cypress.Commands.add("selectSiteTemplate", (templateIndex = 0) => { - cy.get('body').then(($body) => { - const templateSelectors = [ - '[data-testid="template-selection"]', - '.wu-template-selection', - '[class*="template"]', - '.template-item' - ]; - - let templateFound = false; - - templateSelectors.forEach(selector => { - if (!templateFound && $body.find(selector).length > 0) { - cy.get(selector).eq(templateIndex).click(); - templateFound = true; - } - }); - - if (!templateFound) { - cy.log('No template selection found, skipping template selection'); - } - }); -}); - -/** - * Fill billing address information - * @param {object} billingData - Billing address data - */ -Cypress.Commands.add("fillBillingAddress", (billingData = {}) => { - const { - address = '123 Test Street', - city = 'Test City', - state = 'CA', - zipCode = '12345', - country = 'US' - } = billingData; - - cy.get('body').then(($body) => { - if ($body.find('[name*="billing"], [data-testid*="billing"]').length > 0) { - - // Address line 1 - const addressSelectors = [ - '[name="billing_address[address_line_1]"]', - '[name="billing_address_line_1"]', - '[name="billing_address"]', - '[data-testid="billing-address"]' - ]; - - addressSelectors.forEach(selector => { - cy.get('body').then(($addressBody) => { - if ($addressBody.find(selector).length > 0) { - cy.get(selector).type(address); - } - }); - }); - - // City - const citySelectors = [ - '[name="billing_address[city]"]', - '[name="billing_city"]', - '[data-testid="billing-city"]' - ]; - - citySelectors.forEach(selector => { - cy.get('body').then(($cityBody) => { - if ($cityBody.find(selector).length > 0) { - cy.get(selector).type(city); - } - }); - }); - - // State - const stateSelectors = [ - '[name="billing_address[state]"]', - '[name="billing_state"]', - '[data-testid="billing-state"]' - ]; - - stateSelectors.forEach(selector => { - cy.get('body').then(($stateBody) => { - if ($stateBody.find(selector).length > 0) { - cy.get(selector).type(state); - } - }); - }); - - // Zip Code - const zipSelectors = [ - '[name="billing_address[zip_code]"]', - '[name="billing_zip"]', - '[data-testid="billing-zip"]' - ]; - - zipSelectors.forEach(selector => { - cy.get('body').then(($zipBody) => { - if ($zipBody.find(selector).length > 0) { - cy.get(selector).type(zipCode); - } - }); - }); - } - }); -}); - -/** - * Select a payment gateway - * @param {string} gateway - Gateway type ('manual', 'stripe', 'paypal', 'free') - */ -Cypress.Commands.add("selectPaymentGateway", (gateway = 'manual') => { - const gatewaySelectors = [ - `[data-testid="gateway-${gateway}"]`, - `[value="${gateway}"]`, - `[data-gateway="${gateway}"]`, - `#gateway_${gateway}`, - `.gateway-${gateway}` - ]; - - let gatewayFound = false; - - gatewaySelectors.forEach(selector => { - cy.get('body').then(($body) => { - if (!gatewayFound && $body.find(selector).length > 0) { - cy.get(selector).click(); - gatewayFound = true; - } - }); - }); - - if (!gatewayFound) { - cy.log(`Payment gateway ${gateway} not found, proceeding anyway`); - } -}); - -/** - * Proceed to next checkout step - * @param {string} buttonText - Text to look for in the button - */ -Cypress.Commands.add("proceedToNextStep", (buttonText = '') => { - const buttonSelectors = [ - '[data-testid="continue-btn"]', - '[data-testid="next-btn"]', - '.wu-button', - 'button[type="submit"]', - 'input[type="submit"]' - ]; - - const textPatterns = buttonText ? [buttonText] : [ - 'continue', - 'next', - 'proceed', - 'complete', - 'finish', - 'create', - 'register' - ]; - - buttonSelectors.forEach(selector => { - cy.get('body').then(($body) => { - if ($body.find(selector).length > 0) { - textPatterns.forEach(pattern => { - cy.get(selector).then($buttons => { - const matchingButton = Array.from($buttons).find(btn => - btn.textContent.toLowerCase().includes(pattern.toLowerCase()) - ); - if (matchingButton && !matchingButton.disabled) { - cy.wrap(matchingButton).click(); - return false; // Break out of loops - } - }); - }); - } - }); - }); -}); - -/** - * Complete the checkout process - */ -Cypress.Commands.add("completeCheckout", () => { - const completionSelectors = [ - '[data-testid="complete-btn"]', - '[data-testid="finish-btn"]', - '.wu-button', - 'button[type="submit"]' - ]; - - const completionTexts = [ - 'complete', - 'finish', - 'create account', - 'register', - 'pay now', - 'submit' - ]; - - completionSelectors.forEach(selector => { - cy.get('body').then(($body) => { - if ($body.find(selector).length > 0) { - completionTexts.forEach(text => { - cy.get(selector).then($buttons => { - const matchingButton = Array.from($buttons).find(btn => - btn.textContent.toLowerCase().includes(text) - ); - if (matchingButton && !matchingButton.disabled) { - cy.wrap(matchingButton).click({ timeout: 10000 }); - return false; - } - }); - }); - } - }); - }); -}); - -/** - * Verify checkout completion/success - * @param {object} verificationData - Data to verify in success page - */ -Cypress.Commands.add("verifyCheckoutSuccess", (verificationData = {}) => { - const { email, siteTitle, shouldRedirect = true } = verificationData; - - // Wait for redirect if expected - if (shouldRedirect) { - cy.url({ timeout: 30000 }).should('satisfy', url => - url.includes('/confirmation') || - url.includes('/thank') || - url.includes('/success') || - url.includes('/complete') - ); - } - - // Verify success message - const successSelectors = [ - '[data-testid="success-message"]', - '.wu-success', - '.notice-success', - '[class*="success"]', - '.checkout-success' - ]; - - successSelectors.forEach(selector => { - cy.get('body').then(($body) => { - if ($body.find(selector).length > 0) { - cy.get(selector) - .should('be.visible') - .and('contain.text', /success|complete|welcome|thank|registered/i); - } - }); - }); - - // Verify email if provided - if (email) { - cy.get('[data-testid="customer-info"], .wu-customer-info, .customer-details') - .should('contain.text', email); - } - - // Verify site title if provided - if (siteTitle) { - cy.get('[data-testid="site-info"], .wu-site-info, .site-details') - .should('contain.text', siteTitle); - } -}); - -/** - * Assert current checkout step - * @param {number|string} expectedStep - Expected step number or name - */ -Cypress.Commands.add("assertCheckoutStep", (expectedStep) => { - cy.get('[data-testid="checkout-step"], .wu-step, [class*="step"], .checkout-progress') - .should('be.visible') - .and('contain.text', expectedStep); -}); - -/** - * Check if checkout form has validation errors - */ -Cypress.Commands.add("hasValidationErrors", () => { - return cy.get('body').then($body => { - const errorSelectors = [ - '[data-testid="error"]', - '.wu-error', - '.error', - '[class*="error"]', - '.form-error', - '.validation-error' - ]; - - return errorSelectors.some(selector => $body.find(selector + ':visible').length > 0); - }); -}); \ No newline at end of file diff --git a/tests/e2e/cypress/support/commands/index.js b/tests/e2e/cypress/support/commands/index.js index 83a57f8f..8cf5f550 100644 --- a/tests/e2e/cypress/support/commands/index.js +++ b/tests/e2e/cypress/support/commands/index.js @@ -1,10 +1,40 @@ import "./login"; import "./wizard"; -import "./checkout"; import "./domain-mapping"; Cypress.Commands.add("wpCli", (command, options = {}) => { - cy.exec(`npm run env run tests-cli wp ${command}`, options); + cy.exec(`npx wp-env run tests-cli wp ${command}`, { + ...options, + timeout: options.timeout || 60000, + }); +}); + +/** + * Run a PHP file inside the wp-env container via WP-CLI eval-file. + * Path is relative to the plugin root inside the container. + */ +Cypress.Commands.add("wpCliFile", (filePath, options = {}) => { + const containerPath = `/var/www/html/wp-content/plugins/ultimate-multisite/${filePath}`; + + cy.exec(`npx wp-env run tests-cli wp eval-file ${containerPath}`, { + ...options, + timeout: options.timeout || 60000, + }); +}); + +Cypress.Commands.add("loginByApi", (username, password) => { + cy.request({ + method: "POST", + url: "/wp-login.php", + form: true, + body: { + log: username, + pwd: password, + "wp-submit": "Log In", + redirect_to: "/wp-admin/", + testcookie: 1, + }, + }); }); Cypress.Commands.overwrite("type", (originalFn, subject, string, options) => From 4655401480b2bb89d6696213904a3e7ec51a6c15 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 3 Feb 2026 15:26:54 -0700 Subject: [PATCH 26/32] Fix PHPCS violations in E2E PHP fixtures Apply WordPress coding standards: escape echo output with esc_html(), use wp_json_encode() instead of json_encode(), fix spacing in function calls and arrays. Co-Authored-By: Claude Opus 4.5 --- .../cypress/fixtures/setup-checkout-form.php | 23 ++++++----- tests/e2e/cypress/fixtures/setup-product.php | 16 ++++---- .../cypress/fixtures/setup-trial-product.php | 32 ++++++++-------- .../verify-manual-checkout-results.php | 38 +++++++++++++------ .../cypress/fixtures/verify-trial-results.php | 36 ++++++++++++------ 5 files changed, 89 insertions(+), 56 deletions(-) diff --git a/tests/e2e/cypress/fixtures/setup-checkout-form.php b/tests/e2e/cypress/fixtures/setup-checkout-form.php index 10aa59e1..def428a4 100644 --- a/tests/e2e/cypress/fixtures/setup-checkout-form.php +++ b/tests/e2e/cypress/fixtures/setup-checkout-form.php @@ -3,12 +3,17 @@ * Create a checkout form and registration page for e2e testing. * Idempotent: skips creation if the form already exists. */ -$existing = WP_Ultimo\Models\Checkout_Form::query( [ 'search' => 'main-form', 'number' => 1 ] ); +$existing = WP_Ultimo\Models\Checkout_Form::query( + [ + 'search' => 'main-form', + 'number' => 1, + ] +); if ( $existing ) { $form = $existing[0]; - $page_id = wu_get_setting( 'default_registration_page', 0 ); - echo 'form:' . $form->get_id() . ',page:' . $page_id; + $page_id = wu_get_setting('default_registration_page', 0); + echo 'form:' . esc_html($form->get_id()) . ',page:' . esc_html($page_id); return; } @@ -18,14 +23,14 @@ 'settings' => [], ]; -$form = wu_create_checkout_form( $form_data ); +$form = wu_create_checkout_form($form_data); -if ( is_wp_error( $form ) ) { - echo 'error:' . $form->get_error_message(); +if ( is_wp_error($form) ) { + echo 'error:' . esc_html($form->get_error_message()); return; } -$form->use_template( 'single-step' ); +$form->use_template('single-step'); $form->save(); $page_id = wp_insert_post( @@ -39,6 +44,6 @@ ] ); -wu_save_setting( 'default_registration_page', $page_id ); +wu_save_setting('default_registration_page', $page_id); -echo 'form:' . $form->get_id() . ',page:' . $page_id; +echo 'form:' . esc_html($form->get_id()) . ',page:' . esc_html($page_id); diff --git a/tests/e2e/cypress/fixtures/setup-product.php b/tests/e2e/cypress/fixtures/setup-product.php index 8ad1d085..963dc73d 100644 --- a/tests/e2e/cypress/fixtures/setup-product.php +++ b/tests/e2e/cypress/fixtures/setup-product.php @@ -3,13 +3,13 @@ * Create a test product/plan for e2e testing. */ $product = new WP_Ultimo\Models\Product(); -$product->set_name( 'Test Plan' ); -$product->set_slug( 'test-plan' ); -$product->set_amount( 29.99 ); -$product->set_duration( 1 ); -$product->set_duration_unit( 'month' ); -$product->set_type( 'plan' ); -$product->set_active( true ); +$product->set_name('Test Plan'); +$product->set_slug('test-plan'); +$product->set_amount(29.99); +$product->set_duration(1); +$product->set_duration_unit('month'); +$product->set_type('plan'); +$product->set_active(true); $product->save(); -echo $product->get_id(); +echo esc_html($product->get_id()); diff --git a/tests/e2e/cypress/fixtures/setup-trial-product.php b/tests/e2e/cypress/fixtures/setup-trial-product.php index 9efaf2a9..508190d1 100644 --- a/tests/e2e/cypress/fixtures/setup-trial-product.php +++ b/tests/e2e/cypress/fixtures/setup-trial-product.php @@ -4,41 +4,41 @@ * Also adds it to the checkout form's pricing table. */ $product = new WP_Ultimo\Models\Product(); -$product->set_name( 'Trial Plan' ); -$product->set_slug( 'trial-plan' ); -$product->set_amount( 19.99 ); -$product->set_duration( 1 ); -$product->set_duration_unit( 'month' ); -$product->set_trial_duration( 14 ); -$product->set_trial_duration_unit( 'day' ); -$product->set_type( 'plan' ); -$product->set_active( true ); +$product->set_name('Trial Plan'); +$product->set_slug('trial-plan'); +$product->set_amount(19.99); +$product->set_duration(1); +$product->set_duration_unit('month'); +$product->set_trial_duration(14); +$product->set_trial_duration_unit('day'); +$product->set_type('plan'); +$product->set_active(true); $product->save(); $product_id = $product->get_id(); // Add the trial product to the checkout form's pricing table. -$form = WP_Ultimo\Models\Checkout_Form::query( [ 'number' => 1 ] ); +$form = WP_Ultimo\Models\Checkout_Form::query(['number' => 1]); if ( $form ) { $form = $form[0]; $settings = $form->get_settings(); foreach ( $settings as &$step ) { - if ( ! isset( $step['fields'] ) ) { + if ( ! isset($step['fields']) ) { continue; } foreach ( $step['fields'] as &$field ) { - if ( isset( $field['id'] ) && 'pricing_table' === $field['id'] ) { - $existing = $field['pricing_table_products'] ?? ''; - $field['pricing_table_products'] = $existing ? $existing . ',' . $product_id : (string) $product_id; + if ( isset($field['id']) && 'pricing_table' === $field['id'] ) { + $existing = $field['pricing_table_products'] ?? ''; + $field['pricing_table_products'] = $existing ? $existing . ',' . $product_id : (string) $product_id; } } } - $form->set_settings( $settings ); + $form->set_settings($settings); $form->save(); } -echo $product_id; +echo esc_html($product_id); diff --git a/tests/e2e/cypress/fixtures/verify-manual-checkout-results.php b/tests/e2e/cypress/fixtures/verify-manual-checkout-results.php index ae98b523..b3a22127 100644 --- a/tests/e2e/cypress/fixtures/verify-manual-checkout-results.php +++ b/tests/e2e/cypress/fixtures/verify-manual-checkout-results.php @@ -5,25 +5,39 @@ */ // UM payment -$payments = WP_Ultimo\Models\Payment::query( [ 'number' => 1, 'orderby' => 'id', 'order' => 'DESC' ] ); +$payments = WP_Ultimo\Models\Payment::query( + [ + 'number' => 1, + 'orderby' => 'id', + 'order' => 'DESC', + ] +); $um_payment_status = $payments ? $payments[0]->get_status() : 'no-payments'; $um_payment_gateway = $payments ? $payments[0]->get_gateway() : 'none'; $um_payment_total = $payments ? (float) $payments[0]->get_total() : 0; // UM membership -$memberships = WP_Ultimo\Models\Membership::query( [ 'number' => 1, 'orderby' => 'id', 'order' => 'DESC' ] ); +$memberships = WP_Ultimo\Models\Membership::query( + [ + 'number' => 1, + 'orderby' => 'id', + 'order' => 'DESC', + ] +); $um_membership_status = $memberships ? $memberships[0]->get_status() : 'no-memberships'; // UM sites -$sites = WP_Ultimo\Models\Site::query( [ 'type__in' => [ 'customer_owned' ] ] ); -$um_site_count = count( $sites ); +$sites = WP_Ultimo\Models\Site::query(['type__in' => ['customer_owned']]); +$um_site_count = count($sites); $um_site_type = $sites ? $sites[0]->get_type() : 'no-sites'; -echo json_encode( [ - 'um_payment_status' => $um_payment_status, - 'um_payment_gateway' => $um_payment_gateway, - 'um_payment_total' => $um_payment_total, - 'um_membership_status' => $um_membership_status, - 'um_site_count' => $um_site_count, - 'um_site_type' => $um_site_type, -] ); +echo wp_json_encode( + [ + 'um_payment_status' => $um_payment_status, + 'um_payment_gateway' => $um_payment_gateway, + 'um_payment_total' => $um_payment_total, + 'um_membership_status' => $um_membership_status, + 'um_site_count' => $um_site_count, + 'um_site_type' => $um_site_type, + ] +); diff --git a/tests/e2e/cypress/fixtures/verify-trial-results.php b/tests/e2e/cypress/fixtures/verify-trial-results.php index 4725e8c9..e8cc3188 100644 --- a/tests/e2e/cypress/fixtures/verify-trial-results.php +++ b/tests/e2e/cypress/fixtures/verify-trial-results.php @@ -5,21 +5,35 @@ */ // UM payment -$payments = WP_Ultimo\Models\Payment::query( [ 'number' => 1, 'orderby' => 'id', 'order' => 'DESC' ] ); +$payments = WP_Ultimo\Models\Payment::query( + [ + 'number' => 1, + 'orderby' => 'id', + 'order' => 'DESC', + ] +); $um_payment_status = $payments ? $payments[0]->get_status() : 'no-payments'; // UM membership -$memberships = WP_Ultimo\Models\Membership::query( [ 'number' => 1, 'orderby' => 'id', 'order' => 'DESC' ] ); -$um_membership_status = $memberships ? $memberships[0]->get_status() : 'no-memberships'; -$um_membership_trial_end = $memberships ? (string) $memberships[0]->get_date_trial_end() : ''; +$memberships = WP_Ultimo\Models\Membership::query( + [ + 'number' => 1, + 'orderby' => 'id', + 'order' => 'DESC', + ] +); +$um_membership_status = $memberships ? $memberships[0]->get_status() : 'no-memberships'; +$um_membership_trial_end = $memberships ? (string) $memberships[0]->get_date_trial_end() : ''; // UM sites -$sites = WP_Ultimo\Models\Site::query( [ 'type__in' => [ 'customer_owned' ] ] ); +$sites = WP_Ultimo\Models\Site::query(['type__in' => ['customer_owned']]); $um_site_type = $sites ? $sites[0]->get_type() : 'no-sites'; -echo json_encode( [ - 'um_payment_status' => $um_payment_status, - 'um_membership_status' => $um_membership_status, - 'um_membership_trial_end' => $um_membership_trial_end, - 'um_site_type' => $um_site_type, -] ); +echo wp_json_encode( + [ + 'um_payment_status' => $um_payment_status, + 'um_membership_status' => $um_membership_status, + 'um_membership_trial_end' => $um_membership_trial_end, + 'um_site_type' => $um_site_type, + ] +); From 08f14d0e6c4f8456d3358598b7a8ed6ce446e14b Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 3 Feb 2026 17:58:48 -0700 Subject: [PATCH 27/32] Fix e2e workflow: correct spec names, remove invalid CLI flags, fix error handling - Reference actual spec files (000-setup, 010-manual-checkout-flow, 020-free-trial-flow) instead of non-existent ones (checkout-registration, setup-wizard-complete, etc.) - Remove invalid Cypress CLI flags (--video, --screenshot-on-failure, --record, --reporter-options) that caused 'unknown option' errors - Remove continue-on-error from test steps so failures properly fail the job - Remove redundant 'Fail job if tests failed' step Co-Authored-By: Claude Opus 4.5 --- .github/workflows/e2e.yml | 82 +++++++-------------------------------- 1 file changed, 13 insertions(+), 69 deletions(-) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index dfefd944..0d0f0bc2 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -120,66 +120,24 @@ jobs: echo "Base URL: http://localhost:8889" cat cypress.env.json 2>/dev/null || echo "❌ cypress.env.json not found" - - name: Run Setup Wizard Test (Must Run First) + - name: Run Setup Test (Must Run First) id: setup-test - continue-on-error: true run: | - echo "=== Starting Setup Wizard Test ===" - - # Don't exit on errors - we want to capture the result + echo "=== Starting Setup Test ===" npx cypress run \ --config-file cypress.config.test.js \ - --spec "tests/e2e/cypress/integration/setup-wizard-complete.spec.js" \ - --browser ${{ matrix.browser }} \ - --reporter spec \ - --reporter-options "verbose=true" \ - --env "CYPRESS_CRASH_REPORTS=0" \ - --record false \ - --video true \ - --screenshot-on-failure true - - CYPRESS_EXIT_CODE=$? - - if [ $CYPRESS_EXIT_CODE -eq 0 ]; then - echo "✅ Setup wizard test passed successfully" - echo "setup_success=true" >> $GITHUB_OUTPUT - else - echo "❌ Setup wizard test failed with exit code $CYPRESS_EXIT_CODE" - echo "Capturing detailed failure information..." - echo "=== WordPress State ===" - curl -I http://localhost:8889 || echo "WordPress not responding" - echo "=== Test Files ===" - ls -la tests/e2e/cypress/screenshots/ 2>/dev/null || echo "No screenshots directory" - ls -la tests/e2e/cypress/videos/ 2>/dev/null || echo "No videos directory" - echo "=== Docker Logs ===" - docker logs $(docker ps -q --filter "name=tests-wordpress") 2>&1 | tail -10 || echo "No Docker logs" - echo "=== File System ===" - ls -la tests/e2e/cypress/integration/setup-wizard-complete.spec.js || echo "Setup test file missing" - echo "setup_success=false" >> $GITHUB_OUTPUT - fi - - - name: Verify Setup Completed Successfully - if: success() - run: | - echo "=== Verifying Setup Wizard Completion ===" - # Test for setup completion indicators - curl -s "http://localhost:8889/wp-admin/network/" | grep -i "ultimo\|dashboard" && echo "✅ Setup appears successful" || echo "⚠️ Setup verification unclear" + --spec "tests/e2e/cypress/integration/000-setup.spec.js" \ + --browser ${{ matrix.browser }} - name: Run Checkout Tests (After Setup) - if: always() # Run checkout tests regardless of setup wizard result for debugging - continue-on-error: true + id: checkout-tests run: | + set +e echo "=== Starting Checkout Tests ===" - echo "Setup wizard result: ${GITHUB_OUTPUT}" - echo "Setup success flag: ${{ steps.setup-test.outputs.setup_success }}" - echo "Running checkout test suite (setup wizard success not required for debugging)..." - # Run all checkout tests in sequence CHECKOUT_TESTS=( - "tests/e2e/cypress/integration/checkout-registration.spec.js" - "tests/e2e/cypress/integration/checkout-validation.spec.js" - "tests/e2e/cypress/integration/checkout-scenarios.spec.js" - "tests/e2e/cypress/integration/checkout-confirmation.spec.js" + "tests/e2e/cypress/integration/010-manual-checkout-flow.spec.js" + "tests/e2e/cypress/integration/020-free-trial-flow.spec.js" ) TOTAL_FAILURES=0 @@ -190,13 +148,7 @@ jobs: npx cypress run \ --config-file cypress.config.test.js \ --spec "$TEST_SPEC" \ - --browser ${{ matrix.browser }} \ - --reporter spec \ - --reporter-options "verbose=true" \ - --env "CYPRESS_CRASH_REPORTS=0" \ - --record false \ - --video true \ - --screenshot-on-failure true + --browser ${{ matrix.browser }} CYPRESS_EXIT_CODE=$? @@ -209,14 +161,12 @@ jobs: done if [ $TOTAL_FAILURES -gt 0 ]; then - echo "❌ $TOTAL_FAILURES checkout tests failed" - echo "Capturing failure information..." - ls -la tests/e2e/cypress/screenshots/ 2>/dev/null || echo "No screenshots" - ls -la tests/e2e/cypress/videos/ 2>/dev/null || echo "No videos" - else - echo "✅ All checkout tests passed successfully!" + echo "❌ $TOTAL_FAILURES checkout test(s) failed" + exit 1 fi + echo "✅ All checkout tests passed!" + - name: Fix permissions for Cypress output if: always() run: sudo chown -R $USER:$USER tests/e2e/cypress @@ -237,12 +187,6 @@ jobs: name: cypress-videos-${{ matrix.php }}-${{ matrix.browser }} path: tests/e2e/cypress/videos - - name: Fail job if tests failed - if: failure() - run: | - echo "❌ One or more e2e tests failed." - exit 1 - - name: Stop WordPress Environment if: always() run: npm run env:stop From 0ba2b715cf91585aa8d7a1d565859c689705c82e Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 3 Feb 2026 21:24:16 -0700 Subject: [PATCH 28/32] fix searches --- inc/class-ajax.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/inc/class-ajax.php b/inc/class-ajax.php index 71cd4a04..47cf0e86 100644 --- a/inc/class-ajax.php +++ b/inc/class-ajax.php @@ -104,15 +104,21 @@ public function search_models(): void { [ 'model' => 'membership', 'query' => [], + 'number' => 100, 'exclude' => [], // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude ] ); + // Number can be in the query array or it's own. Code uses both. + if (! empty($args['query']['number'])) { + $args['query']['number'] = $args['number']; + } + $query = array_merge( - $args['query'], [ - 'number' => -1, - ] + 'number' => $args['number'], + ], + $args['query'] ); if ($args['exclude']) { From 01cbcb6767f5adeb21cf5a799e3426e2f3323e49 Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 3 Feb 2026 21:24:32 -0700 Subject: [PATCH 29/32] Add the doc --- inc/documentation/berlindb-dynamic-hooks.php | 268 +++++++++---------- 1 file changed, 134 insertions(+), 134 deletions(-) diff --git a/inc/documentation/berlindb-dynamic-hooks.php b/inc/documentation/berlindb-dynamic-hooks.php index 94fca5da..024ecb83 100644 --- a/inc/documentation/berlindb-dynamic-hooks.php +++ b/inc/documentation/berlindb-dynamic-hooks.php @@ -37,7 +37,7 @@ * Fires before blogs are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:883 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -47,7 +47,7 @@ * Fires after blogs query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1159 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -57,7 +57,7 @@ * Filters the SQL clauses for a blogs query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:994 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $clauses { * Associative array of SQL clause strings. @@ -78,7 +78,7 @@ * Filters the columns to search when performing a blogs search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1306 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -91,7 +91,7 @@ * Filters the found blogs after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1595 + * @see vendor/berlindb/core/src/Database/Query.php * * @param object[] $items The array of found blog objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -103,7 +103,7 @@ * Filters a single blog item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2068 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -115,7 +115,7 @@ * Filters the FOUND_ROWS() query for blogs. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:640 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -132,7 +132,7 @@ * column for a customer row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous email verification value. * @param string $new_value The new email verification value. @@ -147,7 +147,7 @@ * column for a customer row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous has trialed value. * @param int $new_value The new has trialed value. @@ -162,7 +162,7 @@ * column for a customer row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous vip value. * @param int $new_value The new vip value. @@ -174,7 +174,7 @@ * Fires before customers are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:883 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -184,7 +184,7 @@ * Fires after customers query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1159 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -194,7 +194,7 @@ * Filters the SQL clauses for a customers query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:994 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $clauses { * Associative array of SQL clause strings. @@ -215,7 +215,7 @@ * Filters the columns to search when performing a customers search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1306 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -228,7 +228,7 @@ * Filters the found customers after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1595 + * @see vendor/berlindb/core/src/Database/Query.php * * @param object[] $items The array of found customer objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -240,7 +240,7 @@ * Filters a single customer item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2068 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -252,7 +252,7 @@ * Filters the FOUND_ROWS() query for customers. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:640 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -269,7 +269,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous code value. * @param string $new_value The new code value. @@ -284,7 +284,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous uses value. * @param int $new_value The new uses value. @@ -299,7 +299,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous max uses value. * @param int $new_value The new max uses value. @@ -314,7 +314,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous apply to renewals value. * @param int $new_value The new apply to renewals value. @@ -329,7 +329,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous type value. * @param string $new_value The new type value. @@ -344,7 +344,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous value value. * @param string $new_value The new value value. @@ -359,7 +359,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous setup fee type value. * @param string $new_value The new setup fee type value. @@ -374,7 +374,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous setup fee value value. * @param string $new_value The new setup fee value value. @@ -389,7 +389,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous active value. * @param int $new_value The new active value. @@ -404,7 +404,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous date start value. * @param string $new_value The new date start value. @@ -419,7 +419,7 @@ * column for a discount code row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous date expiration value. * @param string $new_value The new date expiration value. @@ -431,7 +431,7 @@ * Fires before discount codes are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:883 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -441,7 +441,7 @@ * Fires after discount codes query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1159 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -451,7 +451,7 @@ * Filters the SQL clauses for a discount codes query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:994 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $clauses { * Associative array of SQL clause strings. @@ -472,7 +472,7 @@ * Filters the columns to search when performing a discount codes search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1306 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -485,7 +485,7 @@ * Filters the found discount codes after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1595 + * @see vendor/berlindb/core/src/Database/Query.php * * @param object[] $items The array of found discount code objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -497,7 +497,7 @@ * Filters a single discount code item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2068 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -509,7 +509,7 @@ * Filters the FOUND_ROWS() query for discount codes. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:640 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -526,7 +526,7 @@ * column for a domain row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous domain value. * @param string $new_value The new domain value. @@ -541,7 +541,7 @@ * column for a domain row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous active value. * @param int $new_value The new active value. @@ -556,7 +556,7 @@ * column for a domain row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous primary domain value. * @param int $new_value The new primary domain value. @@ -571,7 +571,7 @@ * column for a domain row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous secure value. * @param int $new_value The new secure value. @@ -586,7 +586,7 @@ * column for a domain row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous stage value. * @param string $new_value The new stage value. @@ -598,7 +598,7 @@ * Fires before domains are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:883 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -608,7 +608,7 @@ * Fires after domains query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1159 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -618,7 +618,7 @@ * Filters the SQL clauses for a domains query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:994 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $clauses { * Associative array of SQL clause strings. @@ -639,7 +639,7 @@ * Filters the columns to search when performing a domains search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1306 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -652,7 +652,7 @@ * Filters the found domains after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1595 + * @see vendor/berlindb/core/src/Database/Query.php * * @param object[] $items The array of found domain objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -664,7 +664,7 @@ * Filters a single domain item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2068 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -676,7 +676,7 @@ * Filters the FOUND_ROWS() query for domains. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:640 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -693,7 +693,7 @@ * column for a event row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous author id value. * @param int $new_value The new author id value. @@ -708,7 +708,7 @@ * column for a event row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous object id value. * @param int $new_value The new object id value. @@ -720,7 +720,7 @@ * Fires before events are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:883 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -730,7 +730,7 @@ * Fires after events query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1159 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -740,7 +740,7 @@ * Filters the SQL clauses for a events query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:994 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $clauses { * Associative array of SQL clause strings. @@ -761,7 +761,7 @@ * Filters the columns to search when performing a events search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1306 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -774,7 +774,7 @@ * Filters the found events after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1595 + * @see vendor/berlindb/core/src/Database/Query.php * * @param object[] $items The array of found event objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -786,7 +786,7 @@ * Filters a single event item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2068 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -798,7 +798,7 @@ * Filters the FOUND_ROWS() query for events. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:640 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -815,7 +815,7 @@ * column for a form row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous name value. * @param string $new_value The new name value. @@ -830,7 +830,7 @@ * column for a form row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous slug value. * @param string $new_value The new slug value. @@ -845,7 +845,7 @@ * column for a form row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous active value. * @param int $new_value The new active value. @@ -860,7 +860,7 @@ * column for a form row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous settings value. * @param string $new_value The new settings value. @@ -872,7 +872,7 @@ * Fires before forms are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:883 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -882,7 +882,7 @@ * Fires after forms query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1159 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -892,7 +892,7 @@ * Filters the SQL clauses for a forms query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:994 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $clauses { * Associative array of SQL clause strings. @@ -913,7 +913,7 @@ * Filters the columns to search when performing a forms search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1306 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -926,7 +926,7 @@ * Filters the found forms after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1595 + * @see vendor/berlindb/core/src/Database/Query.php * * @param object[] $items The array of found form objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -938,7 +938,7 @@ * Filters a single form item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2068 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -950,7 +950,7 @@ * Filters the FOUND_ROWS() query for forms. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:640 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -967,7 +967,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous plan id value. * @param int $new_value The new plan id value. @@ -982,7 +982,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous initial amount value. * @param string $new_value The new initial amount value. @@ -997,7 +997,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous recurring value. * @param int $new_value The new recurring value. @@ -1012,7 +1012,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous auto renew value. * @param int $new_value The new auto renew value. @@ -1027,7 +1027,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous duration value. * @param int $new_value The new duration value. @@ -1042,7 +1042,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous amount value. * @param string $new_value The new amount value. @@ -1057,7 +1057,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous date expiration value. * @param string $new_value The new date expiration value. @@ -1072,7 +1072,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous date payment plan completed value. * @param string $new_value The new date payment plan completed value. @@ -1087,7 +1087,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous times billed value. * @param int $new_value The new times billed value. @@ -1102,7 +1102,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous status value. * @param string $new_value The new status value. @@ -1117,7 +1117,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous gateway customer id value. * @param string $new_value The new gateway customer id value. @@ -1132,7 +1132,7 @@ * column for a membership row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous gateway subscription id value. * @param string $new_value The new gateway subscription id value. @@ -1144,7 +1144,7 @@ * Fires before memberships are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:883 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1154,7 +1154,7 @@ * Fires after memberships query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1159 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1164,7 +1164,7 @@ * Filters the SQL clauses for a memberships query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:994 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $clauses { * Associative array of SQL clause strings. @@ -1185,7 +1185,7 @@ * Filters the columns to search when performing a memberships search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1306 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -1198,7 +1198,7 @@ * Filters the found memberships after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1595 + * @see vendor/berlindb/core/src/Database/Query.php * * @param object[] $items The array of found membership objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1210,7 +1210,7 @@ * Filters a single membership item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2068 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1222,7 +1222,7 @@ * Filters the FOUND_ROWS() query for memberships. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:640 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1239,7 +1239,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous status value. * @param string $new_value The new status value. @@ -1254,7 +1254,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous customer id value. * @param int $new_value The new customer id value. @@ -1269,7 +1269,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous membership id value. * @param int $new_value The new membership id value. @@ -1284,7 +1284,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous parent id value. * @param int $new_value The new parent id value. @@ -1299,7 +1299,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous product id value. * @param int $new_value The new product id value. @@ -1314,7 +1314,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous subtotal value. * @param string $new_value The new subtotal value. @@ -1329,7 +1329,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous refund total value. * @param string $new_value The new refund total value. @@ -1344,7 +1344,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous tax total value. * @param string $new_value The new tax total value. @@ -1359,7 +1359,7 @@ * column for a payment row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous total value. * @param string $new_value The new total value. @@ -1371,7 +1371,7 @@ * Fires before payments are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:883 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1381,7 +1381,7 @@ * Fires after payments query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1159 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1391,7 +1391,7 @@ * Filters the SQL clauses for a payments query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:994 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $clauses { * Associative array of SQL clause strings. @@ -1412,7 +1412,7 @@ * Filters the columns to search when performing a payments search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1306 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -1425,7 +1425,7 @@ * Filters the found payments after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1595 + * @see vendor/berlindb/core/src/Database/Query.php * * @param object[] $items The array of found payment objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1437,7 +1437,7 @@ * Filters a single payment item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2068 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1449,7 +1449,7 @@ * Filters the FOUND_ROWS() query for payments. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:640 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1463,7 +1463,7 @@ * Fires before posts are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:883 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1473,7 +1473,7 @@ * Fires after posts query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1159 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1483,7 +1483,7 @@ * Filters the SQL clauses for a posts query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:994 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $clauses { * Associative array of SQL clause strings. @@ -1504,7 +1504,7 @@ * Filters the columns to search when performing a posts search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1306 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -1517,7 +1517,7 @@ * Filters the found posts after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1595 + * @see vendor/berlindb/core/src/Database/Query.php * * @param object[] $items The array of found post objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1529,7 +1529,7 @@ * Filters a single post item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2068 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1541,7 +1541,7 @@ * Filters the FOUND_ROWS() query for posts. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:640 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1558,7 +1558,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous parent id value. * @param int $new_value The new parent id value. @@ -1573,7 +1573,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous amount value. * @param string $new_value The new amount value. @@ -1588,7 +1588,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $old_value The previous setup fee value. * @param string $new_value The new setup fee value. @@ -1603,7 +1603,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous recurring value. * @param int $new_value The new recurring value. @@ -1618,7 +1618,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous trial duration value. * @param int $new_value The new trial duration value. @@ -1633,7 +1633,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous duration value. * @param int $new_value The new duration value. @@ -1648,7 +1648,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous billing cycles value. * @param int $new_value The new billing cycles value. @@ -1663,7 +1663,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous list order value. * @param int $new_value The new list order value. @@ -1678,7 +1678,7 @@ * column for a product row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous active value. * @param int $new_value The new active value. @@ -1690,7 +1690,7 @@ * Fires before products are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:883 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1700,7 +1700,7 @@ * Fires after products query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1159 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1710,7 +1710,7 @@ * Filters the SQL clauses for a products query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:994 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $clauses { * Associative array of SQL clause strings. @@ -1731,7 +1731,7 @@ * Filters the columns to search when performing a products search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1306 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -1744,7 +1744,7 @@ * Filters the found products after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1595 + * @see vendor/berlindb/core/src/Database/Query.php * * @param object[] $items The array of found product objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1756,7 +1756,7 @@ * Filters a single product item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2068 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1768,7 +1768,7 @@ * Filters the FOUND_ROWS() query for products. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:640 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1785,7 +1785,7 @@ * column for a webhook row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous active value. * @param int $new_value The new active value. @@ -1800,7 +1800,7 @@ * column for a webhook row. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2303 + * @see vendor/berlindb/core/src/Database/Query.php * * @param int $old_value The previous hidden value. * @param int $new_value The new hidden value. @@ -1812,7 +1812,7 @@ * Fires before webhooks are fetched from the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:883 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1822,7 +1822,7 @@ * Fires after webhooks query vars have been parsed. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1159 + * @see vendor/berlindb/core/src/Database/Query.php * * @param \BerlinDB\Database\Query $query The query instance (passed by reference). */ @@ -1832,7 +1832,7 @@ * Filters the SQL clauses for a webhooks query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:994 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $clauses { * Associative array of SQL clause strings. @@ -1853,7 +1853,7 @@ * Filters the columns to search when performing a webhooks search. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1306 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string[] $search_columns Array of column names to search. * @param string $search The search term. @@ -1866,7 +1866,7 @@ * Filters the found webhooks after a query. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:1595 + * @see vendor/berlindb/core/src/Database/Query.php * * @param object[] $items The array of found webhook objects. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1878,7 +1878,7 @@ * Filters a single webhook item before it is inserted or updated in the database. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:2068 + * @see vendor/berlindb/core/src/Database/Query.php * * @param array $item The item data as an associative array. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). @@ -1890,7 +1890,7 @@ * Filters the FOUND_ROWS() query for webhooks. * * @since 2.0.0 - * @see vendor/berlindb/core/src/Database/Query.php:640 + * @see vendor/berlindb/core/src/Database/Query.php * * @param string $sql The SQL query to count found rows. * @param \BerlinDB\Database\Query $query The query instance (passed by reference). From 3a09c64931ae5b275cf3989f7473cc62307cafce Mon Sep 17 00:00:00 2001 From: David Stone Date: Tue, 3 Feb 2026 21:25:32 -0700 Subject: [PATCH 30/32] fix legacy --- tests/e2e/cypress/fixtures/setup-tables.php | 2 +- views/legacy/signup/signup-nav-links.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/e2e/cypress/fixtures/setup-tables.php b/tests/e2e/cypress/fixtures/setup-tables.php index 857b7784..d7d7f707 100644 --- a/tests/e2e/cypress/fixtures/setup-tables.php +++ b/tests/e2e/cypress/fixtures/setup-tables.php @@ -10,6 +10,6 @@ $installer->_install_database_tables(); } -update_network_option( null, WP_Ultimo::NETWORK_OPTION_SETUP_FINISHED, time() ); +update_network_option(null, WP_Ultimo::NETWORK_OPTION_SETUP_FINISHED, time()); echo $loader->is_installed() ? 'installed' : 'failed'; diff --git a/views/legacy/signup/signup-nav-links.php b/views/legacy/signup/signup-nav-links.php index 54d8625c..7d285150 100644 --- a/views/legacy/signup/signup-nav-links.php +++ b/views/legacy/signup/signup-nav-links.php @@ -50,7 +50,7 @@ - + From 7dda86b14fe2dc95cada3ef6cb4d85545853c0b8 Mon Sep 17 00:00:00 2001 From: David Stone Date: Wed, 4 Feb 2026 14:46:14 -0700 Subject: [PATCH 31/32] Use correct logic --- inc/class-ajax.php | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/inc/class-ajax.php b/inc/class-ajax.php index 47cf0e86..3ee1779f 100644 --- a/inc/class-ajax.php +++ b/inc/class-ajax.php @@ -110,17 +110,10 @@ public function search_models(): void { ); // Number can be in the query array or it's own. Code uses both. - if (! empty($args['query']['number'])) { + if (empty($args['query']['number'])) { $args['query']['number'] = $args['number']; } - $query = array_merge( - [ - 'number' => $args['number'], - ], - $args['query'] - ); - if ($args['exclude']) { if (is_string($args['exclude'])) { $args['exclude'] = explode(',', $args['exclude']); // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude From 98ef9749b842c2d199e0ca84f51d4dc7eb0a266b Mon Sep 17 00:00:00 2001 From: David Stone Date: Wed, 4 Feb 2026 14:58:09 -0700 Subject: [PATCH 32/32] Update test matrix to require PHP 8.2 minimum Remove PHP 7.4, 8.0, and 8.1 from the CI test matrix. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index dde1fe35..ebb550f5 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-version: ["7.4", "8.0", "8.1", "8.2", "8.3", "8.4", "8.5"] + php-version: ["8.2", "8.3", "8.4", "8.5"] services: mysql: image: mariadb:11.4