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' );
+ }
+ }
+ );
} )();