Merge pull request #3456 from woocommerce/PCP-4803-modify-create-order-payload-to-display-email-and-phone-in-pay-pal-popup

Modify Create Order payload to display email and phone in PayPal popup (4803)
This commit is contained in:
Emili Castells 2025-06-23 10:54:23 +02:00 committed by GitHub
commit 8fe7615eec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 276 additions and 30 deletions

View file

@ -32,7 +32,6 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Settings\Service\BrandedExperience\ActivationDetector;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
@ -82,6 +81,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Settings\Enum\InstallationPathEnum;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ContactPreferenceFactory;
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
return array(
'api.host' => static function( ContainerInterface $container ) : string {
@ -330,6 +331,22 @@ return array(
$container->get( 'api.endpoint.order' )
);
},
'api.factory.contact-preference' => static function ( ContainerInterface $container ): ContactPreferenceFactory {
if ( $container->has( 'settings.data.settings' ) ) {
$settings = $container->get( 'settings.data.settings' );
assert( $settings instanceof SettingsModel );
$contact_module_active = $settings->get_enable_contact_module();
} else {
// #legacy-ui: Auto-enable the feature; can be disabled via eligibility hook.
$contact_module_active = true;
}
return new ContactPreferenceFactory(
$contact_module_active,
$container->get( 'settings.merchant-details' )
);
},
'api.factory.payment-token' => static function ( ContainerInterface $container ) : PaymentTokenFactory {
return new PaymentTokenFactory();
},

View file

@ -30,6 +30,9 @@ class ExperienceContext {
public const PAYMENT_METHOD_UNRESTRICTED = 'UNRESTRICTED';
public const PAYMENT_METHOD_IMMEDIATE_PAYMENT_REQUIRED = 'IMMEDIATE_PAYMENT_REQUIRED';
public const CONTACT_PREFERENCE_NO_CONTACT_INFO = 'NO_CONTACT_INFO';
public const CONTACT_PREFERENCE_UPDATE_CONTACT_INFO = 'UPDATE_CONTACT_INFO';
/**
* The return url.
*/
@ -70,6 +73,12 @@ class ExperienceContext {
*/
private ?string $payment_method_preference = null;
/**
* Controls the contact module, and when defined, the API response will
* include additional details in the `purchase_units[].shipping` object.
*/
private ?string $contact_preference = null;
/**
* The callback config.
*/
@ -229,6 +238,28 @@ class ExperienceContext {
return $obj;
}
/**
* Returns the contact preference.
*/
public function contact_preference(): ?string {
return $this->contact_preference;
}
/**
* Sets the contact preference.
*
* This preference is only available for the payment source 'paypal' and 'venmo'.
* https://developer.paypal.com/docs/api/orders/v2/#definition-paypal_wallet_experience_context
*
* @param string|null $new_value The value to set.
*/
public function with_contact_preference( ?string $new_value ): ExperienceContext {
$obj = clone $this;
$obj->contact_preference = $new_value;
return $obj;
}
/**
* Returns the callback config.
*/

View file

@ -0,0 +1,72 @@
<?php
/**
* Returns contact_preference for the given state.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ExperienceContext;
use WooCommerce\PayPalCommerce\WcGateway\Helper\MerchantDetails;
/**
* Class ContactPreferenceFactory
*/
class ContactPreferenceFactory {
/**
* Whether the contact module toggle is enabled in the plugin settings.
* Allows eligible merchants to opt out of the feature.
*/
private bool $is_contact_module_active;
/**
* Used to determine if a merchant is eligible to use the contact preference.
*/
private MerchantDetails $merchant_details;
/**
* Constructor.
*
* @param bool $is_contact_module_active Whether custom contact details are enabled
* in the plugin settings.
* @param MerchantDetails $merchant_details Service 'settings.merchant-details'.
*/
public function __construct(
bool $is_contact_module_active,
MerchantDetails $merchant_details
) {
$this->is_contact_module_active = $is_contact_module_active;
$this->merchant_details = $merchant_details;
}
/**
* Returns contact_preference for the given state.
*
* @param string $payment_source_key Name of the payment_source.
* @return string|null
*/
public function from_state( string $payment_source_key ) : ?string {
$payment_sources_with_contact = array( 'paypal', 'venmo' );
/**
* In case the payment-source does not support the contact-info preference
* we return null to remove the property from the context.
*/
if ( ! in_array( $payment_source_key, $payment_sources_with_contact, true ) ) {
return null;
}
if ( ! $this->is_contact_module_active ) {
return ExperienceContext::CONTACT_PREFERENCE_NO_CONTACT_INFO;
}
if ( ! $this->merchant_details->is_eligible_for( MerchantDetails::FEATURE_CONTACT_MODULE ) ) {
return ExperienceContext::CONTACT_PREFERENCE_NO_CONTACT_INFO;
}
return ExperienceContext::CONTACT_PREFERENCE_UPDATE_CONTACT_INFO;
}
}

View file

@ -181,6 +181,20 @@ class ExperienceContextBuilder {
return $builder;
}
/**
* Applies a custom contact preference to the experience context.
*
* @param string|null $preference The new preference to apply.
*/
public function with_contact_preference( ?string $preference = null ) : ExperienceContextBuilder {
$builder = clone $this;
$builder->experience_context = $builder->experience_context
->with_contact_preference( $preference );
return $builder;
}
/**
* Returns the ExperienceContext.
*/

View file

@ -228,6 +228,7 @@ return array(
$request_data,
$purchase_unit_factory,
$container->get( 'api.factory.shipping-preference' ),
$container->get( 'api.factory.contact-preference' ),
$container->get( 'wcgateway.builder.experience-context' ),
$order_endpoint,
$payer_factory,

View file

@ -37,6 +37,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ContactPreferenceFactory;
/**
* Class CreateOrderEndpoint
@ -68,6 +69,11 @@ class CreateOrderEndpoint implements EndpointInterface {
*/
private $shipping_preference_factory;
/**
* The contact_preference factors.
*/
private ContactPreferenceFactory $contact_preference_factory;
/**
* The ExperienceContextBuilder.
*/
@ -189,6 +195,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* @param RequestData $request_data The RequestData object.
* @param PurchaseUnitFactory $purchase_unit_factory The PurchaseUnit factory.
* @param ShippingPreferenceFactory $shipping_preference_factory The shipping_preference factory.
* @param ContactPreferenceFactory $contact_preference_factory The contact_preference factory.
* @param ExperienceContextBuilder $experience_context_builder The ExperienceContextBuilder.
* @param OrderEndpoint $order_endpoint The OrderEndpoint object.
* @param PayerFactory $payer_factory The PayerFactory object.
@ -208,6 +215,7 @@ class CreateOrderEndpoint implements EndpointInterface {
RequestData $request_data,
PurchaseUnitFactory $purchase_unit_factory,
ShippingPreferenceFactory $shipping_preference_factory,
ContactPreferenceFactory $contact_preference_factory,
ExperienceContextBuilder $experience_context_builder,
OrderEndpoint $order_endpoint,
PayerFactory $payer_factory,
@ -224,23 +232,24 @@ class CreateOrderEndpoint implements EndpointInterface {
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->shipping_preference_factory = $shipping_preference_factory;
$this->experience_context_builder = $experience_context_builder;
$this->api_endpoint = $order_endpoint;
$this->payer_factory = $payer_factory;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->early_order_handler = $early_order_handler;
$this->registration_needed = $registration_needed;
$this->card_billing_data_mode = $card_billing_data_mode;
$this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts;
$this->handle_shipping_in_paypal = $handle_shipping_in_paypal;
$this->request_data = $request_data;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->shipping_preference_factory = $shipping_preference_factory;
$this->contact_preference_factory = $contact_preference_factory;
$this->experience_context_builder = $experience_context_builder;
$this->api_endpoint = $order_endpoint;
$this->payer_factory = $payer_factory;
$this->session_handler = $session_handler;
$this->settings = $settings;
$this->early_order_handler = $early_order_handler;
$this->registration_needed = $registration_needed;
$this->card_billing_data_mode = $card_billing_data_mode;
$this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts;
$this->handle_shipping_in_paypal = $handle_shipping_in_paypal;
$this->server_side_shipping_callback_enabled = $server_side_shipping_callback_enabled;
$this->funding_sources_without_redirect = $funding_sources_without_redirect;
$this->logger = $logger;
$this->funding_sources_without_redirect = $funding_sources_without_redirect;
$this->logger = $logger;
}
/**
@ -449,12 +458,20 @@ class CreateOrderEndpoint implements EndpointInterface {
}
}
$payment_source_key = 'paypal';
if ( in_array( $funding_source, array( 'venmo' ), true ) ) {
$payment_source_key = $funding_source;
if ( 'venmo' === $funding_source ) {
$payment_source_key = 'venmo';
} else {
$payment_source_key = 'paypal';
}
$experience_context = $this->experience_context_builder->with_default_paypal_config( $shipping_preference, $action );
$contact_preference = $this->contact_preference_factory->from_state(
$payment_source_key
);
$experience_context = $this->experience_context_builder
->with_default_paypal_config( $shipping_preference, $action )
->with_contact_preference( $contact_preference );
if ( $this->server_side_shipping_callback_enabled
&& $shipping_preference === ExperienceContext::SHIPPING_PREFERENCE_GET_FROM_FILE ) {
$experience_context = $experience_context->with_shipping_callback();

View file

@ -104,9 +104,10 @@ return array(
return $state->get_environment();
},
'settings.merchant-details' => static function ( ContainerInterface $container ) : MerchantDetails {
$woo_country = $container->get( 'api.shop.country' );
$woo_country = $container->get( 'api.shop.country' );
$eligibility_checks = $container->get( 'wcgateway.feature-eligibility.list' );
return new MerchantDetails( $woo_country, $woo_country );
return new MerchantDetails( $woo_country, $woo_country, $eligibility_checks );
},
'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets {
$state = $container->get( 'onboarding.state' );

View file

@ -672,6 +672,8 @@ return array(
$merchant_country = $data->get_merchant_country();
$woo_data = $data->get_woo_settings();
return new MerchantDetails( $merchant_country, $woo_data['country'] );
$eligibility_checks = $container->get( 'wcgateway.feature-eligibility.list' );
return new MerchantDetails( $merchant_country, $woo_data['country'], $eligibility_checks );
},
);

View file

@ -16,6 +16,8 @@ use WooCommerce\PayPalCommerce\Settings\Service\DataSanitizer;
* Class SettingsModel
*
* Handles the storage and retrieval of PayPal Commerce settings in WordPress options table.
*
* DI Service: 'settings.data.settings'
*/
class SettingsModel extends AbstractDataModel {
@ -97,7 +99,7 @@ class SettingsModel extends AbstractDataModel {
'authorize_only' => false,
'capture_virtual_orders' => false,
'save_paypal_and_venmo' => false,
'enable_contact_module' => false,
'enable_contact_module' => true,
'save_card_details' => false,
'enable_pay_now' => false,
'enable_logging' => false,

View file

@ -2172,6 +2172,23 @@ return array(
);
};
},
/**
* Returns a centralized list of feature eligibility checks.
*
* This is a helper service which is used by the `MerchantDetails` class and
* should not be directly accessed.
*/
'wcgateway.feature-eligibility.list' => static function( ContainerInterface $container ): array {
return array(
MerchantDetails::FEATURE_SAVE_PAYPAL_VENMO => $container->get( 'save-payment-methods.eligibility.check' ),
MerchantDetails::FEATURE_ADVANCED_CARD_PROCESSING => $container->get( 'card-fields.eligibility.check' ),
MerchantDetails::FEATURE_GOOGLE_PAY => $container->get( 'googlepay.eligibility.check' ),
MerchantDetails::FEATURE_APPLE_PAY => $container->get( 'applepay.eligibility.check' ),
MerchantDetails::FEATURE_CONTACT_MODULE => $container->get( 'wcgateway.contact-module.eligibility.check' ),
);
},
/**
* Returns a prefix for the site, ensuring the same site always gets the same prefix (unless the URL changes).
*/

View file

@ -13,6 +13,34 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Helper;
* Main information source about merchant details.
*/
class MerchantDetails {
/**
* Save tokenized PayPal and Venmo payment details, required for subscriptions and saving
* payment methods in user account.
*/
public const FEATURE_SAVE_PAYPAL_VENMO = 'save_paypal_venmo';
/**
* Advanced card processing eligibility. Required for credit- and debit-card processing.
*/
public const FEATURE_ADVANCED_CARD_PROCESSING = 'acdc';
/**
* Merchant eligibility to use Google Pay.
*/
public const FEATURE_GOOGLE_PAY = 'googlepay';
/**
* Whether Apple Pay can be used by the merchant. Apple Pay requires an Apple device (like
* iPhone) to be used by customers.
*/
public const FEATURE_APPLE_PAY = 'applepay';
/**
* Contact module allows the merchant to unlock the "Custom Shipping Contact" toggle.
*/
public const FEATURE_CONTACT_MODULE = 'contact_module';
/**
* The merchant's country according to PayPal, which might be different from
* the WooCommerce country.
@ -30,15 +58,27 @@ class MerchantDetails {
*/
private string $store_country;
/**
* A collection of feature eligibility checks. The value can be either a
* boolean (static eligibility) or a callback that returns a boolean (lazy check).
*
* @var array
*/
private array $eligibility_checks;
/**
* Constructor.
*
* @param string $merchant_country Initial merchant country.
* @param string $store_country Initial store country.
* @param string $merchant_country Merchant country provided by PayPal's API. Not editable.
* @param string $store_country WooCommerce store country, can be changed by the site
* admin via the WooCommerce settings.
* @param array $eligibility_checks Array of eligibility checks. Default service:
* 'wcgateway.feature-eligibility.list'.
*/
public function __construct( string $merchant_country, string $store_country ) {
$this->merchant_country = $merchant_country;
$this->store_country = $store_country;
public function __construct( string $merchant_country, string $store_country, array $eligibility_checks ) {
$this->merchant_country = $merchant_country;
$this->store_country = $store_country;
$this->eligibility_checks = $eligibility_checks;
}
/**
@ -63,4 +103,33 @@ class MerchantDetails {
public function get_shop_country() : string {
return $this->store_country;
}
/**
* Tests, if the merchant is eligible to use a certain feature.
* Feature checks are reliable _after_ the "plugins_loaded" action finished.
*
* Note:
* To register features for detection by this method, the features must be
* present in the service `wcgateway.contact-module.eligibility.check`, and
* also define a public FEATURE_* const in the class header.
* Adding all features is an ongoing task.
*
* @param string $feature One of the public self::FEATURE_* values.
* @return bool Whether the merchant can use the relevant feature.
*/
public function is_eligible_for( string $feature ) : bool {
if ( ! array_key_exists( $feature, $this->eligibility_checks ) ) {
return false;
}
$check = $this->eligibility_checks[ $feature ];
if ( is_bool( $check ) ) {
return $check;
}
if ( is_callable( $check ) ) {
return (bool) $check();
}
return false;
}
}

View file

@ -12,6 +12,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ContactPreferenceFactory;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\TestCase;
@ -148,6 +149,7 @@ class CreateOrderEndpointTest extends TestCase
{
$request_data = Mockery::mock(RequestData::class);
$shippingPreferenceFactory = Mockery::mock(ShippingPreferenceFactory::class);
$contactPreferenceFactory = Mockery::mock(ContactPreferenceFactory::class);
$experienceContextBuilder = Mockery::mock(ExperienceContextBuilder::class);
$purchase_unit_factory = Mockery::mock(PurchaseUnitFactory::class);
$order_endpoint = Mockery::mock(OrderEndpoint::class);
@ -161,6 +163,7 @@ class CreateOrderEndpointTest extends TestCase
$request_data,
$purchase_unit_factory,
$shippingPreferenceFactory,
$contactPreferenceFactory,
$experienceContextBuilder,
$order_endpoint,
$payer_factory,