diff --git a/assets/js/paypal-setup-wizard.js b/assets/js/paypal-setup-wizard.js new file mode 100644 index 00000000..a5d1b6f7 --- /dev/null +++ b/assets/js/paypal-setup-wizard.js @@ -0,0 +1,70 @@ +// PayPal setup wizard — test connection step. +// +// Pings the wu_paypal_setup_wizard_test AJAX endpoint and renders the +// status with Vue. Triggered from views/wizards/paypal-setup/test.php. +// +// @since 2.6.0 + +(function($) { + $( document ).ready( function () { + if ( typeof wu_paypal_setup_wizard === 'undefined' ) { + return; + } + + var data = wu_paypal_setup_wizard; + + new Vue( { + el: '#wu-paypal-setup-wizard-test', + data: { + loading: true, + success: false, + webhookStatus: 'not_attempted', + errorMessage: data.error_message, + rawResponse: data.waiting_message, + }, + mounted: function () { + this.runTest(); + }, + methods: { + runTest: function () { + var that = this; + this.loading = true; + this.success = false; + this.rawResponse = data.waiting_message; + + $.ajax( { + url: data.ajax_url, + method: 'POST', + data: { + action: 'wu_paypal_setup_wizard_test', + nonce: data.nonce, + sandbox_mode: data.sandbox_mode, + }, + success: function ( response ) { + that.loading = false; + that.rawResponse = JSON.stringify( response, null, 2 ); + + if ( response.success ) { + that.success = true; + that.webhookStatus = response.data.webhook_status || 'not_attempted'; + } else { + that.success = false; + that.errorMessage = + ( response.data && response.data.message ) || + data.error_message; + } + }, + error: function ( xhr ) { + that.loading = false; + that.success = false; + that.errorMessage = data.error_message; + that.rawResponse = + ( xhr.responseText && xhr.responseText.slice( 0, 1000 ) ) || + data.error_message; + }, + } ); + }, + }, + } ); + } ); +} )( jQuery ); diff --git a/assets/js/paypal-setup-wizard.min.js b/assets/js/paypal-setup-wizard.min.js new file mode 100644 index 00000000..f6ade592 --- /dev/null +++ b/assets/js/paypal-setup-wizard.min.js @@ -0,0 +1 @@ +(function(t){t(document).ready(function(){if(typeof wu_paypal_setup_wizard!="undefined"){var s=wu_paypal_setup_wizard;new Vue({el:"#wu-paypal-setup-wizard-test",data:{loading:!0,success:!1,webhookStatus:"not_attempted",errorMessage:s.error_message,rawResponse:s.waiting_message},mounted:function(){this.runTest()},methods:{runTest:function(){var e=this;this.loading=!0,this.success=!1,this.rawResponse=s.waiting_message,t.ajax({url:s.ajax_url,method:"POST",data:{action:"wu_paypal_setup_wizard_test",nonce:s.nonce,sandbox_mode:s.sandbox_mode},success:function(a){e.loading=!1,e.rawResponse=JSON.stringify(a,null,2),a.success?(e.success=!0,e.webhookStatus=a.data.webhook_status||"not_attempted"):(e.success=!1,e.errorMessage=a.data&&a.data.message||s.error_message)},error:function(a){e.loading=!1,e.success=!1,e.errorMessage=s.error_message,e.rawResponse=a.responseText&&a.responseText.slice(0,1e3)||s.error_message}})}}})}})})(jQuery); diff --git a/constants.php b/constants.php index 2b9ada98..cc6ad46d 100644 --- a/constants.php +++ b/constants.php @@ -51,3 +51,23 @@ if ( ! defined('WU_EXTERNAL_CRON_ENABLED')) { define('WU_EXTERNAL_CRON_ENABLED', false); } + +/** + * Feature flag: Enable PayPal OAuth ("Connect with PayPal") onboarding. + * + * When set to true, enables the PayPal Partner Referrals one-click onboarding + * flow. This requires an active PayPal Commerce Platform partner agreement — + * `/v2/customer/partner-referrals`, partner-token minting via the proxy, and + * `PayPal-Auth-Assertion` impersonation only work for approved partners. + * + * Defaults to false. Without partner status, merchants set up PayPal via the + * guided manual-credentials wizard (developer.paypal.com REST app). When + * Ultimate Multisite is approved as a PayPal partner again, define + * WU_PAYPAL_OAUTH_ENABLED as true in wp-config.php to re-enable the + * one-click flow. + * + * @since 2.6.0 + */ +if ( ! defined('WU_PAYPAL_OAUTH_ENABLED')) { + define('WU_PAYPAL_OAUTH_ENABLED', false); +} diff --git a/inc/admin-pages/class-paypal-setup-wizard-admin-page.php b/inc/admin-pages/class-paypal-setup-wizard-admin-page.php new file mode 100644 index 00000000..c71e7a74 --- /dev/null +++ b/inc/admin-pages/class-paypal-setup-wizard-admin-page.php @@ -0,0 +1,501 @@ + 'manage_network', + ]; + + /** + * Sandbox vs live mode for the wizard session. + * + * Resolved in page_loaded() from the persisted `paypal_rest_sandbox_mode` + * setting. Sandbox is the safer default for a fresh setup. + * + * @since 2.6.0 + * @var bool + */ + protected $sandbox_mode = true; + + /** + * Register early-load hooks. + * + * @since 2.6.0 + * @return void + */ + public function page_loaded(): void { + + $this->sandbox_mode = (bool) (int) wu_get_setting('paypal_rest_sandbox_mode', 1); + + parent::page_loaded(); + } + + /** + * Wizard title. + * + * @since 2.6.0 + * @return string + */ + public function get_title(): string { + + return __('PayPal Setup', 'ultimate-multisite'); + } + + /** + * Menu label (only shown on the network sidebar when discoverable). + * + * @since 2.6.0 + * @return string + */ + public function get_menu_title() { + + return __('PayPal Setup', 'ultimate-multisite'); + } + + /** + * Wizard sections. + * + * @since 2.6.0 + * @return array + */ + public function get_sections() { + + return [ + 'welcome' => [ + 'title' => __('Welcome', 'ultimate-multisite'), + 'view' => [$this, 'section_welcome'], + 'handler' => [$this, 'handle_welcome'], + ], + 'instructions' => [ + 'title' => __('Get Credentials', 'ultimate-multisite'), + 'view' => [$this, 'section_instructions'], + ], + 'configure' => [ + 'title' => __('Configure', 'ultimate-multisite'), + 'view' => [$this, 'section_configure'], + 'handler' => [$this, 'handle_configure'], + ], + 'test' => [ + 'title' => __('Test Connection', 'ultimate-multisite'), + 'view' => [$this, 'section_test'], + ], + 'done' => [ + 'title' => __('Done', 'ultimate-multisite'), + 'view' => [$this, 'section_done'], + ], + ]; + } + + /** + * Welcome section. + * + * @since 2.6.0 + * @return void + */ + public function section_welcome(): void { + + wu_get_template( + 'wizards/paypal-setup/welcome', + [ + 'page' => $this, + 'sandbox_mode' => $this->sandbox_mode, + ] + ); + } + + /** + * Welcome handler — persists the sandbox/live toggle and advances. + * + * @since 2.6.0 + * @return void + */ + public function handle_welcome(): void { + + check_admin_referer('saving_welcome', '_wpultimo_nonce'); + + $sandbox = isset($_POST['paypal_sandbox_mode']) ? 1 : 0; + + wu_save_setting('paypal_rest_sandbox_mode', $sandbox); + + $this->sandbox_mode = (bool) $sandbox; + + wp_safe_redirect($this->get_next_section_link()); + + exit; + } + + /** + * Instructions section — guides the user through creating a REST app. + * + * @since 2.6.0 + * @return void + */ + public function section_instructions(): void { + + wu_get_template( + 'wizards/paypal-setup/instructions', + [ + 'page' => $this, + 'sandbox_mode' => $this->sandbox_mode, + 'developer_url' => $this->sandbox_mode + ? 'https://developer.paypal.com/dashboard/applications/sandbox' + : 'https://developer.paypal.com/dashboard/applications/live', + ] + ); + + $this->render_submit_box(); + } + + /** + * Configure section — paste credentials into the form. + * + * @since 2.6.0 + * @return void + */ + public function section_configure(): void { + + $mode_prefix = $this->sandbox_mode ? 'sandbox' : 'live'; + + $client_id = (string) wu_get_setting("paypal_rest_{$mode_prefix}_client_id", ''); + $client_secret = (string) wu_get_setting("paypal_rest_{$mode_prefix}_client_secret", ''); + + wu_get_template( + 'wizards/paypal-setup/configure', + [ + 'page' => $this, + 'sandbox_mode' => $this->sandbox_mode, + 'mode_label' => $this->sandbox_mode + ? __('Sandbox', 'ultimate-multisite') + : __('Live', 'ultimate-multisite'), + 'client_id' => $client_id, + 'client_secret' => $client_secret, + ] + ); + } + + /** + * Configure handler — sanitises and saves the pasted credentials. + * + * @since 2.6.0 + * @return void + */ + public function handle_configure(): void { + + check_admin_referer('saving_configure', '_wpultimo_nonce'); + + if ( ! current_user_can('manage_network_options')) { + wp_die(esc_html__('You do not have permission to do this.', 'ultimate-multisite')); + } + + $mode_prefix = $this->sandbox_mode ? 'sandbox' : 'live'; + $client_id = isset($_POST['paypal_client_id']) ? sanitize_text_field(wp_unslash($_POST['paypal_client_id'])) : ''; + $client_secret = isset($_POST['paypal_client_secret']) ? sanitize_text_field(wp_unslash($_POST['paypal_client_secret'])) : ''; + + if (empty($client_id) || empty($client_secret)) { + set_site_transient( + 'wu_paypal_wizard_notice', + [ + 'type' => 'error', + 'message' => __('Please paste both the Client ID and the Client Secret before continuing.', 'ultimate-multisite'), + ], + 60 + ); + + wp_safe_redirect(remove_query_arg(['_wpultimo_nonce', 'saving_configure'])); + + exit; + } + + wu_save_setting("paypal_rest_{$mode_prefix}_client_id", $client_id); + wu_save_setting("paypal_rest_{$mode_prefix}_client_secret", $client_secret); + + // Make sure paypal-rest is in the active gateways list. + $active_gateways = (array) wu_get_setting('active_gateways', []); + + if ( ! in_array('paypal-rest', $active_gateways, true)) { + $active_gateways[] = 'paypal-rest'; + wu_save_setting('active_gateways', $active_gateways); + } + + wp_safe_redirect($this->get_next_section_link()); + + exit; + } + + /** + * Test section — Vue widget that pings the test_credentials AJAX endpoint. + * + * @since 2.6.0 + * @return void + */ + public function section_test(): void { + + wp_enqueue_script('wu-vue'); + + wp_enqueue_script( + 'wu-paypal-setup-wizard-test', + wu_get_asset('paypal-setup-wizard.js', 'js'), + [ + 'jquery', + 'wu-vue', + ], + wu_get_version(), + true + ); + + wp_localize_script( + 'wu-paypal-setup-wizard-test', + 'wu_paypal_setup_wizard', + [ + 'ajax_url' => admin_url('admin-ajax.php'), + 'nonce' => wp_create_nonce('wu_paypal_setup_wizard'), + 'sandbox_mode' => $this->sandbox_mode ? 1 : 0, + 'waiting_message' => __('Verifying your credentials with PayPal…', 'ultimate-multisite'), + 'success_message' => __('Your credentials were accepted by PayPal.', 'ultimate-multisite'), + 'webhook_message' => __('We installed the webhook automatically.', 'ultimate-multisite'), + 'error_message' => __('PayPal rejected the credentials. Double-check the Client ID and Secret on the previous step.', 'ultimate-multisite'), + ] + ); + + wu_get_template( + 'wizards/paypal-setup/test', + [ + 'page' => $this, + 'sandbox_mode' => $this->sandbox_mode, + ] + ); + } + + /** + * Done section — confirmation and link back to settings. + * + * @since 2.6.0 + * @return void + */ + public function section_done(): void { + + wu_get_template( + 'wizards/paypal-setup/done', + [ + 'page' => $this, + 'sandbox_mode' => $this->sandbox_mode, + ] + ); + } + + /** + * Register the AJAX handlers used by the test step. + * + * Called from class-wp-ultimo.php during the admin-pages registration + * pass so that the AJAX endpoints are registered even when the wizard + * page itself isn't currently rendering. + * + * @since 2.6.0 + * @return void + */ + public function register_ajax_handlers(): void { + + add_action('wp_ajax_wu_paypal_setup_wizard_test', [$this, 'ajax_test_credentials']); + } + + /** + * AJAX handler that validates the saved credentials with PayPal. + * + * Calls the PayPal REST `/v1/oauth2/token` endpoint with client_credentials + * grant type to verify the saved Client ID and Secret. On success, installs + * the webhook so the merchant doesn't have to do it manually. + * + * @since 2.6.0 + * @return void + */ + public function ajax_test_credentials(): void { + + check_ajax_referer('wu_paypal_setup_wizard', 'nonce'); + + if ( ! current_user_can('manage_network_options')) { + wp_send_json_error( + [ + 'message' => __('You do not have permission to do this.', 'ultimate-multisite'), + ] + ); + } + + $sandbox = isset($_POST['sandbox_mode']) ? (bool) (int) $_POST['sandbox_mode'] : true; + $mode_prefix = $sandbox ? 'sandbox' : 'live'; + + $client_id = (string) wu_get_setting("paypal_rest_{$mode_prefix}_client_id", ''); + $client_secret = (string) wu_get_setting("paypal_rest_{$mode_prefix}_client_secret", ''); + + if (empty($client_id) || empty($client_secret)) { + wp_send_json_error( + [ + 'message' => __('Credentials are missing. Please return to the previous step.', 'ultimate-multisite'), + ] + ); + } + + $api_base = $sandbox ? 'https://api-m.sandbox.paypal.com' : 'https://api-m.paypal.com'; + + // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- Required for PayPal Basic auth. + $auth_header = 'Basic ' . base64_encode($client_id . ':' . $client_secret); + + $response = wp_remote_post( + $api_base . '/v1/oauth2/token', + [ + 'headers' => [ + 'Authorization' => $auth_header, + 'Content-Type' => 'application/x-www-form-urlencoded', + ], + 'body' => 'grant_type=client_credentials', + 'timeout' => 30, + ] + ); + + if (is_wp_error($response)) { + wu_log_add('paypal', 'Wizard credentials test failed (transport): ' . $response->get_error_message(), LogLevel::ERROR); + + wp_send_json_error( + [ + 'message' => sprintf( + /* translators: %s: transport-level error message */ + __('Could not reach PayPal: %s', 'ultimate-multisite'), + $response->get_error_message() + ), + ] + ); + } + + $code = wp_remote_retrieve_response_code($response); + $body = json_decode(wp_remote_retrieve_body($response), true); + + if (200 !== $code || empty($body['access_token'])) { + $paypal_message = ''; + + if (is_array($body)) { + $paypal_message = (string) ( + $body['error_description'] + ?? $body['error'] + ?? '' + ); + } + + wu_log_add( + 'paypal', + sprintf('Wizard credentials test rejected (HTTP %d): %s', $code, $paypal_message), + LogLevel::ERROR + ); + + wp_send_json_error( + [ + 'message' => $paypal_message + ? sprintf( + /* translators: %s: PayPal error message */ + __('PayPal rejected the credentials: %s', 'ultimate-multisite'), + $paypal_message + ) + : __('PayPal rejected the credentials. Please double-check the Client ID and Secret.', 'ultimate-multisite'), + ] + ); + } + + // Credentials valid — try to install the webhook automatically. + $webhook_status = 'not_attempted'; + $webhook_message = ''; + $gateway = wu_get_gateway('paypal-rest'); + + if ($gateway instanceof PayPal_REST_Gateway) { + $gateway->set_test_mode($sandbox); + + $webhook_result = $gateway->install_webhook(); + + if (true === $webhook_result) { + $webhook_status = 'installed'; + $webhook_message = __('Webhook installed.', 'ultimate-multisite'); + } elseif (is_wp_error($webhook_result)) { + $webhook_status = 'failed'; + $webhook_message = $webhook_result->get_error_message(); + + wu_log_add( + 'paypal', + 'Wizard webhook auto-install failed: ' . $webhook_message, + LogLevel::WARNING + ); + } + } + + wp_send_json_success( + [ + 'message' => __('Credentials accepted by PayPal.', 'ultimate-multisite'), + 'webhook_status' => $webhook_status, + 'webhook_message' => $webhook_message, + 'next_url' => $this->get_next_section_link(), + ] + ); + } +} diff --git a/inc/class-wp-ultimo.php b/inc/class-wp-ultimo.php index 1e52b429..88d69c88 100644 --- a/inc/class-wp-ultimo.php +++ b/inc/class-wp-ultimo.php @@ -852,6 +852,9 @@ protected function load_admin_only_pages(): void { new WP_Ultimo\Admin_Pages\Hosting_Integration_Wizard_Admin_Page(); + $paypal_setup_wizard = new WP_Ultimo\Admin_Pages\PayPal_Setup_Wizard_Admin_Page(); + $paypal_setup_wizard->register_ajax_handlers(); + new WP_Ultimo\Admin_Pages\Event_List_Admin_Page(); new WP_Ultimo\Admin_Pages\Event_View_Admin_Page(); diff --git a/inc/gateways/class-paypal-oauth-handler.php b/inc/gateways/class-paypal-oauth-handler.php index 82c83941..ca3ed195 100644 --- a/inc/gateways/class-paypal-oauth-handler.php +++ b/inc/gateways/class-paypal-oauth-handler.php @@ -38,6 +38,12 @@ class PayPal_OAuth_Handler { /** * Initialize the OAuth handler. * + * Short-circuits when the WU_PAYPAL_OAUTH_ENABLED feature flag is off, so + * AJAX endpoints, the OAuth return callback, and the admin-notice surface + * are not registered for non-partner installs. Settings persistence and + * read helpers (is_merchant_connected, get_merchant_details) remain + * available so existing connections degrade gracefully. + * * @since 2.0.0 * @return void */ @@ -45,6 +51,10 @@ public function init(): void { $this->test_mode = (bool) (int) wu_get_setting('paypal_rest_sandbox_mode', true); + if ( ! $this->is_oauth_feature_enabled()) { + return; + } + // Register AJAX handlers add_action('wp_ajax_wu_paypal_connect', [$this, 'ajax_initiate_oauth']); add_action('wp_ajax_wu_paypal_disconnect', [$this, 'ajax_disconnect']); @@ -496,32 +506,44 @@ public function is_configured(): bool { /** * Check if the PayPal OAuth Connect feature is enabled. * - * The feature flag is controlled by the PayPal proxy plugin on - * ultimatemultisite.com. OAuth Connect is only available when the - * proxy has partner credentials configured (i.e. the PayPal - * partnership is active). The result is cached for 12 hours. + * Resolution order (highest priority first): * - * Local override: define WU_PAYPAL_OAUTH_ENABLED as true in - * wp-config.php to force-enable without the proxy check. + * 1. The `wu_paypal_oauth_enabled` filter, if it returns a non-null + * bool. This is the canonical override hook for tests and addons. + * 2. The `WU_PAYPAL_OAUTH_ENABLED` constant, if defined. This is the + * framework-wide default (set in constants.php to `false` since + * v2.6.0). When the constant is defined the proxy probe is skipped + * entirely so non-partner installs make no outbound HTTP requests + * during settings rendering. + * 3. The `wu_paypal_oauth_enabled` site transient, populated by a + * previous proxy probe. + * 4. A live `/status` probe to the partner proxy, cached for 12 hours + * on success and 1 hour on failure. + * + * Step (4) is dead code on shipped installs because the constant short- + * circuits in step (2). It remains in the codebase so the proxy probe + * can be re-enabled (by leaving the constant undefined in wp-config.php) + * once Ultimate Multisite is approved as a PayPal partner again. + * + * To re-enable one-click onboarding after partner approval, define + * `WU_PAYPAL_OAUTH_ENABLED` as `true` in wp-config.php. * * @since 2.0.0 * @return bool */ public function is_oauth_feature_enabled(): bool { - // Local constant override (useful for dev/testing) - if (defined('WU_PAYPAL_OAUTH_ENABLED')) { - return (bool) WU_PAYPAL_OAUTH_ENABLED; - } - /** * Filters whether the PayPal OAuth Connect feature is enabled. * - * Return a non-null value to override the remote check. + * Return a non-null value to override the constant, transient, and + * proxy probe. Used by tests and by addons that need to force-toggle + * the flow without mutating wp-config.php. * * @since 2.0.0 * - * @param bool|null $enabled Null to use remote check, bool to override. + * @param bool|null $enabled Null to defer to lower-priority sources, + * bool to override. */ $override = apply_filters('wu_paypal_oauth_enabled', null); @@ -529,6 +551,12 @@ public function is_oauth_feature_enabled(): bool { return (bool) $override; } + // Constant default — primary on/off switch. Defined in constants.php + // as false so non-partner installs never reach the proxy probe. + if (defined('WU_PAYPAL_OAUTH_ENABLED')) { + return (bool) WU_PAYPAL_OAUTH_ENABLED; + } + // Check cached flag from proxy $cached = get_site_transient('wu_paypal_oauth_enabled'); diff --git a/inc/gateways/class-paypal-rest-gateway.php b/inc/gateways/class-paypal-rest-gateway.php index eb421316..036a7bec 100644 --- a/inc/gateways/class-paypal-rest-gateway.php +++ b/inc/gateways/class-paypal-rest-gateway.php @@ -1734,6 +1734,22 @@ public function settings(): void { ], ] ); + } else { + // Guided wizard CTA — surfaces the manual-credentials walkthrough + // when the partner OAuth flow is unavailable. + wu_register_settings_field( + 'payment-gateways', + 'paypal_rest_setup_wizard_cta', + [ + 'title' => __('Guided setup', 'ultimate-multisite'), + 'desc' => __('Walk through PayPal credential creation step by step, with verification and automatic webhook installation.', 'ultimate-multisite'), + 'type' => 'html', + 'content' => [$this, 'render_setup_wizard_cta'], + 'require' => [ + 'active_gateways' => 'paypal-rest', + ], + ] + ); } // Build the require array for manual key fields. @@ -1815,6 +1831,13 @@ public function settings(): void { __('Webhooks are automatically configured when you connect your PayPal account or save settings with valid API credentials.', 'ultimate-multisite') ); + // Webhook URL display — visible whenever the manual credential fields + // are visible, which mirrors the $live_key_require gating above. + $webhook_require = $live_key_require; + // $live_key_require pins sandbox to 0 — webhook URL is the same in + // both modes, so drop that pin to keep it visible in sandbox too. + unset($webhook_require['paypal_rest_sandbox_mode']); + wu_register_settings_field( 'payment-gateways', 'paypal_rest_webhook_url', @@ -1826,10 +1849,7 @@ public function settings(): void { 'copy' => true, 'display_value' => $this->get_webhook_listener_url(), 'wrapper_classes' => '', - 'require' => [ - 'active_gateways' => 'paypal-rest', - 'paypal_rest_show_manual_keys' => 1, - ], + 'require' => $webhook_require, ] ); } @@ -1947,6 +1967,32 @@ public function render_oauth_connection(): void { // } } + /** + * Renders the "Open Setup Wizard" CTA button. + * + * Surfaced when the partner OAuth path is disabled, so merchants get a + * one-click route into the guided manual-credentials walkthrough. + * + * @since 2.6.0 + * @return void + */ + public function render_setup_wizard_cta(): void { + + $wizard_url = wu_network_admin_url('wp-ultimo-paypal-setup-wizard'); + + printf( + '
', + esc_html__('First time setting up PayPal?', 'ultimate-multisite'), + esc_html__('The guided wizard walks you through creating PayPal API credentials, verifies them, and installs the webhook automatically — no copy-and-paste from the PayPal docs required.', 'ultimate-multisite'), + esc_url($wizard_url), + esc_html__('Open setup wizard', 'ultimate-multisite') + ); + } + /** * Renders an admin notice when the store currency is not supported by PayPal. * diff --git a/tests/WP_Ultimo/Admin_Pages/PayPal_Setup_Wizard_Admin_Page_Test.php b/tests/WP_Ultimo/Admin_Pages/PayPal_Setup_Wizard_Admin_Page_Test.php new file mode 100644 index 00000000..48f59b5b --- /dev/null +++ b/tests/WP_Ultimo/Admin_Pages/PayPal_Setup_Wizard_Admin_Page_Test.php @@ -0,0 +1,742 @@ +page = new PayPal_Setup_Wizard_Admin_Page(); + + // Ensure persisted settings start clean. + wu_save_setting('paypal_rest_sandbox_mode', 1); + wu_save_setting('paypal_rest_sandbox_client_id', ''); + wu_save_setting('paypal_rest_sandbox_client_secret', ''); + wu_save_setting('paypal_rest_live_client_id', ''); + wu_save_setting('paypal_rest_live_client_secret', ''); + wu_save_setting('paypal_rest_sandbox_webhook_id', ''); + wu_save_setting('paypal_rest_live_webhook_id', ''); + + // Clear any cached tokens or partner data from previous tests. + delete_site_transient('wu_paypal_rest_access_token_sandbox'); + delete_site_transient('wu_paypal_rest_access_token_live'); + delete_site_transient('wu_paypal_rest_partner_data_sandbox'); + delete_site_transient('wu_paypal_rest_partner_data_live'); + } + + /** + * Tear down: clean up superglobals and persisted settings. + */ + protected function tearDown(): void { + + unset( + $_GET['integration'], + $_GET['step'], + $_REQUEST['step'], + $_POST['paypal_sandbox_mode'], + $_POST['paypal_client_id'], + $_POST['paypal_client_secret'], + $_POST['_wpultimo_nonce'], + $_REQUEST['_wpultimo_nonce'], + $_POST['nonce'], + $_POST['sandbox_mode'] + ); + + remove_all_filters('pre_http_request'); + remove_all_filters('wp_redirect'); + + parent::tearDown(); + } + + // ------------------------------------------------------------------------- + // Page properties + // ------------------------------------------------------------------------- + + /** + * Page ID is the expected slug. + */ + public function test_page_id(): void { + + $ref = new \ReflectionClass($this->page); + $prop = $ref->getProperty('id'); + $prop->setAccessible(true); + + $this->assertEquals('wp-ultimo-paypal-setup-wizard', $prop->getValue($this->page)); + } + + /** + * Page type is submenu. + */ + public function test_page_type(): void { + + $ref = new \ReflectionClass($this->page); + $prop = $ref->getProperty('type'); + $prop->setAccessible(true); + + $this->assertEquals('submenu', $prop->getValue($this->page)); + } + + /** + * Parent is 'none' so the wizard is a hidden network admin page. + */ + public function test_page_parent(): void { + + $ref = new \ReflectionClass($this->page); + $prop = $ref->getProperty('parent'); + $prop->setAccessible(true); + + $this->assertEquals('none', $prop->getValue($this->page)); + } + + /** + * highlight_menu_slug is 'wp-ultimo-settings'. + */ + public function test_highlight_menu_slug(): void { + + $ref = new \ReflectionClass($this->page); + $prop = $ref->getProperty('highlight_menu_slug'); + $prop->setAccessible(true); + + $this->assertEquals('wp-ultimo-settings', $prop->getValue($this->page)); + } + + /** + * supported_panels requires manage_network on the network admin menu. + */ + public function test_supported_panels(): void { + + $ref = new \ReflectionClass($this->page); + $prop = $ref->getProperty('supported_panels'); + $prop->setAccessible(true); + $panels = $prop->getValue($this->page); + + $this->assertArrayHasKey('network_admin_menu', $panels); + $this->assertEquals('manage_network', $panels['network_admin_menu']); + } + + // ------------------------------------------------------------------------- + // Titles + // ------------------------------------------------------------------------- + + public function test_get_title_returns_string(): void { + + $title = $this->page->get_title(); + + $this->assertIsString($title); + $this->assertNotEmpty($title); + } + + public function test_get_menu_title_returns_string(): void { + + $title = $this->page->get_menu_title(); + + $this->assertIsString($title); + $this->assertNotEmpty($title); + } + + // ------------------------------------------------------------------------- + // get_sections() + // ------------------------------------------------------------------------- + + public function test_get_sections_returns_array(): void { + + $sections = $this->page->get_sections(); + + $this->assertIsArray($sections); + } + + /** + * @dataProvider expected_sections_provider + */ + public function test_get_sections_includes_expected_section(string $key): void { + + $sections = $this->page->get_sections(); + + $this->assertArrayHasKey($key, $sections, "Wizard sections must include '{$key}'"); + } + + /** + * @return array+ ' . esc_html($mode_label) . '' + ); + ?> +
+ + ++ +
++ +
++ +
+ ++ +
++ ' . esc_html($mode_label) . '' + ); + ?> +
+ ++ + +
++ +
+ + + + +
+ Create App, give it a name like Ultimate Multisite, and select Merchant as the app type. PayPal will generate a Client ID and Secret immediately.', 'ultimate-multisite'),
+ [
+ 'strong' => [],
+ 'code' => [],
+ ]
+ );
+ ?>
+
+ Client ID at the top. Click Show next to Secret to reveal it. Copy both values — you will paste them into Ultimate Multisite on the next step.', 'ultimate-multisite'), + [ + 'strong' => [], + ] + ); + ?> +
++ Features on the same page. Make sure Accept payments is checked. Subscriptions should also be checked if you plan to sell recurring memberships. Save the change if you toggled anything.', 'ultimate-multisite'), + [ + 'strong' => [], + ] + ); + ?> +
++ + Sandbox → Accounts on the same dashboard.', 'ultimate-multisite'), + [ + 'strong' => [], + ] + ); + ?> +
++ +
+ +{{ rawResponse }}
+ + +
+ +
+ + +
+ +