Merge branch 'refs/heads/trunk' into PCP-4156-implement-3ds-for-google-pay

This commit is contained in:
carmenmaymo 2025-03-03 09:51:20 +01:00
commit d66a7521f5
No known key found for this signature in database
GPG key ID: 6023F686B0F3102E
35 changed files with 665 additions and 135 deletions

View file

@ -94,6 +94,45 @@ function as_schedule_single_action( $timestamp, $hook, $args = array(), $group =
return 0; 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 * HTML API: WP_HTML_Tag_Processor class
*/ */

View file

@ -91,9 +91,12 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )(); $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( if ( apply_filters(
'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled', '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" )(); $modules[] = ( require "$modules_dir/ppcp-settings/module.php" )();
} }

View file

@ -172,6 +172,16 @@ return array(
$container->get( 'settings.data.settings' ), $container->get( 'settings.data.settings' ),
$subscription_map_helper->map() $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 { '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.styling_map_helper' ),
$container->get( 'compat.settings.settings_tab_map_helper' ), $container->get( 'compat.settings.settings_tab_map_helper' ),
$container->get( 'compat.settings.subscription_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 { 'compat.settings.styling_map_helper' => static function() : StylingSettingsMapHelper {

View file

@ -10,7 +10,9 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Compat\Settings; namespace WooCommerce\PayPalCommerce\Compat\Settings;
use RuntimeException; use RuntimeException;
use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings;
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel; use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings; use WooCommerce\PayPalCommerce\Settings\Data\StylingSettings;
@ -72,6 +74,13 @@ class SettingsMapHelper {
*/ */
protected GeneralSettingsMapHelper $general_settings_map_helper; protected GeneralSettingsMapHelper $general_settings_map_helper;
/**
* Whether the new settings module is enabled.
*
* @var bool
*/
protected bool $new_settings_module_enabled;
/** /**
* Constructor. * Constructor.
* *
@ -80,6 +89,7 @@ class SettingsMapHelper {
* @param SettingsTabMapHelper $settings_tab_map_helper A helper for mapping the old/new settings tab settings. * @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 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 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. * @throws RuntimeException When an old key has multiple mappings.
*/ */
public function __construct( public function __construct(
@ -87,7 +97,8 @@ class SettingsMapHelper {
StylingSettingsMapHelper $styling_settings_map_helper, StylingSettingsMapHelper $styling_settings_map_helper,
SettingsTabMapHelper $settings_tab_map_helper, SettingsTabMapHelper $settings_tab_map_helper,
SubscriptionSettingsMapHelper $subscription_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->validate_settings_map( $settings_map );
$this->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->settings_tab_map_helper = $settings_tab_map_helper;
$this->subscription_map_helper = $subscription_map_helper; $this->subscription_map_helper = $subscription_map_helper;
$this->general_settings_map_helper = $general_settings_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. * @return mixed|null The value of the mapped setting, or null if not found.
*/ */
public function mapped_value( string $old_key ) { public function mapped_value( string $old_key ) {
if ( ! $this->new_settings_module_enabled ) {
return null;
}
$this->ensure_map_initialized(); $this->ensure_map_initialized();
if ( ! isset( $this->key_to_model[ $old_key ] ) ) { if ( ! isset( $this->key_to_model[ $old_key ] ) ) {
return null; return null;
@ -147,6 +163,10 @@ class SettingsMapHelper {
* @return bool True if the key exists in the new settings, false otherwise. * @return bool True if the key exists in the new settings, false otherwise.
*/ */
public function has_mapped_key( string $old_key ) : bool { public function has_mapped_key( string $old_key ) : bool {
if ( ! $this->new_settings_module_enabled ) {
return false;
}
$this->ensure_map_initialized(); $this->ensure_map_initialized();
return isset( $this->key_to_model[ $old_key ] ); return isset( $this->key_to_model[ $old_key ] );
@ -169,7 +189,11 @@ class SettingsMapHelper {
switch ( true ) { switch ( true ) {
case $model instanceof StylingSettings: 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: case $model instanceof GeneralSettings:
return $this->general_settings_map_helper->mapped_value( $old_key, $this->model_cache[ $model_id ] ); 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;
}
} }

View file

@ -10,7 +10,11 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat\Settings; namespace WooCommerce\PayPalCommerce\Compat\Settings;
use RuntimeException; use RuntimeException;
use WooCommerce\PayPalCommerce\Applepay\ApplePayGateway;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait; 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; use WooCommerce\PayPalCommerce\Settings\DTO\LocationStylingDTO;
/** /**
@ -23,7 +27,7 @@ class StylingSettingsMapHelper {
use ContextTrait; 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. * 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. * Retrieves the value of a mapped key from the new settings.
* *
* @param string $old_key The key from the legacy settings. * @param string $old_key The key from the legacy 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 mixed The value of the mapped setting, (null if not found). * @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 ) { switch ( $old_key ) {
case 'smart_button_locations': case 'smart_button_locations':
return $this->mapped_smart_button_locations_value( $styling_models ); 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 ); return $this->mapped_pay_later_button_locations_value( $styling_models );
case 'disable_funding': 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': 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': 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': 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: default:
foreach ( $this->locations_map() as $old_location_name => $new_location_name ) { 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. * 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. * @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(); $disabled_funding = array();
$locations_to_context_map = $this->current_context_to_new_button_location_map(); $locations_to_context_map = $this->current_context_to_new_button_location_map();
$current_context = $locations_to_context_map[ $this->context() ] ?? ''; $current_context = $locations_to_context_map[ $this->context() ] ?? '';
assert( $payment_settings instanceof PaymentSettings );
foreach ( $styling_models as $model ) { foreach ( $styling_models as $model ) {
if ( $model->location !== $current_context || in_array( 'venmo', $model->methods, true ) ) { if ( $model->location === $current_context ) {
continue; 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. * Retrieves the mapped enabled or disabled button 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 string $button_name The button name (see {@link self::BUTTON_NAMES}). * @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. * @return int The enabled (1) or disabled (0) state.
* @throws RuntimeException If an invalid button name is provided. * @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 ) ) { if ( ! in_array( $button_name, self::BUTTON_NAMES, true ) ) {
throw new RuntimeException( 'Wrong button name is provided.' ); throw new RuntimeException( 'Wrong button name is provided.' );
} }
$locations_to_context_map = $this->current_context_to_new_button_location_map(); $locations_to_context_map = $this->current_context_to_new_button_location_map();
$current_context = $locations_to_context_map[ $this->context() ] ?? ''; $current_context = $locations_to_context_map[ $this->context() ] ?? '';
assert( $payment_settings instanceof PaymentSettings );
foreach ( $styling_models as $model ) { foreach ( $styling_models as $model ) {
if ( ! $model->enabled if ( $model->enabled && $model->location === $current_context ) {
|| $model->location !== $current_context if ( in_array( $button_name, $model->methods, true ) && $payment_settings->is_method_enabled( $button_name ) ) {
|| ! in_array( $button_name, $model->methods, true ) return 1;
) { }
continue;
} }
}
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 {
?>
<style data-hide-gateway='<?php echo esc_attr( GooglePayGateway::ID ); ?>'>
.wc_payment_method.payment_method_ppcp-googlepay {
display: none;
}
</style>
<?php
}
);
} }
return 0; return 0;

View file

@ -81,22 +81,23 @@ const GooglePayComponent = ( { isEditing, buttonAttributes } ) => {
}; };
const features = [ 'products' ]; const features = [ 'products' ];
if ( buttonConfig?.is_enabled ) {
registerExpressPaymentMethod( { registerExpressPaymentMethod( {
name: buttonData.id, name: buttonData.id,
title: `PayPal - ${ buttonData.title }`, title: `PayPal - ${ buttonData.title }`,
description: __( description: __(
'Eligible users will see the PayPal button.', 'Eligible users will see the PayPal button.',
'woocommerce-paypal-payments' 'woocommerce-paypal-payments'
), ),
gatewayId: 'ppcp-gateway', gatewayId: 'ppcp-gateway',
label: <div dangerouslySetInnerHTML={ { __html: buttonData.title } } />, label: <div dangerouslySetInnerHTML={ { __html: buttonData.title } } />,
content: <GooglePayComponent isEditing={ false } />, content: <GooglePayComponent isEditing={ false } />,
edit: <GooglePayComponent isEditing={ true } />, edit: <GooglePayComponent isEditing={ true } />,
ariaLabel: buttonData.title, ariaLabel: buttonData.title,
canMakePayment: () => buttonData.enabled, canMakePayment: () => buttonData.enabled,
supports: { supports: {
features, features,
style: [ 'height', 'borderRadius' ], style: [ 'height', 'borderRadius' ],
}, },
} ); } );
}

View file

@ -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.
<details>
<summary><strong>Setup the PayPal REST app</strong></summary>
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
</details>
**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<br/>(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.

View file

@ -32,7 +32,6 @@ const ButtonOrPlaceholder = ( {
if ( href ) { if ( href ) {
buttonProps.href = href; buttonProps.href = href;
buttonProps.target = 'PPFrame';
buttonProps[ 'data-paypal-button' ] = 'true'; buttonProps[ 'data-paypal-button' ] = 'true';
buttonProps[ 'data-paypal-onboard-button' ] = 'true'; buttonProps[ 'data-paypal-onboard-button' ] = 'true';
} }

View file

@ -12,7 +12,8 @@ import AdvancedOptionsForm from '../Components/AdvancedOptionsForm';
const StepWelcome = ( { setStep, currentStep } ) => { const StepWelcome = ( { setStep, currentStep } ) => {
const { storeCountry } = CommonHooks.useWooSettings(); const { storeCountry } = CommonHooks.useWooSettings();
const { canUseCardPayments } = OnboardingHooks.useFlags(); const { canUseCardPayments, canUseFastlane, canUsePayLater } =
OnboardingHooks.useFlags();
const nonAcdcIcons = [ 'paypal', 'visa', 'mastercard', 'amex', 'discover' ]; const nonAcdcIcons = [ 'paypal', 'visa', 'mastercard', 'amex', 'discover' ];
return ( return (
@ -54,8 +55,8 @@ const StepWelcome = ( { setStep, currentStep } ) => {
<Separator className="ppcp-r-page-welcome-mode-separator" /> <Separator className="ppcp-r-page-welcome-mode-separator" />
<WelcomeDocs <WelcomeDocs
useAcdc={ canUseCardPayments } useAcdc={ canUseCardPayments }
isFastlane={ true } isFastlane={ canUseFastlane }
isPayLater={ true } isPayLater={ canUsePayLater }
storeCountry={ storeCountry } storeCountry={ storeCountry }
/> />
<Separator text={ __( 'or', 'woocommerce-paypal-payments' ) } /> <Separator text={ __( 'or', 'woocommerce-paypal-payments' ) } />

View file

@ -3,6 +3,7 @@ import { __, sprintf } from '@wordpress/i18n';
import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock'; import SettingsBlock from '../../../../../ReusableComponents/SettingsBlock';
import { ControlToggleButton } from '../../../../../ReusableComponents/Controls'; import { ControlToggleButton } from '../../../../../ReusableComponents/Controls';
import { SettingsHooks } from '../../../../../../data'; import { SettingsHooks } from '../../../../../../data';
import { useMerchantInfo } from '../../../../../../data/common/hooks';
const SavePaymentMethods = () => { const SavePaymentMethods = () => {
const { const {
@ -12,6 +13,8 @@ const SavePaymentMethods = () => {
setSaveCardDetails, setSaveCardDetails,
} = SettingsHooks.useSettings(); } = SettingsHooks.useSettings();
const { features } = useMerchantInfo();
return ( return (
<SettingsBlock <SettingsBlock
title={ __( title={ __(
@ -38,8 +41,13 @@ const SavePaymentMethods = () => {
'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later', 'https://woocommerce.com/document/woocommerce-paypal-payments/#pay-later',
'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods' 'https://woocommerce.com/document/woocommerce-paypal-payments/#alternative-payment-methods'
) } ) }
value={ savePaypalAndVenmo } value={
features.save_paypal_and_venmo.enabled
? savePaypalAndVenmo
: false
}
onChange={ setSavePaypalAndVenmo } onChange={ setSavePaypalAndVenmo }
disabled={ ! features.save_paypal_and_venmo.enabled }
/> />
<ControlToggleButton <ControlToggleButton

View file

@ -24,6 +24,8 @@ const defaultTransient = Object.freeze( {
canUseCardPayments: false, canUseCardPayments: false,
canUseSubscriptions: false, canUseSubscriptions: false,
shouldSkipPaymentMethods: false, shouldSkipPaymentMethods: false,
canUseFastlane: false,
canUsePayLater: false,
} ), } ),
} ); } );

View file

@ -49,6 +49,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig; use WooCommerce\PayPalCommerce\PayLaterConfigurator\Endpoint\SaveConfig;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment; use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState; use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState;
use WooCommerce\PayPalCommerce\Settings\Service\InternalRestService;
return array( return array(
'settings.url' => static function ( ContainerInterface $container ) : string { 'settings.url' => static function ( ContainerInterface $container ) : string {
@ -69,13 +70,17 @@ return array(
$can_use_subscriptions = $container->has( 'wc-subscriptions.helper' ) && $container->get( 'wc-subscriptions.helper' ) $can_use_subscriptions = $container->has( 'wc-subscriptions.helper' ) && $container->get( 'wc-subscriptions.helper' )
->plugin_is_active(); ->plugin_is_active();
$should_skip_payment_methods = class_exists( '\WC_Payments' ); $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( return new OnboardingProfile(
$can_use_casual_selling, $can_use_casual_selling,
$can_use_vaulting, $can_use_vaulting,
$can_use_card_payments, $can_use_card_payments,
$can_use_subscriptions, $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 { 'settings.data.general' => static function ( ContainerInterface $container ) : GeneralSettings {
@ -168,7 +173,10 @@ return array(
return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) ); return new OnboardingRestEndpoint( $container->get( 'settings.data.onboarding' ) );
}, },
'settings.rest.common' => static function ( ContainerInterface $container ) : CommonRestEndpoint { '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 { 'settings.rest.payment' => static function ( ContainerInterface $container ) : PaymentRestEndpoint {
return new PaymentRestEndpoint( return new PaymentRestEndpoint(
@ -309,7 +317,12 @@ return array(
$container->get( 'api.env.endpoint.login-seller' ), $container->get( 'api.env.endpoint.login-seller' ),
$container->get( 'api.repository.partner-referrals-data' ), $container->get( 'api.repository.partner-referrals-data' ),
$container->get( 'settings.connection-state' ), $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' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },

View file

@ -70,21 +70,21 @@ class MerchantConnectionDTO {
/** /**
* Constructor. * Constructor.
* *
* @param bool $is_sandbox Whether this connection is a sandbox account. * @param bool $is_sandbox Whether this connection is a sandbox account.
* @param string $client_id API client ID. * @param string $client_id API client ID.
* @param string $client_secret API client secret. * @param string $client_secret API client secret.
* @param string $merchant_id PayPal's 13-character merchant ID. * @param string $merchant_id PayPal's 13-character merchant ID.
* @param string $merchant_email Email address of the merchant account. * @param string $merchant_email Email address of the merchant account.
* @param string $merchant_country Merchant's country. * @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( public function __construct(
bool $is_sandbox, bool $is_sandbox,
string $client_id, string $client_id,
string $client_secret, string $client_secret,
string $merchant_id, string $merchant_id,
string $merchant_email, string $merchant_email = '',
string $merchant_country, string $merchant_country = '',
string $seller_type = SellerTypeEnum::UNKNOWN string $seller_type = SellerTypeEnum::UNKNOWN
) { ) {
$this->is_sandbox = $is_sandbox; $this->is_sandbox = $is_sandbox;

View file

@ -250,6 +250,11 @@ class GeneralSettings extends AbstractDataModel {
* @return string * @return string
*/ */
public function get_merchant_country() : 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']; return $this->data['merchant_country'];
} }
} }

View file

@ -44,6 +44,8 @@ class OnboardingProfile extends AbstractDataModel {
* @param bool $can_use_card_payments Whether credit card payments are possible. * @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 $can_use_subscriptions Whether WC Subscriptions plugin is active.
* @param bool $should_skip_payment_methods Whether it should skip payment methods screen. * @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. * @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_vaulting = false,
bool $can_use_card_payments = false, bool $can_use_card_payments = false,
bool $can_use_subscriptions = 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(); parent::__construct();
@ -61,6 +65,8 @@ class OnboardingProfile extends AbstractDataModel {
$this->flags['can_use_card_payments'] = $can_use_card_payments; $this->flags['can_use_card_payments'] = $can_use_card_payments;
$this->flags['can_use_subscriptions'] = $can_use_subscriptions; $this->flags['can_use_subscriptions'] = $can_use_subscriptions;
$this->flags['should_skip_payment_methods'] = $should_skip_payment_methods; $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;
} }
/** /**

View file

@ -105,6 +105,12 @@ class PaymentSettings extends AbstractDataModel {
return $this->get_paylater_enabled(); return $this->get_paylater_enabled();
default: default:
if (
! did_filter( 'woocommerce_payment_gateways' )
|| doing_filter( 'woocommerce_payment_gateways' )
) {
return true;
}
$gateway = $this->get_gateway( $method_id ); $gateway = $this->get_gateway( $method_id );
if ( $gateway ) { if ( $gateway ) {

View file

@ -87,7 +87,7 @@ class AuthenticationRestEndpoint extends RestEndpoint {
* } * }
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base . '/direct', '/' . $this->rest_base . '/direct',
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,
@ -125,7 +125,7 @@ class AuthenticationRestEndpoint extends RestEndpoint {
* } * }
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base . '/oauth', '/' . $this->rest_base . '/oauth',
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,
@ -155,7 +155,7 @@ class AuthenticationRestEndpoint extends RestEndpoint {
* POST /wp-json/wc/v3/wc_paypal/authenticate/disconnect * POST /wp-json/wc/v3/wc_paypal/authenticate/disconnect
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base . '/disconnect', '/' . $this->rest_base . '/disconnect',
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,

View file

@ -13,6 +13,8 @@ use WP_REST_Server;
use WP_REST_Response; use WP_REST_Response;
use WP_REST_Request; use WP_REST_Request;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings; 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 * REST controller for "common" settings, which are used and modified by
@ -22,6 +24,11 @@ use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
* internal data model. * internal data model.
*/ */
class CommonRestEndpoint extends RestEndpoint { 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. * The base path for this REST controller.
* *
@ -36,6 +43,13 @@ class CommonRestEndpoint extends RestEndpoint {
*/ */
protected GeneralSettings $settings; 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. * Field mapping for request to profile transformation.
* *
@ -104,10 +118,27 @@ class CommonRestEndpoint extends RestEndpoint {
/** /**
* Constructor. * 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 ) { public function __construct( GeneralSettings $settings, PartnersEndpoint $partners_endpoint ) {
$this->settings = $settings; $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 * GET /wp-json/wc/v3/wc_paypal/common
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::READABLE, 'methods' => WP_REST_Server::READABLE,
@ -134,7 +165,7 @@ class CommonRestEndpoint extends RestEndpoint {
* } * }
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,
@ -147,7 +178,7 @@ class CommonRestEndpoint extends RestEndpoint {
* GET /wp-json/wc/v3/wc_paypal/common/merchant * GET /wp-json/wc/v3/wc_paypal/common/merchant
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
"/$this->rest_base/merchant", "/$this->rest_base/merchant",
array( array(
'methods' => WP_REST_Server::READABLE, 'methods' => WP_REST_Server::READABLE,
@ -155,6 +186,19 @@ class CommonRestEndpoint extends RestEndpoint {
'permission_callback' => array( $this, 'check_permission' ), '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 ); 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 * Appends the "merchant" attribute to the extra_data collection, which
* contains details about the merchant's PayPal account, like the merchant ID. * contains details about the merchant's PayPal account, like the merchant ID.

View file

@ -62,7 +62,7 @@ class CompleteOnClickEndpoint extends RestEndpoint {
*/ */
public function register_routes(): void { public function register_routes(): void {
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,

View file

@ -64,7 +64,7 @@ class FeaturesRestEndpoint extends RestEndpoint {
public function register_routes(): void { public function register_routes(): void {
// GET /features - Get features list. // GET /features - Get features list.
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
array( array(

View file

@ -60,7 +60,7 @@ class LoginLinkRestEndpoint extends RestEndpoint {
* } * }
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,

View file

@ -83,6 +83,12 @@ class OnboardingRestEndpoint extends RestEndpoint {
'should_skip_payment_methods' => array( 'should_skip_payment_methods' => array(
'js_name' => 'shouldSkipPaymentMethods', '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 * GET /wp-json/wc/v3/wc_paypal/onboarding
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::READABLE, 'methods' => WP_REST_Server::READABLE,
@ -120,7 +126,7 @@ class OnboardingRestEndpoint extends RestEndpoint {
* } * }
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,

View file

@ -63,7 +63,7 @@ class PayLaterMessagingEndpoint extends RestEndpoint {
* GET wc/v3/wc_paypal/pay_later_messaging * GET wc/v3/wc_paypal/pay_later_messaging
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::READABLE, 'methods' => WP_REST_Server::READABLE,
@ -76,7 +76,7 @@ class PayLaterMessagingEndpoint extends RestEndpoint {
* POST wc/v3/wc_paypal/pay_later_messaging * POST wc/v3/wc_paypal/pay_later_messaging
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,

View file

@ -111,7 +111,7 @@ class PaymentRestEndpoint extends RestEndpoint {
* GET wc/v3/wc_paypal/payment * GET wc/v3/wc_paypal/payment
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::READABLE, 'methods' => WP_REST_Server::READABLE,
@ -131,7 +131,7 @@ class PaymentRestEndpoint extends RestEndpoint {
* } * }
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,

View file

@ -87,7 +87,7 @@ class RefreshFeatureStatusEndpoint extends RestEndpoint {
* POST /wp-json/wc/v3/wc_paypal/refresh-features * POST /wp-json/wc/v3/wc_paypal/refresh-features
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,

View file

@ -54,7 +54,7 @@ class ResetDismissedTodosEndpoint extends RestEndpoint {
* POST wc/v3/wc_paypal/reset-dismissed-todos * POST wc/v3/wc_paypal/reset-dismissed-todos
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,

View file

@ -18,10 +18,8 @@ use WP_REST_Response;
abstract class RestEndpoint extends WC_REST_Controller { abstract class RestEndpoint extends WC_REST_Controller {
/** /**
* Endpoint namespace. * Endpoint namespace.
*
* @var string
*/ */
protected $namespace = 'wc/v3/wc_paypal'; protected const NAMESPACE = 'wc/v3/wc_paypal';
/** /**
* Verify access. * Verify access.

View file

@ -109,7 +109,7 @@ class SettingsRestEndpoint extends RestEndpoint {
* POST wc/v3/wc_paypal/settings * POST wc/v3/wc_paypal/settings
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
array( array(

View file

@ -107,7 +107,7 @@ class StylingRestEndpoint extends RestEndpoint {
* GET wc/v3/wc_paypal/styling * GET wc/v3/wc_paypal/styling
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::READABLE, 'methods' => WP_REST_Server::READABLE,
@ -123,7 +123,7 @@ class StylingRestEndpoint extends RestEndpoint {
* } * }
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,

View file

@ -88,7 +88,7 @@ class TodosRestEndpoint extends RestEndpoint {
public function register_routes(): void { public function register_routes(): void {
// GET/POST /todos - Get todos list and update dismissed todos. // GET/POST /todos - Get todos list and update dismissed todos.
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
array( array(
@ -106,7 +106,7 @@ class TodosRestEndpoint extends RestEndpoint {
// POST /todos/reset - Reset dismissed todos. // POST /todos/reset - Reset dismissed todos.
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base . '/reset', '/' . $this->rest_base . '/reset',
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,
@ -117,7 +117,7 @@ class TodosRestEndpoint extends RestEndpoint {
// POST /todos/complete - Mark todo as completed on click. // POST /todos/complete - Mark todo as completed on click.
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base . '/complete', '/' . $this->rest_base . '/complete',
array( array(
'methods' => WP_REST_Server::EDITABLE, 'methods' => WP_REST_Server::EDITABLE,

View file

@ -79,7 +79,7 @@ class WebhookSettingsEndpoint extends RestEndpoint {
* POST /wp-json/wc/v3/wc_paypal/webhooks * POST /wp-json/wc/v3/wc_paypal/webhooks
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base, '/' . $this->rest_base,
array( array(
array( array(
@ -100,7 +100,7 @@ class WebhookSettingsEndpoint extends RestEndpoint {
* POST /wp-json/wc/v3/wc_paypal/webhooks/simulate * POST /wp-json/wc/v3/wc_paypal/webhooks/simulate
*/ */
register_rest_route( register_rest_route(
$this->namespace, static::NAMESPACE,
'/' . $this->rest_base . '/simulate', '/' . $this->rest_base . '/simulate',
array( array(
array( array(

View file

@ -27,6 +27,7 @@ use WooCommerce\PayPalCommerce\Settings\DTO\MerchantConnectionDTO;
use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar; use WooCommerce\PayPalCommerce\Webhooks\WebhookRegistrar;
use WooCommerce\PayPalCommerce\Settings\Enum\SellerTypeEnum; use WooCommerce\PayPalCommerce\Settings\Enum\SellerTypeEnum;
use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState; use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState;
use WooCommerce\PayPalCommerce\Settings\Endpoint\CommonRestEndpoint;
/** /**
* Class that manages the connection to PayPal. * Class that manages the connection to PayPal.
@ -75,11 +76,11 @@ class AuthenticationManager {
private ConnectionState $connection_state; 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. * Constructor.
@ -89,7 +90,7 @@ class AuthenticationManager {
* @param EnvironmentConfig $login_endpoint API handler to fetch merchant credentials. * @param EnvironmentConfig $login_endpoint API handler to fetch merchant credentials.
* @param PartnerReferralsData $referrals_data Partner referrals data. * @param PartnerReferralsData $referrals_data Partner referrals data.
* @param ConnectionState $connection_state Connection state manager. * @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. * @param ?LoggerInterface $logger Logging instance.
*/ */
public function __construct( public function __construct(
@ -98,16 +99,16 @@ class AuthenticationManager {
EnvironmentConfig $login_endpoint, EnvironmentConfig $login_endpoint,
PartnerReferralsData $referrals_data, PartnerReferralsData $referrals_data,
ConnectionState $connection_state, ConnectionState $connection_state,
PartnersEndpoint $partners_endpoint, InternalRestService $rest_service,
?LoggerInterface $logger = null ?LoggerInterface $logger = null
) { ) {
$this->common_settings = $common_settings; $this->common_settings = $common_settings;
$this->connection_host = $connection_host; $this->connection_host = $connection_host;
$this->login_endpoint = $login_endpoint; $this->login_endpoint = $login_endpoint;
$this->referrals_data = $referrals_data; $this->referrals_data = $referrals_data;
$this->connection_state = $connection_state; $this->connection_state = $connection_state;
$this->partners_endpoint = $partners_endpoint; $this->rest_service = $rest_service;
$this->logger = $logger ?: new NullLogger(); $this->logger = $logger ?: new NullLogger();
} }
/** /**
@ -188,6 +189,7 @@ class AuthenticationManager {
* PayPal account using a client ID and secret. * PayPal account using a client ID and secret.
* *
* Part of the "Direct Connection" (Manual Connection) flow. * 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 bool $use_sandbox Whether to use the sandbox mode.
* @param string $client_id The client ID. * @param string $client_id The client ID.
@ -208,19 +210,13 @@ class AuthenticationManager {
$payee = $this->request_payee( $client_id, $client_secret, $use_sandbox ); $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( $connection = new MerchantConnectionDTO(
$use_sandbox, $use_sandbox,
$client_id, $client_id,
$client_secret, $client_secret,
$payee['merchant_id'], $payee['merchant_id'],
$payee['email_address'], $payee['email_address'],
! is_null( $seller_status ) ? $seller_status->country() : '', '',
SellerTypeEnum::BUSINESS SellerTypeEnum::BUSINESS
); );
@ -286,17 +282,10 @@ class AuthenticationManager {
*/ */
$connection = $this->common_settings->get_merchant_data(); $connection = $this->common_settings->get_merchant_data();
try { $connection->is_sandbox = $use_sandbox;
$seller_status = $this->partners_endpoint->seller_status(); $connection->client_id = $credentials['client_id'];
} catch ( PayPalApiException $exception ) { $connection->client_secret = $credentials['client_secret'];
$seller_status = null; $connection->merchant_id = $credentials['merchant_id'];
}
$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() : '';
$this->update_connection_details( $connection ); $this->update_connection_details( $connection );
} }
@ -332,13 +321,6 @@ class AuthenticationManager {
$connection->seller_type = $seller_type; $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 ); $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. * Stores the provided details in the data model.
* *
@ -470,6 +484,9 @@ class AuthenticationManager {
// Update the connection status and set the environment flags. // Update the connection status and set the environment flags.
$this->connection_state->connect( $connection->is_sandbox ); $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 * Request to flush caches before authenticating the merchant, to
* ensure the new merchant does not use stale data from previous * ensure the new merchant does not use stale data from previous

View file

@ -0,0 +1,135 @@
<?php
/**
* Service that allows calling internal REST endpoints from server-side.
*
* @package WooCommerce\PayPalCommerce\Settings\Service
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Settings\Service;
use Throwable;
use Psr\Log\LoggerInterface;
use WP_Http_Cookie;
/**
* Consume internal REST endpoints from server-side.
*
* This service makes a real HTTP request to the endpoint, forwarding the
* authentication cookies of the current request to maintain the user session
* while invoking a completely isolated and freshly initialized server request.
*/
class InternalRestService {
/**
* Logger instance.
*
* In this case, the logger is quite important for debugging, because the main
* functionality of this class cannot be step-debugged using Xdebug: While
* a Xdebug session is active, the remote call to the current server is also
* blocked and will end in a timeout.
*
* @var LoggerInterface
*/
private LoggerInterface $logger;
/**
* Constructor.
*
* @param LoggerInterface $logger Logger instance.
*/
public function __construct( LoggerInterface $logger ) {
$this->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;
}
}

View file

@ -53,9 +53,19 @@ class SettingsModule implements ServiceModule, ExecutableModule {
* Returns whether the old settings UI should be loaded. * Returns whether the old settings UI should be loaded.
*/ */
public static function should_use_the_old_ui() : bool { 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( return apply_filters(
'woocommerce_paypal_payments_should_use_the_old_ui', 'woocommerce_paypal_payments_should_use_the_old_ui',
get_option( SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI ) === 'yes' $opt_out_choice
); );
} }

View file

@ -229,4 +229,21 @@ define( 'PPCP_PAYPAL_BN_CODE', 'Woo_PPCP' );
return class_exists( 'woocommerce' ); 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' );
}
}
);
} )(); } )();