diff --git a/.psalm/stubs.php b/.psalm/stubs.php index 56ba72451..27484ec28 100644 --- a/.psalm/stubs.php +++ b/.psalm/stubs.php @@ -94,6 +94,45 @@ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group = return 0; } +/** + * Retrieves the number of times a filter has been applied during the current request. + * + * @since 6.1.0 + * + * @global int[] $wp_filters Stores the number of times each filter was triggered. + * + * @param string $hook_name The name of the filter hook. + * @return int The number of times the filter hook has been applied. + */ +function did_filter( $hook_name ) { + return 0; +} + +/** + * Returns whether or not a filter hook is currently being processed. + * + * The function current_filter() only returns the most recent filter being executed. + * did_filter() returns the number of times a filter has been applied during + * the current request. + * + * This function allows detection for any filter currently being executed + * (regardless of whether it's the most recent filter to fire, in the case of + * hooks called from hook callbacks) to be verified. + * + * @since 3.9.0 + * + * @see current_filter() + * @see did_filter() + * @global string[] $wp_current_filter Current filter. + * + * @param string|null $hook_name Optional. Filter hook to check. Defaults to null, + * which checks if any filter is currently being run. + * @return bool Whether the filter is currently in the stack. + */ +function doing_filter( $hook_name = null ) { + return false; +} + /** * HTML API: WP_HTML_Tag_Processor class */ diff --git a/modules.php b/modules.php index 21a4e645e..0d65644f2 100644 --- a/modules.php +++ b/modules.php @@ -91,9 +91,12 @@ return function ( string $root_dir ): iterable { $modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )(); } + $show_new_ux = '1' === get_option( 'woocommerce-ppcp-is-new-merchant' ); + $preview_new_ux = '1' === getenv( 'PCP_SETTINGS_ENABLED' ); + if ( apply_filters( 'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled', - getenv( 'PCP_SETTINGS_ENABLED' ) === '1' + $show_new_ux || $preview_new_ux ) ) { $modules[] = ( require "$modules_dir/ppcp-settings/module.php" )(); } diff --git a/modules/ppcp-compat/services.php b/modules/ppcp-compat/services.php index e8ffe6adb..e523f10ff 100644 --- a/modules/ppcp-compat/services.php +++ b/modules/ppcp-compat/services.php @@ -172,6 +172,16 @@ return array( $container->get( 'settings.data.settings' ), $subscription_map_helper->map() ), + /** + * We need to pass the PaymentSettings model instance to use it in some helpers. + * Once the new settings module is permanently enabled, + * this model can be passed as a dependency to the appropriate helper classes. + * For now, we must pass it this way to avoid errors when the new settings module is disabled. + */ + new SettingsMap( + $container->get( 'settings.data.payment' ), + array() + ), ); }, 'compat.settings.settings_map_helper' => static function( ContainerInterface $container ) : SettingsMapHelper { @@ -180,7 +190,8 @@ return array( $container->get( 'compat.settings.styling_map_helper' ), $container->get( 'compat.settings.settings_tab_map_helper' ), $container->get( 'compat.settings.subscription_map_helper' ), - $container->get( 'compat.settings.general_map_helper' ) + $container->get( 'compat.settings.general_map_helper' ), + $container->get( 'wcgateway.settings.admin-settings-enabled' ) ); }, 'compat.settings.styling_map_helper' => static function() : StylingSettingsMapHelper { diff --git a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php index 843b275c0..6d3f5d5b4 100644 --- a/modules/ppcp-compat/src/Settings/SettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/SettingsMapHelper.php @@ -10,7 +10,9 @@ declare( strict_types = 1 ); namespace WooCommerce\PayPalCommerce\Compat\Settings; use RuntimeException; +use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; +use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel; use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings; @@ -72,6 +74,13 @@ class SettingsMapHelper { */ protected GeneralSettingsMapHelper $general_settings_map_helper; + /** + * Whether the new settings module is enabled. + * + * @var bool + */ + protected bool $new_settings_module_enabled; + /** * Constructor. * @@ -80,6 +89,7 @@ class SettingsMapHelper { * @param SettingsTabMapHelper $settings_tab_map_helper A helper for mapping the old/new settings tab settings. * @param SubscriptionSettingsMapHelper $subscription_map_helper A helper for mapping old and new subscription settings. * @param GeneralSettingsMapHelper $general_settings_map_helper A helper for mapping old and new general settings. + * @param bool $new_settings_module_enabled Whether the new settings module is enabled. * @throws RuntimeException When an old key has multiple mappings. */ public function __construct( @@ -87,7 +97,8 @@ class SettingsMapHelper { StylingSettingsMapHelper $styling_settings_map_helper, SettingsTabMapHelper $settings_tab_map_helper, SubscriptionSettingsMapHelper $subscription_map_helper, - GeneralSettingsMapHelper $general_settings_map_helper + GeneralSettingsMapHelper $general_settings_map_helper, + bool $new_settings_module_enabled ) { $this->validate_settings_map( $settings_map ); $this->settings_map = $settings_map; @@ -95,6 +106,7 @@ class SettingsMapHelper { $this->settings_tab_map_helper = $settings_tab_map_helper; $this->subscription_map_helper = $subscription_map_helper; $this->general_settings_map_helper = $general_settings_map_helper; + $this->new_settings_module_enabled = $new_settings_module_enabled; } /** @@ -124,6 +136,10 @@ class SettingsMapHelper { * @return mixed|null The value of the mapped setting, or null if not found. */ public function mapped_value( string $old_key ) { + if ( ! $this->new_settings_module_enabled ) { + return null; + } + $this->ensure_map_initialized(); if ( ! isset( $this->key_to_model[ $old_key ] ) ) { return null; @@ -147,6 +163,10 @@ class SettingsMapHelper { * @return bool True if the key exists in the new settings, false otherwise. */ public function has_mapped_key( string $old_key ) : bool { + if ( ! $this->new_settings_module_enabled ) { + return false; + } + $this->ensure_map_initialized(); return isset( $this->key_to_model[ $old_key ] ); @@ -169,7 +189,11 @@ class SettingsMapHelper { switch ( true ) { case $model instanceof StylingSettings: - return $this->styling_settings_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] ); + return $this->styling_settings_map_helper->mapped_value( + $old_key, + $this->model_cache[ $model_id ], + $this->get_payment_settings_model() + ); case $model instanceof GeneralSettings: return $this->general_settings_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] ); @@ -217,4 +241,23 @@ class SettingsMapHelper { } } } + + /** + * Retrieves the PaymentSettings model instance. + * + * Once the new settings module is permanently enabled, + * this model can be passed as a dependency to the appropriate helper classes. + * For now, we must pass it this way to avoid errors when the new settings module is disabled. + * + * @return AbstractDataModel|null + */ + protected function get_payment_settings_model() : ?AbstractDataModel { + foreach ( $this->settings_map as $settings_map_instance ) { + if ( $settings_map_instance->get_model() instanceof PaymentSettings ) { + return $settings_map_instance->get_model(); + } + } + + return null; + } } diff --git a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php index bc0f1b8eb..42a810670 100644 --- a/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php +++ b/modules/ppcp-compat/src/Settings/StylingSettingsMapHelper.php @@ -10,7 +10,11 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Compat\Settings; use RuntimeException; +use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway; use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; +use WooCommerce\PayPalCommerce\Googlepay\GooglePayGateway; +use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel; +use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings; use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO; /** @@ -23,7 +27,7 @@ class StylingSettingsMapHelper { use ContextTrait; - protected const BUTTON_NAMES = array( 'googlepay', 'applepay', 'pay-later' ); + protected const BUTTON_NAMES = array( GooglePayGateway::ID, ApplePayGateway::ID, 'pay-later' ); /** * Maps old setting keys to new setting style names. @@ -64,12 +68,13 @@ class StylingSettingsMapHelper { /** * Retrieves the value of a mapped key from the new settings. * - * @param string $old_key The key from the legacy settings. - * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param string $old_key The key from the legacy settings. + * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param AbstractDataModel|null $payment_settings The payment settings model. * * @return mixed The value of the mapped setting, (null if not found). */ - public function mapped_value( string $old_key, array $styling_models ) { + public function mapped_value( string $old_key, array $styling_models, ?AbstractDataModel $payment_settings ) { switch ( $old_key ) { case 'smart_button_locations': return $this->mapped_smart_button_locations_value( $styling_models ); @@ -81,16 +86,16 @@ class StylingSettingsMapHelper { return $this->mapped_pay_later_button_locations_value( $styling_models ); case 'disable_funding': - return $this->mapped_disabled_funding_value( $styling_models ); + return $this->mapped_disabled_funding_value( $styling_models, $payment_settings ); case 'googlepay_button_enabled': - return $this->mapped_button_enabled_value( $styling_models, 'googlepay' ); + return $this->mapped_button_enabled_value( $styling_models, GooglePayGateway::ID, $payment_settings ); case 'applepay_button_enabled': - return $this->mapped_button_enabled_value( $styling_models, 'applepay' ); + return $this->mapped_button_enabled_value( $styling_models, ApplePayGateway::ID, $payment_settings ); case 'pay_later_button_enabled': - return $this->mapped_button_enabled_value( $styling_models, 'pay-later' ); + return $this->mapped_button_enabled_value( $styling_models, 'pay-later', $payment_settings ); default: foreach ( $this->locations_map() as $old_location_name => $new_location_name ) { @@ -224,52 +229,80 @@ class StylingSettingsMapHelper { /** * Retrieves the mapped disabled funding value from the new settings. * - * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param AbstractDataModel|null $payment_settings The payment settings model. * @return array|null The list of disabled funding, or null if none are disabled. */ - protected function mapped_disabled_funding_value( array $styling_models ): ?array { + protected function mapped_disabled_funding_value( array $styling_models, ?AbstractDataModel $payment_settings ): ?array { + if ( is_null( $payment_settings ) ) { + return null; + } + $disabled_funding = array(); $locations_to_context_map = $this->current_context_to_new_button_location_map(); $current_context = $locations_to_context_map[ $this->context() ] ?? ''; + assert( $payment_settings instanceof PaymentSettings ); foreach ( $styling_models as $model ) { - if ( $model->location !== $current_context || in_array( 'venmo', $model->methods, true ) ) { - continue; + if ( $model->location === $current_context ) { + if ( ! in_array( 'venmo', $model->methods, true ) || ! $payment_settings->get_venmo_enabled() ) { + $disabled_funding[] = 'venmo'; + } } - - $disabled_funding[] = 'venmo'; - - return $disabled_funding; } - return null; + return $disabled_funding; } /** * Retrieves the mapped enabled or disabled button value from the new settings. * - * @param LocationStylingDTO[] $styling_models The list of location styling models. - * @param string $button_name The button name (see {@link self::BUTTON_NAMES}). + * @param LocationStylingDTO[] $styling_models The list of location styling models. + * @param string $button_name The button name (see {@link self::BUTTON_NAMES}). + * @param AbstractDataModel|null $payment_settings The payment settings model. * @return int The enabled (1) or disabled (0) state. * @throws RuntimeException If an invalid button name is provided. */ - protected function mapped_button_enabled_value( array $styling_models, string $button_name ): ?int { + protected function mapped_button_enabled_value( array $styling_models, string $button_name, ?AbstractDataModel $payment_settings ): ?int { + if ( is_null( $payment_settings ) ) { + return null; + } + if ( ! in_array( $button_name, self::BUTTON_NAMES, true ) ) { throw new RuntimeException( 'Wrong button name is provided.' ); } $locations_to_context_map = $this->current_context_to_new_button_location_map(); $current_context = $locations_to_context_map[ $this->context() ] ?? ''; + assert( $payment_settings instanceof PaymentSettings ); foreach ( $styling_models as $model ) { - if ( ! $model->enabled - || $model->location !== $current_context - || ! in_array( $button_name, $model->methods, true ) - ) { - continue; + if ( $model->enabled && $model->location === $current_context ) { + if ( in_array( $button_name, $model->methods, true ) && $payment_settings->is_method_enabled( $button_name ) ) { + return 1; + } } + } - return 1; + if ( $current_context === 'classic_checkout' ) { + /** + * Outputs an inline CSS style that hides the Google Pay gateway (on Classic Checkout) + * In case if the button is disabled from the styling settings but the gateway itself is enabled. + * + * @return void + */ + add_action( + 'woocommerce_paypal_payments_checkout_button_render', + static function (): void { + ?> + + { }; const features = [ 'products' ]; - -registerExpressPaymentMethod( { - name: buttonData.id, - title: `PayPal - ${ buttonData.title }`, - description: __( - 'Eligible users will see the PayPal button.', - 'woocommerce-paypal-payments' - ), - gatewayId: 'ppcp-gateway', - label:
, - content: , - edit: , - ariaLabel: buttonData.title, - canMakePayment: () => buttonData.enabled, - supports: { - features, - style: [ 'height', 'borderRadius' ], - }, -} ); +if ( buttonConfig?.is_enabled ) { + registerExpressPaymentMethod( { + name: buttonData.id, + title: `PayPal - ${ buttonData.title }`, + description: __( + 'Eligible users will see the PayPal button.', + 'woocommerce-paypal-payments' + ), + gatewayId: 'ppcp-gateway', + label:
, + content: , + edit: , + ariaLabel: buttonData.title, + canMakePayment: () => buttonData.enabled, + supports: { + features, + style: [ 'height', 'borderRadius' ], + }, + } ); +} diff --git a/modules/ppcp-settings/docs/authentication-flows.md b/modules/ppcp-settings/docs/authentication-flows.md new file mode 100644 index 000000000..bf666726f --- /dev/null +++ b/modules/ppcp-settings/docs/authentication-flows.md @@ -0,0 +1,120 @@ +# Authentication Flows + +The settings UI offers two distinct authentication methods: + +- OAuth +- Direct API + +## OAuth + +This is the usual authentication UI for most users. It opens a "PayPal popup" with a login mask. +The authentication flow consists of **three steps**: + +- Generate a referral URL with a special token +- Translate a one-time OAuth secret into permanent API credentials +- Complete authentication by confirming the token from step 1 + +**Usage:** + +1. Available on the first onboarding page (for sandbox login), or on the last page of the onboarding wizard. +2. Authentication is initiated by clicking a "Connect" button which opens a popup with a PayPal login mask + - Sometimes the login opens in a new tab, mainly on Mac when using the browser in full-screen mode +3. After completing the login, the final page shows a "Return to your store" button; clicking that button closes the popup/tab and completes the authentication process + +**More details on what happens:** + +```mermaid +sequenceDiagram + autonumber + participant R as React API + participant S as PHP Server + + R->>S: Request partner referral URL + Note over S: Generate and store a one-time token + create participant W as WooCommerce API + S->>W: Request referral URL + destroy W + W->>S: Generate the full partner referral URL + S->>R: Return referral URL + create participant P as PayPal Popup + R->>P: Open PayPal popup, which was generated by WooCommerce APi + Note over P: Complete login inside Popup + P->>R: Call JS function with OAuth ID and shared secret + R->>S: Send OAuth data to REST endpoint + create participant PP as PayPal API + S->>PP: Request permanent credentials + PP->>S: Translate one-time secret to permanent credentials + destroy P + P->>R: Redirect browser tab with unique token + Note over R: App unmounts during redirect + + Note over S: During page load + Note over S: Verify token and finalize authentication + S->>PP: Request merchant details + destroy PP + PP->>S: Return merchant details (e.g. country) + Note over S: Render the settings page with React app + S->>R: Boot react app in "settings mode" +``` + +1. Authentication starts _before_ the "Connect" button is rendered, as we generate a one-time partner referral URL + - See `ConnectionUrlGenerator::generate()` + - This referral URL configures PayPal: Which items render inside the Popup? What is the "return + URL" for the final step? Is it a sandbox or live login? +2. _...The merchant completes the login or account creation flow inside the popup..._ +3. During page-load of the final confirmation page inside the popup: PayPal directly calls a JS function on the WooCommerce settings page, i.e. the popup communicates with the open WooCommerce tab. This JS function sends an oauth ID and shared secret (OTP) to a REST endpoint + - See `AuthenticatoinRestEndpoint::connect_oauth()` + - See `AuthenticationManager::authenticate_via_oauth()` → translates the one-time shared secret + into a permanent client secret + - At this stage, the authentication is _incomplete_, as some details are only provided by the + final step +4. When clicking the "Return to store" button, the popup closes and the WooCommerce settings page "reloads"; it's actually a _redirect_ which is initiated by PayPal and receives a unique token (which was generated by the `ConnectionUrlGenerator`) that is required to complete authentication. + - See `ConnectionListener::process()` + - See `AuthenticationManager::finish_oauth_authentication()` + - This listener runs on every wp-admin page load and bails if the required token is not present +5. After the final page reload, the React app directly enters "Settings mode" + +## Direct API + +This method is only available for business accounts, as it requires the merchant to create a PayPal REST app that's linked to their account. + +
+Setup the PayPal REST app + +1. Visit https://developer.paypal.com/ +2. In section "Apps & Credentials" click "Create App" +3. After the app is ready, it displays the `Client ID` and `Secret Key` values + +
+ +**Usage:** + +1. Available on the first onboarding screen, via the "See advanced options" form at the bottom of the page +2. Activate the "Manual Connection" toggle; then enter the `Client ID` and `Secret Key` and hit Enter + +**What happens:** + +```mermaid +sequenceDiagram + participant R as React + participant S as Server + participant P as PayPal API + + R->>S: Send credentials to REST endpoint + S->>P: Authenticate via Direct API + P->>S: Return authentication result + S->>P: Request merchant details + P->>S: Return merchant details (e.g. country) + + Note over S: Process authentication result + S->>R: Return authentication status + Note over R: Update UI to authenticated state
(no page reload) + +``` + +1. Client ID and Secret are sent to a REST endpoint of the plugin. The authentication happens on server-side. + - See `AuthenticatoinRestEndpoint::connect_direct()` + - See `AuthenticationManager::authenticate_via_direct_api()` +2. After authentication is completed, the merchant account is prepared on server side and a confirmation is returned to the React app. + - See `AuthenticationManager::update_connection_details()` → condition `is_merchant_connected()` +3. The React app directly switches to the "Settings mode" without a page reload. diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js index 29434f344..55932143c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Components/ConnectionButton.js @@ -32,7 +32,6 @@ const ButtonOrPlaceholder = ( { if ( href ) { buttonProps.href = href; - buttonProps.target = 'PPFrame'; buttonProps[ 'data-paypal-button' ] = 'true'; buttonProps[ 'data-paypal-onboard-button' ] = 'true'; } diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js index 9d2bdacb2..7d044256c 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepWelcome.js @@ -12,7 +12,8 @@ import AdvancedOptionsForm from '../Components/AdvancedOptionsForm'; const StepWelcome = ( { setStep, currentStep } ) => { const { storeCountry } = CommonHooks.useWooSettings(); - const { canUseCardPayments } = OnboardingHooks.useFlags(); + const { canUseCardPayments, canUseFastlane, canUsePayLater } = + OnboardingHooks.useFlags(); const nonAcdcIcons = [ 'paypal', 'visa', 'mastercard', 'amex', 'discover' ]; return ( @@ -54,8 +55,8 @@ const StepWelcome = ( { setStep, currentStep } ) => { diff --git a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js index 9b77076e2..e1e15670a 100644 --- a/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js +++ b/modules/ppcp-settings/resources/js/Components/Screens/Settings/Components/Settings/Blocks/SavePaymentMethods.js @@ -3,6 +3,7 @@ import { __, sprintf } from '@wordpress/i18n'; import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock'; import { ControlToggleButton } from '../../../../../ReusableComponents/Controls'; import { SettingsHooks } from '../../../../../../data'; +import { useMerchantInfo } from '../../../../../../data/common/hooks'; const SavePaymentMethods = () => { const { @@ -12,6 +13,8 @@ const SavePaymentMethods = () => { setSaveCardDetails, } = SettingsHooks.useSettings(); + const { features } = useMerchantInfo(); + return ( { 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later', 'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods' ) } - value={ savePaypalAndVenmo } + value={ + features.save_paypal_and_venmo.enabled + ? savePaypalAndVenmo + : false + } onChange={ setSavePaypalAndVenmo } + disabled={ ! features.save_paypal_and_venmo.enabled } /> static function ( ContainerInterface $container ) : string { @@ -69,13 +70,17 @@ return array( $can_use_subscriptions = $container->has( 'wc-subscriptions.helper' ) && $container->get( 'wc-subscriptions.helper' ) ->plugin_is_active(); $should_skip_payment_methods = class_exists( '\WC_Payments' ); + $can_use_fastlane = $container->get( 'axo.eligible' ); + $can_use_pay_later = $container->get( 'button.helper.messages-apply' ); return new OnboardingProfile( $can_use_casual_selling, $can_use_vaulting, $can_use_card_payments, $can_use_subscriptions, - $should_skip_payment_methods + $should_skip_payment_methods, + $can_use_fastlane, + $can_use_pay_later->for_country() ); }, 'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings { @@ -168,7 +173,10 @@ return array( return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) ); }, 'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint { - return new CommonRestEndpoint( $container->get( 'settings.data.general' ) ); + return new CommonRestEndpoint( + $container->get( 'settings.data.general' ), + $container->get( 'api.endpoint.partners' ) + ); }, 'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint { return new PaymentRestEndpoint( @@ -309,7 +317,12 @@ return array( $container->get( 'api.env.endpoint.login-seller' ), $container->get( 'api.repository.partner-referrals-data' ), $container->get( 'settings.connection-state' ), - $container->get( 'api.endpoint.partners' ), + $container->get( 'settings.service.rest-service' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'settings.service.rest-service' => static function ( ContainerInterface $container ) : InternalRestService { + return new InternalRestService( $container->get( 'woocommerce.logger.woocommerce' ) ); }, diff --git a/modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php b/modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php index ed9098b48..de97e3590 100644 --- a/modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php +++ b/modules/ppcp-settings/src/DTO/MerchantConnectionDTO.php @@ -70,21 +70,21 @@ class MerchantConnectionDTO { /** * Constructor. * - * @param bool $is_sandbox Whether this connection is a sandbox account. - * @param string $client_id API client ID. - * @param string $client_secret API client secret. - * @param string $merchant_id PayPal's 13-character merchant ID. - * @param string $merchant_email Email address of the merchant account. + * @param bool $is_sandbox Whether this connection is a sandbox account. + * @param string $client_id API client ID. + * @param string $client_secret API client secret. + * @param string $merchant_id PayPal's 13-character merchant ID. + * @param string $merchant_email Email address of the merchant account. * @param string $merchant_country Merchant's country. - * @param string $seller_type Whether the merchant is a business or personal account. + * @param string $seller_type Whether the merchant is a business or personal account. */ public function __construct( bool $is_sandbox, string $client_id, string $client_secret, string $merchant_id, - string $merchant_email, - string $merchant_country, + string $merchant_email = '', + string $merchant_country = '', string $seller_type = SellerTypeEnum::UNKNOWN ) { $this->is_sandbox = $is_sandbox; diff --git a/modules/ppcp-settings/src/Data/GeneralSettings.php b/modules/ppcp-settings/src/Data/GeneralSettings.php index 7953d6d95..5c816c4cb 100644 --- a/modules/ppcp-settings/src/Data/GeneralSettings.php +++ b/modules/ppcp-settings/src/Data/GeneralSettings.php @@ -250,6 +250,11 @@ class GeneralSettings extends AbstractDataModel { * @return string */ public function get_merchant_country() : string { + // When we don't know the merchant's real country, we assume it's the Woo store-country. + if ( empty( $this->data['merchant_country'] ) ) { + return $this->woo_settings['country']; + } + return $this->data['merchant_country']; } } diff --git a/modules/ppcp-settings/src/Data/OnboardingProfile.php b/modules/ppcp-settings/src/Data/OnboardingProfile.php index aa1b6736b..6c41a1718 100644 --- a/modules/ppcp-settings/src/Data/OnboardingProfile.php +++ b/modules/ppcp-settings/src/Data/OnboardingProfile.php @@ -44,6 +44,8 @@ class OnboardingProfile extends AbstractDataModel { * @param bool $can_use_card_payments Whether credit card payments are possible. * @param bool $can_use_subscriptions Whether WC Subscriptions plugin is active. * @param bool $should_skip_payment_methods Whether it should skip payment methods screen. + * @param bool $can_use_fastlane Whether it can use Fastlane or not. + * @param bool $can_use_pay_later Whether it can use Pay Later or not. * * @throws RuntimeException If the OPTION_KEY is not defined in the child class. */ @@ -52,7 +54,9 @@ class OnboardingProfile extends AbstractDataModel { bool $can_use_vaulting = false, bool $can_use_card_payments = false, bool $can_use_subscriptions = false, - bool $should_skip_payment_methods = false + bool $should_skip_payment_methods = false, + bool $can_use_fastlane = false, + bool $can_use_pay_later = false ) { parent::__construct(); @@ -61,6 +65,8 @@ class OnboardingProfile extends AbstractDataModel { $this->flags['can_use_card_payments'] = $can_use_card_payments; $this->flags['can_use_subscriptions'] = $can_use_subscriptions; $this->flags['should_skip_payment_methods'] = $should_skip_payment_methods; + $this->flags['can_use_fastlane'] = $can_use_fastlane; + $this->flags['can_use_pay_later'] = $can_use_pay_later; } /** diff --git a/modules/ppcp-settings/src/Data/PaymentSettings.php b/modules/ppcp-settings/src/Data/PaymentSettings.php index 52ac3e419..7af4b2aa1 100644 --- a/modules/ppcp-settings/src/Data/PaymentSettings.php +++ b/modules/ppcp-settings/src/Data/PaymentSettings.php @@ -105,6 +105,12 @@ class PaymentSettings extends AbstractDataModel { return $this->get_paylater_enabled(); default: + if ( + ! did_filter( 'woocommerce_payment_gateways' ) + || doing_filter( 'woocommerce_payment_gateways' ) + ) { + return true; + } $gateway = $this->get_gateway( $method_id ); if ( $gateway ) { diff --git a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php index d6499270b..1915d998f 100644 --- a/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/AuthenticationRestEndpoint.php @@ -87,7 +87,7 @@ class AuthenticationRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/direct', array( 'methods' => WP_REST_Server::EDITABLE, @@ -125,7 +125,7 @@ class AuthenticationRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/oauth', array( 'methods' => WP_REST_Server::EDITABLE, @@ -155,7 +155,7 @@ class AuthenticationRestEndpoint extends RestEndpoint { * POST /wp-json/wc/v3/wc_paypal/authenticate/disconnect */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/disconnect', array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php index 8014e3817..323d10762 100644 --- a/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CommonRestEndpoint.php @@ -13,6 +13,8 @@ use WP_REST_Server; use WP_REST_Response; use WP_REST_Request; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; +use WooCommerce\PayPalCommerce\Settings\Service\InternalRestService; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint; /** * REST controller for "common" settings, which are used and modified by @@ -22,6 +24,11 @@ use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; * internal data model. */ class CommonRestEndpoint extends RestEndpoint { + /** + * Full REST path to the merchant-details endpoint, relative to the namespace. + */ + protected const SELLER_ACCOUNT_PATH = 'common/seller-account'; + /** * The base path for this REST controller. * @@ -36,6 +43,13 @@ class CommonRestEndpoint extends RestEndpoint { */ protected GeneralSettings $settings; + /** + * The Partners-Endpoint instance to request seller details from PayPal's API. + * + * @var PartnersEndpoint + */ + protected PartnersEndpoint $partners_endpoint; + /** * Field mapping for request to profile transformation. * @@ -104,10 +118,27 @@ class CommonRestEndpoint extends RestEndpoint { /** * Constructor. * - * @param GeneralSettings $settings The settings instance. + * @param GeneralSettings $settings The settings instance. + * @param PartnersEndpoint $partners_endpoint Partners-API to get merchant details from PayPal. */ - public function __construct( GeneralSettings $settings ) { - $this->settings = $settings; + public function __construct( GeneralSettings $settings, PartnersEndpoint $partners_endpoint ) { + $this->settings = $settings; + $this->partners_endpoint = $partners_endpoint; + } + + /** + * Returns the path to the "Get Seller Account Details" REST route. + * This is an internal route which is consumed by the plugin itself during onboarding. + * + * @param bool $full_route Whether to return the full endpoint path or just the route name. + * @return string The full path to the REST endpoint. + */ + public static function seller_account_route( bool $full_route = false ) : string { + if ( $full_route ) { + return '/' . static::NAMESPACE . '/' . self::SELLER_ACCOUNT_PATH; + } + + return self::SELLER_ACCOUNT_PATH; } /** @@ -118,7 +149,7 @@ class CommonRestEndpoint extends RestEndpoint { * GET /wp-json/wc/v3/wc_paypal/common */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::READABLE, @@ -134,7 +165,7 @@ class CommonRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, @@ -147,7 +178,7 @@ class CommonRestEndpoint extends RestEndpoint { * GET /wp-json/wc/v3/wc_paypal/common/merchant */ register_rest_route( - $this->namespace, + static::NAMESPACE, "/$this->rest_base/merchant", array( 'methods' => WP_REST_Server::READABLE, @@ -155,6 +186,19 @@ class CommonRestEndpoint extends RestEndpoint { 'permission_callback' => array( $this, 'check_permission' ), ) ); + + /** + * GET /wp-json/wc/v3/wc_paypal/common/seller-account + */ + register_rest_route( + static::NAMESPACE, + self::seller_account_route(), + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_seller_account_info' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); } /** @@ -205,6 +249,19 @@ class CommonRestEndpoint extends RestEndpoint { return $this->return_success( $js_data, $extra_data ); } + /** + * Requests details from the PayPal API. + * + * Used during onboarding to enrich the merchant details in the DB. + * + * @return WP_REST_Response Seller details, provided by PayPal's API. + */ + public function get_seller_account_info() : WP_REST_Response { + $seller_status = $this->partners_endpoint->seller_status(); + + return $this->return_success( array( 'country' => $seller_status->country() ) ); + } + /** * Appends the "merchant" attribute to the extra_data collection, which * contains details about the merchant's PayPal account, like the merchant ID. diff --git a/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php b/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php index 25d734b0a..a7290f136 100644 --- a/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/CompleteOnClickEndpoint.php @@ -62,7 +62,7 @@ class CompleteOnClickEndpoint extends RestEndpoint { */ public function register_routes(): void { register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php index 025d606bd..f5c733e40 100644 --- a/modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/FeaturesRestEndpoint.php @@ -64,7 +64,7 @@ class FeaturesRestEndpoint extends RestEndpoint { public function register_routes(): void { // GET /features - Get features list. register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( array( diff --git a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php index c2b0e9ff3..b9972349f 100644 --- a/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/LoginLinkRestEndpoint.php @@ -60,7 +60,7 @@ class LoginLinkRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php index 2572d9e6e..e9459be9d 100644 --- a/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/OnboardingRestEndpoint.php @@ -83,6 +83,12 @@ class OnboardingRestEndpoint extends RestEndpoint { 'should_skip_payment_methods' => array( 'js_name' => 'shouldSkipPaymentMethods', ), + 'can_use_fastlane' => array( + 'js_name' => 'canUseFastlane', + ), + 'can_use_pay_later' => array( + 'js_name' => 'canUsePayLater', + ), ); /** @@ -104,7 +110,7 @@ class OnboardingRestEndpoint extends RestEndpoint { * GET /wp-json/wc/v3/wc_paypal/onboarding */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::READABLE, @@ -120,7 +126,7 @@ class OnboardingRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php b/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php index 5713ce570..d8a322215 100644 --- a/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PayLaterMessagingEndpoint.php @@ -63,7 +63,7 @@ class PayLaterMessagingEndpoint extends RestEndpoint { * GET wc/v3/wc_paypal/pay_later_messaging */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::READABLE, @@ -76,7 +76,7 @@ class PayLaterMessagingEndpoint extends RestEndpoint { * POST wc/v3/wc_paypal/pay_later_messaging */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php index d718a7916..f2d813065 100644 --- a/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/PaymentRestEndpoint.php @@ -111,7 +111,7 @@ class PaymentRestEndpoint extends RestEndpoint { * GET wc/v3/wc_paypal/payment */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::READABLE, @@ -131,7 +131,7 @@ class PaymentRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php index 3b17b84ed..a5be3f207 100644 --- a/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RefreshFeatureStatusEndpoint.php @@ -87,7 +87,7 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint { * POST /wp-json/wc/v3/wc_paypal/refresh-features */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php b/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php index b922127d2..5906836d0 100644 --- a/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/ResetDismissedTodosEndpoint.php @@ -54,7 +54,7 @@ class ResetDismissedTodosEndpoint extends RestEndpoint { * POST wc/v3/wc_paypal/reset-dismissed-todos */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php index e9e9948ab..f0cf1306a 100644 --- a/modules/ppcp-settings/src/Endpoint/RestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/RestEndpoint.php @@ -18,10 +18,8 @@ use WP_REST_Response; abstract class RestEndpoint extends WC_REST_Controller { /** * Endpoint namespace. - * - * @var string */ - protected $namespace = 'wc/v3/wc_paypal'; + protected const NAMESPACE = 'wc/v3/wc_paypal'; /** * Verify access. diff --git a/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php index 9f200ed99..06f639bba 100644 --- a/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/SettingsRestEndpoint.php @@ -109,7 +109,7 @@ class SettingsRestEndpoint extends RestEndpoint { * POST wc/v3/wc_paypal/settings */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( array( diff --git a/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php index 450549e43..7e4057704 100644 --- a/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/StylingRestEndpoint.php @@ -107,7 +107,7 @@ class StylingRestEndpoint extends RestEndpoint { * GET wc/v3/wc_paypal/styling */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::READABLE, @@ -123,7 +123,7 @@ class StylingRestEndpoint extends RestEndpoint { * } */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php b/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php index b1cd77b49..fa922f2f7 100644 --- a/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/TodosRestEndpoint.php @@ -88,7 +88,7 @@ class TodosRestEndpoint extends RestEndpoint { public function register_routes(): void { // GET/POST /todos - Get todos list and update dismissed todos. register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( array( @@ -106,7 +106,7 @@ class TodosRestEndpoint extends RestEndpoint { // POST /todos/reset - Reset dismissed todos. register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/reset', array( 'methods' => WP_REST_Server::EDITABLE, @@ -117,7 +117,7 @@ class TodosRestEndpoint extends RestEndpoint { // POST /todos/complete - Mark todo as completed on click. register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/complete', array( 'methods' => WP_REST_Server::EDITABLE, diff --git a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php index 81e8f4335..6bf3c6352 100644 --- a/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php +++ b/modules/ppcp-settings/src/Endpoint/WebhookSettingsEndpoint.php @@ -79,7 +79,7 @@ class WebhookSettingsEndpoint extends RestEndpoint { * POST /wp-json/wc/v3/wc_paypal/webhooks */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base, array( array( @@ -100,7 +100,7 @@ class WebhookSettingsEndpoint extends RestEndpoint { * POST /wp-json/wc/v3/wc_paypal/webhooks/simulate */ register_rest_route( - $this->namespace, + static::NAMESPACE, '/' . $this->rest_base . '/simulate', array( array( diff --git a/modules/ppcp-settings/src/Service/AuthenticationManager.php b/modules/ppcp-settings/src/Service/AuthenticationManager.php index 9e55ee838..4badb92d7 100644 --- a/modules/ppcp-settings/src/Service/AuthenticationManager.php +++ b/modules/ppcp-settings/src/Service/AuthenticationManager.php @@ -27,6 +27,7 @@ use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO; use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar; use WooCommerce\PayPalCommerce\Settings\Enum\SellerTypeEnum; use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState; +use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint; /** * Class that manages the connection to PayPal. @@ -75,11 +76,11 @@ class AuthenticationManager { private ConnectionState $connection_state; /** - * Partners endpoint. + * Internal REST service, to consume own REST handlers in a separate request. * - * @var PartnersEndpoint + * @var InternalRestService */ - private PartnersEndpoint $partners_endpoint; + private InternalRestService $rest_service; /** * Constructor. @@ -89,7 +90,7 @@ class AuthenticationManager { * @param EnvironmentConfig $login_endpoint API handler to fetch merchant credentials. * @param PartnerReferralsData $referrals_data Partner referrals data. * @param ConnectionState $connection_state Connection state manager. - * @param PartnersEndpoint $partners_endpoint Partners endpoint. + * @param InternalRestService $rest_service Allows calling internal REST endpoints. * @param ?LoggerInterface $logger Logging instance. */ public function __construct( @@ -98,16 +99,16 @@ class AuthenticationManager { EnvironmentConfig $login_endpoint, PartnerReferralsData $referrals_data, ConnectionState $connection_state, - PartnersEndpoint $partners_endpoint, + InternalRestService $rest_service, ?LoggerInterface $logger = null ) { - $this->common_settings = $common_settings; - $this->connection_host = $connection_host; - $this->login_endpoint = $login_endpoint; - $this->referrals_data = $referrals_data; - $this->connection_state = $connection_state; - $this->partners_endpoint = $partners_endpoint; - $this->logger = $logger ?: new NullLogger(); + $this->common_settings = $common_settings; + $this->connection_host = $connection_host; + $this->login_endpoint = $login_endpoint; + $this->referrals_data = $referrals_data; + $this->connection_state = $connection_state; + $this->rest_service = $rest_service; + $this->logger = $logger ?: new NullLogger(); } /** @@ -188,6 +189,7 @@ class AuthenticationManager { * PayPal account using a client ID and secret. * * Part of the "Direct Connection" (Manual Connection) flow. + * This connection type is only available to business merchants. * * @param bool $use_sandbox Whether to use the sandbox mode. * @param string $client_id The client ID. @@ -208,19 +210,13 @@ class AuthenticationManager { $payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); - try { - $seller_status = $this->partners_endpoint->seller_status(); - } catch ( PayPalApiException $exception ) { - $seller_status = null; - } - $connection = new MerchantConnectionDTO( $use_sandbox, $client_id, $client_secret, $payee['merchant_id'], $payee['email_address'], - ! is_null( $seller_status ) ? $seller_status->country() : '', + '', SellerTypeEnum::BUSINESS ); @@ -286,17 +282,10 @@ class AuthenticationManager { */ $connection = $this->common_settings->get_merchant_data(); - try { - $seller_status = $this->partners_endpoint->seller_status(); - } catch ( PayPalApiException $exception ) { - $seller_status = null; - } - - $connection->is_sandbox = $use_sandbox; - $connection->client_id = $credentials['client_id']; - $connection->client_secret = $credentials['client_secret']; - $connection->merchant_id = $credentials['merchant_id']; - $connection->merchant_country = ! is_null( $seller_status ) ? $seller_status->country() : ''; + $connection->is_sandbox = $use_sandbox; + $connection->client_id = $credentials['client_id']; + $connection->client_secret = $credentials['client_secret']; + $connection->merchant_id = $credentials['merchant_id']; $this->update_connection_details( $connection ); } @@ -332,13 +321,6 @@ class AuthenticationManager { $connection->seller_type = $seller_type; } - try { - $seller_status = $this->partners_endpoint->seller_status(); - } catch ( PayPalApiException $exception ) { - $seller_status = null; - } - $connection->merchant_country = ! is_null( $seller_status ) ? $seller_status->country() : ''; - $this->update_connection_details( $connection ); } @@ -449,6 +431,38 @@ class AuthenticationManager { ); } + /** + * Fetches additional details about the connected merchant from PayPal + * and stores them in the DB. + * + * This process only works after persisting basic connection details. + * + * @return void + */ + private function enrich_merchant_details() : void { + if ( ! $this->common_settings->is_merchant_connected() ) { + return; + } + + try { + $endpoint = CommonRestEndpoint::seller_account_route( true ); + $details = $this->rest_service->get_response( $endpoint ); + } catch ( Throwable $exception ) { + $this->logger->warning( 'Could not determine merchant country: ' . $exception->getMessage() ); + return; + } + + // Request the merchant details via a PayPal API request. + $connection = $this->common_settings->get_merchant_data(); + + // Enrich the connection details with additional details. + $connection->merchant_country = $details['country']; + + // Persist the changes. + $this->common_settings->set_merchant_data( $connection ); + $this->common_settings->save(); + } + /** * Stores the provided details in the data model. * @@ -470,6 +484,9 @@ class AuthenticationManager { // Update the connection status and set the environment flags. $this->connection_state->connect( $connection->is_sandbox ); + // At this point, we can use the PayPal API to get more details about the seller. + $this->enrich_merchant_details(); + /** * Request to flush caches before authenticating the merchant, to * ensure the new merchant does not use stale data from previous diff --git a/modules/ppcp-settings/src/Service/InternalRestService.php b/modules/ppcp-settings/src/Service/InternalRestService.php new file mode 100644 index 000000000..66e055de9 --- /dev/null +++ b/modules/ppcp-settings/src/Service/InternalRestService.php @@ -0,0 +1,135 @@ +logger = $logger; + } + + /** + * Performs a REST call to the defined local REST endpoint. + * + * @param string $endpoint The endpoint for which the token is generated. + * @return mixed The REST response. + */ + public function get_response( string $endpoint ) { + $rest_url = rest_url( $endpoint ); + $rest_nonce = wp_create_nonce( 'wp_rest' ); + $auth_cookies = $this->build_authentication_cookie(); + + $this->logger->info( "Calling internal REST endpoint: $rest_url" ); + + $response = wp_remote_request( + $rest_url, + array( + 'method' => 'GET', + 'headers' => array( + 'Content-Type' => 'application/json', + 'X-WP-Nonce' => $rest_nonce, + ), + 'cookies' => $auth_cookies, + ) + ); + + if ( is_wp_error( $response ) ) { + $this->logger->error( 'Internal REST error', array( 'response' => $response ) ); + + return array(); + } + + $body = wp_remote_retrieve_body( $response ); + + try { + $json = json_decode( $body, true, 512, JSON_THROW_ON_ERROR ); + } catch ( Throwable $exception ) { + $this->logger->error( + 'Internal REST error: Invalid JSON response', + array( + 'error' => $exception->getMessage(), + 'response_body' => $body, + ) + ); + + return array(); + } + + if ( ! $json || empty( $json['success'] ) ) { + $this->logger->error( 'Internal REST error: Invalid response', array( 'json' => $json ) ); + + return array(); + } + + $this->logger->info( 'Internal REST success', array( 'data' => $json['data'] ) ); + + return $json['data']; + } + + /** + * Generate the cookie collection with relevant WordPress authentication + * cookies, which allows us to extend the current user's session to the + * called REST endpoint. + * + * @return array A list of cookies that are required to authenticate the user. + */ + private function build_authentication_cookie() : array { + $cookies = array(); + + // Cookie names are defined in constants and can be changed by site owners. + $wp_cookie_constants = array( 'AUTH_COOKIE', 'SECURE_AUTH_COOKIE', 'LOGGED_IN_COOKIE' ); + + foreach ( $wp_cookie_constants as $cookie_const ) { + $cookie_name = (string) constant( $cookie_const ); + + if ( ! isset( $_COOKIE[ $cookie_name ] ) ) { + continue; + } + + $cookies[] = new WP_Http_Cookie( + array( + 'name' => $cookie_name, + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + 'value' => wp_unslash( $_COOKIE[ $cookie_name ] ), + ) + ); + } + + return $cookies; + } +} diff --git a/modules/ppcp-settings/src/SettingsModule.php b/modules/ppcp-settings/src/SettingsModule.php index bf7cd64a1..f812b241b 100644 --- a/modules/ppcp-settings/src/SettingsModule.php +++ b/modules/ppcp-settings/src/SettingsModule.php @@ -53,9 +53,19 @@ class SettingsModule implements ServiceModule, ExecutableModule { * Returns whether the old settings UI should be loaded. */ public static function should_use_the_old_ui() : bool { + // New merchants should never see the legacy UI. + $show_new_ux = '1' === get_option( 'woocommerce-ppcp-is-new-merchant' ); + + if ( $show_new_ux ) { + return false; + } + + // Existing merchants can opt-in to see the new UI. + $opt_out_choice = 'yes' === get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI ); + return apply_filters( 'woocommerce_paypal_payments_should_use_the_old_ui', - get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI ) === 'yes' + $opt_out_choice ); } diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index d9e1a2277..37a075c87 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -229,4 +229,21 @@ define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' ); return class_exists( 'woocommerce' ); } + add_action( + 'woocommerce_paypal_payments_gateway_migrate', + /** + * Set new merchant flag on plugin install. + * + * When installing the plugin for the first time, we direct the user to + * the new UI without a data migration, and fully hide the legacy UI. + * + * @param string|false $version String with previous installed plugin version. + * Boolean false on first installation on a new site. + */ + static function ( $version ) { + if ( ! $version ) { + update_option( 'woocommerce-ppcp-is-new-merchant', '1' ); + } + } + ); } )();