Merge branch 'trunk' into PCP-3928-component-cleanup-icons-paths-links-code-style

# Conflicts:
#	modules/ppcp-settings/resources/js/Components/Screens/Onboarding/Steps/StepProducts.js
This commit is contained in:
carmenmaymo 2025-07-30 11:03:11 +02:00
commit f4888c08e4
No known key found for this signature in database
GPG key ID: 6023F686B0F3102E
242 changed files with 11283 additions and 1781 deletions

View file

@ -17,3 +17,7 @@ indent_style = space
[*.yml]
indent_size = 2
[*.php]
ij_php_variable_naming_style = snake_case
ij_php_getters_setters_naming_style = snake_case

View file

@ -12,7 +12,7 @@ jobs:
name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }}
steps:
- uses: ddev/github-action-setup-ddev@v1
- uses: ddev/github-action-setup-ddev@1c7ef18595da42355373cb6d9417a6f44d758b93 # v1.10.1
with:
autostart: false

View file

@ -27,7 +27,7 @@ jobs:
create_archive:
needs: check_version
uses: inpsyde/reusable-workflows/.github/workflows/build-plugin-archive.yml@main
uses: inpsyde/reusable-workflows/.github/workflows/build-plugin-archive.yml@a9af34f34e95cbe18703198c7e972e97ebcd7473
with:
PHP_VERSION: 7.4
NODE_VERSION: 22

View file

@ -25,7 +25,7 @@ jobs:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
with:
php-version: 7.4

View file

@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
with:
php-version: ${{ matrix.php-versions }}
@ -22,7 +22,7 @@ jobs:
run: composer validate
- name: Install dependencies
uses: ramsey/composer-install@v1
uses: ramsey/composer-install@a7320a0581dcd0432930c48a0e7ced67e6ec17e8 # v1.3.0
with:
composer-options: "--prefer-dist"

View file

@ -14,7 +14,7 @@ jobs:
- name: Check spelling
id: spelling
uses: crate-ci/typos@v1.30.2
uses: crate-ci/typos@7bc041cbb7ca9167c9e0e4ccbb26f48eb0f9d4e0 # v1.30.2
with:
# Path to config file
config: .github/workflows-config/typos.toml

View file

@ -1,5 +1,36 @@
*** Changelog ***
= 3.0.8 - 2025-07-28 =
* Enhancement - Migration from Legacy Settings to New Settings as opt-in via banner & button #3491
* Enhancement - Replace call to `billing-agreements/agreement-tokens` with checking the capabilities for Reference Transactions #3495
* Enhancement - Add Fastlane 3D Secure support #3493
* Enhancement - Improved PHP 8.4 compatibility #3534
* Fix - `INVALID_REQUEST` error due to wrong `landing_page` value after upgrade to 3.0.7 #3521
* Fix - Incorrect Amount via Express Payment for WooCommerce Product Bundles #3516
* Fix - Onboarding failed via "Connect to PayPal" in new UI due to race condition #3385
* Fix - Fatal error when PayPal Payments is active without WooCommerce #3502
* Fix - PayPal Subscription transaction failed in various scenarios #3515
* Fix - Rounding differences potentially lead to order failure (author @luzat) #3373
* Fix - Google Pay payment on block checkout may fail when ACDC is default payment selection #3506
* Fix - Product Prices Disappear in some cases when WooCommerce Subscriptions is active #3519
= 3.0.7 - 2025-07-01 =
* Enhancement - Remove `application_context` in favor of `experience_context` object #3431
**NOTE**: If you were modifying the `application_context` object programmatically, you may need to update your code to utilize `experience_context` for your customizations.
* Enhancement - Add Contact Module feature
* Enhancement - Add WooCommerce Tracks integration
* Enhancement - Onboarding notification for Firefox browser #3433
* Enhancement - Reset BN code on plugin uninstall #3471
* Enhancement - Add "Stay updated with PayPal" option in the old and new settings UI #3430
* Enhancement - Add French Territories to the supported ACDC countries list #3438
* Enhancement - Auto-enable logging during onboarding #3369
* Fix - DUPLICATE_INVOICE_ID in Sandbox due to missing invoice prefix #3435
* Fix - Subscription product could not be unlinked from PayPal Subscription #3429
* Fix - PayPal button greyed out on single product page for variable products with >2 attributes #3395
* Fix - APMs automatically enabled despite selecting "No, ..." during onboarding #3362
* Fix - Ditch items logic does not work when using saved card payment #3476
* Fix - billing-agreements endpoint called too frequently when not enabled for Reference Transactions #3459
= 3.0.6 - 2025-05-27 =
* Enhancement - Implement 3D secure check for Google Pay #3163
* Enhancement - Add options for "Disable Credit Cards" and "Language" #3226

510
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -33,6 +33,7 @@ return function ( string $root_dir ): iterable {
( require "$modules_dir/ppcp-blocks/module.php" )(),
( require "$modules_dir/ppcp-paypal-subscriptions/module.php" )(),
( require "$modules_dir/ppcp-local-alternative-payment-methods/module.php" )(),
( require "$modules_dir/ppcp-settings/module.php" )(),
);
// phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
@ -91,15 +92,5 @@ 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',
$show_new_ux || $preview_new_ux
) ) {
$modules[] = ( require "$modules_dir/ppcp-settings/module.php" )();
}
return $modules;
};

View file

@ -80,7 +80,7 @@ class Renderer implements RendererInterface {
printf(
'<div class="notice notice-%s %s" %s%s><p>%s</p></div>',
$message->type(),
esc_attr( $message->type() ),
( $message->is_dismissible() ) ? 'is-dismissible' : '',
( $message->wrapper() ? sprintf( 'data-ppcp-wrapper="%s"', esc_attr( $message->wrapper() ) ) : '' ),
// Use `empty()` in condition, to avoid false phpcs warning.

View file

@ -16,7 +16,8 @@ return array(
'wcgateway.builder.experience-context' => static function ( ContainerInterface $container ): ExperienceContextBuilder {
return new ExperienceContextBuilder(
$container->get( 'wcgateway.settings' )
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.shipping.callback.factory.url' )
);
},
);

View file

@ -9,46 +9,33 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ClientCredentials;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans;
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory;
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;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\IdentityToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AddressFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ContactPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExchangeRateFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\FraudProcessorResponseFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ItemFactory;
@ -57,31 +44,46 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayeeFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentsFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenActionLinksFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlatformFeeFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerReceivableBreakdownFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ReturnUrlFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
use WooCommerce\PayPalCommerce\Settings\Enum\InstallationPathEnum;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'api.host' => static function( ContainerInterface $container ) : string {
@ -273,13 +275,10 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): BillingAgreementsEndpoint {
return new BillingAgreementsEndpoint(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.reference-transaction-status' => static fn ( ContainerInterface $container ): ReferenceTransactionStatus => new ReferenceTransactionStatus(
$container->get( 'api.endpoint.partners' ),
$container->get( 'api.reference-transaction-status-cache' )
),
'api.endpoint.catalog-products' => static function ( ContainerInterface $container ): CatalogProducts {
return new CatalogProducts(
$container->get( 'api.host' ),
@ -330,6 +329,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();
},
@ -396,6 +411,9 @@ return array(
$container->get( 'api.factory.shipping-option' )
);
},
'api.factory.return-url' => static function ( ContainerInterface $container ): ReturnUrlFactory {
return new ReturnUrlFactory();
},
'api.factory.shipping-preference' => static function ( ContainerInterface $container ): ShippingPreferenceFactory {
return new ShippingPreferenceFactory();
},
@ -682,6 +700,11 @@ return array(
'GB' => $default_currencies,
'US' => $default_currencies,
'NO' => $default_currencies,
'YT' => $default_currencies,
'RE' => $default_currencies,
'GP' => $default_currencies,
'GF' => $default_currencies,
'MQ' => $default_currencies,
)
);
},
@ -761,6 +784,11 @@ return array(
'amex' => array( 'JPY' ),
'jcb' => array( 'JPY' ),
),
'YT' => $mastercard_visa_amex, // Mayotte.
'RE' => $mastercard_visa_amex, // Reunion.
'GP' => $mastercard_visa_amex, // Guadelope.
'GF' => $mastercard_visa_amex, // French Guiana.
'MQ' => $mastercard_visa_amex, // Martinique.
)
);
},
@ -848,6 +876,9 @@ return array(
'api.user-id-token-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-id-token-cache' );
},
'api.reference-transaction-status-cache' => static function( ContainerInterface $container ): Cache {
return new Cache( 'ppcp-reference-transaction-status-cache' );
},
'api.user-id-token' => static function( ContainerInterface $container ): UserIdToken {
return new UserIdToken(
$container->get( 'api.host' ),

View file

@ -1,147 +0,0 @@
<?php
/**
* The billing agreements endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Exception;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use Psr\Log\LoggerInterface;
/**
* Class BillingAgreementsEndpoint
*/
class BillingAgreementsEndpoint {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* BillingAgreementsEndpoint constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->logger = $logger;
}
/**
* Creates a billing agreement token.
*
* @param string $description The description.
* @param string $return_url The return URL.
* @param string $cancel_url The cancel URL.
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function create_token( string $description, string $return_url, string $cancel_url ): stdClass {
$data = array(
'description' => $description,
'payer' => array(
'payment_method' => 'PAYPAL',
),
'plan' => array(
'type' => 'MERCHANT_INITIATED_BILLING',
'merchant_preferences' => array(
'return_url' => $return_url,
'cancel_url' => $cancel_url,
'skip_shipping_address' => true,
),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing-agreements/agreement-tokens';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to create a billing agreement token.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 201 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $json;
}
/**
* Checks if reference transactions are enabled in account.
*
* @throws RuntimeException If the request fails (no auth, no connection, etc.).
*/
public function reference_transaction_enabled(): bool {
try {
if ( wc_string_to_bool( get_transient( 'ppcp_reference_transaction_enabled' ) ) === true ) {
return true;
}
$this->is_request_logging_enabled = false;
try {
$this->create_token(
'Checking if reference transactions are enabled',
'https://example.com/return',
'https://example.com/cancel'
);
} finally {
$this->is_request_logging_enabled = true;
set_transient( 'ppcp_reference_transaction_enabled', true, MONTH_IN_SECONDS );
}
return true;
} catch ( Exception $exception ) {
delete_transient( 'ppcp_reference_transaction_enabled' );
return false;
}
}
}

View file

@ -176,10 +176,10 @@ class OrderEndpoint {
public function create(
array $items,
string $shipping_preference,
Payer $payer = null,
?Payer $payer = null,
string $payment_method = '',
array $request_data = array(),
PaymentSource $payment_source = null
?PaymentSource $payment_source = null
): Order {
$bearer = $this->bearer->bearer();
$data = array(
@ -444,9 +444,7 @@ class OrderEndpoint {
}
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
__( 'Could not retrieve order.', 'woocommerce-paypal-payments' )
);
$error = new RuntimeException( 'Could not retrieve order.' );
$this->logger->warning( $error->getMessage() );
throw $error;
@ -456,7 +454,7 @@ class OrderEndpoint {
if ( 404 === $status_code || empty( $response['body'] ) ) {
$error = new RuntimeException(
__( 'Could not retrieve order.', 'woocommerce-paypal-payments' ),
'Could not retrieve order.',
404
);
$this->logger->warning(
@ -585,9 +583,6 @@ class OrderEndpoint {
$data = array(
'payment_source' => $payment_source,
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
'application_context' => array(
'locale' => 'es-MX',
),
);
$args = array(

View file

@ -113,9 +113,7 @@ class WebhookEndpoint {
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
throw new RuntimeException(
__( 'Not able to create a webhook.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'Not able to create a webhook.' );
}
$json = json_decode( $response['body'] );
@ -151,9 +149,7 @@ class WebhookEndpoint {
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
throw new RuntimeException(
__( 'Not able to load webhooks list.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'Not able to load webhooks list.' );
}
$json = json_decode( $response['body'] );
@ -195,9 +191,7 @@ class WebhookEndpoint {
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException(
__( 'Not able to delete the webhook.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'Not able to delete the webhook.' );
}
$status_code = (int) wp_remote_retrieve_response_code( $response );
@ -250,9 +244,7 @@ class WebhookEndpoint {
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
throw new RuntimeException(
__( 'Not able to simulate webhook.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'Not able to simulate webhook.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
@ -312,9 +304,7 @@ class WebhookEndpoint {
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) {
$error = new RuntimeException(
__( 'Not able to verify webhook event.', 'woocommerce-paypal-payments' )
);
$error = new RuntimeException( 'Not able to verify webhook event.' );
$this->logger->log(
'warning',
$error->getMessage(),
@ -340,9 +330,7 @@ class WebhookEndpoint {
public function verify_current_request_for_webhook( Webhook $webhook ): bool {
if ( ! $webhook->id() ) {
$error = new RuntimeException(
__( 'Not a valid webhook to verify.', 'woocommerce-paypal-payments' )
);
$error = new RuntimeException( 'Not a valid webhook to verify.' );
$this->logger->log( 'warning', $error->getMessage(), array( 'webhook' => $webhook ) );
throw $error;
}
@ -369,11 +357,7 @@ class WebhookEndpoint {
$error = new RuntimeException(
sprintf(
// translators: %s is the headers key.
__(
'Not a valid webhook event. Header %s is missing',
'woocommerce-paypal-payments'
),
$key
)
);

View file

@ -41,7 +41,7 @@ class Amount {
* @param Money $money The money.
* @param AmountBreakdown|null $breakdown The breakdown.
*/
public function __construct( Money $money, AmountBreakdown $breakdown = null ) {
public function __construct( Money $money, ?AmountBreakdown $breakdown = null ) {
$this->money = $money;
$this->breakdown = $breakdown;
}

View file

@ -62,8 +62,7 @@ class AuthorizationStatus {
if ( ! in_array( $status, self::VALID_STATUS, true ) ) {
throw new RuntimeException(
sprintf(
// translators: %s is the current status.
__( '%s is not a valid status', 'woocommerce-paypal-payments' ),
'%s is not a valid status',
$status
)
);

View file

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* The config for experience_context.order_update_callback_config.
*/
class CallbackConfig {
public const EVENT_SHIPPING_ADDRESS = 'SHIPPING_ADDRESS';
public const EVENT_SHIPPING_OPTIONS = 'SHIPPING_OPTIONS';
/**
* The events.
*
* @var string[]
*/
private array $events;
/**
* The URL that will be called when the events occur.
*/
private string $url;
/**
* @param string[] $events The events.
* @param string $url The URL that will be called when the events occur.
*/
public function __construct( array $events, string $url ) {
$this->events = $events;
$this->url = $url;
}
/**
* Returns the object as array.
*/
public function to_array(): array {
return array(
'callback_events' => $this->events,
'callback_url' => $this->url,
);
}
}

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,17 @@ 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.
*/
private ?CallbackConfig $order_update_callback_config = null;
/**
* Returns the return URL.
*/
@ -161,6 +175,10 @@ class ExperienceContext {
* @param string|null $new_value The value to set.
*/
public function with_landing_page( ?string $new_value ): ExperienceContext {
if ( $new_value && strtoupper( $new_value ) === 'BILLING' ) {
$new_value = self::LANDING_PAGE_GUEST_CHECKOUT;
}
$obj = clone $this;
$obj->landing_page = $new_value;
@ -224,6 +242,47 @@ 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.
*/
public function order_update_callback_config(): ?CallbackConfig {
return $this->order_update_callback_config;
}
/**
* Sets the callback config.
*
* @param CallbackConfig|null $new_value The value to set.
*/
public function with_order_update_callback_config( ?CallbackConfig $new_value ): ExperienceContext {
$obj = clone $this;
$obj->order_update_callback_config = $new_value;
return $obj;
}
/**
* Returns the object as array.
*/
@ -236,6 +295,9 @@ class ExperienceContext {
if ( $value === null ) {
continue;
}
if ( is_object( $value ) && method_exists( $value, 'to_array' ) ) {
$value = $value->to_array();
}
$data[ $prop->getName() ] = $value;
}

View file

@ -114,13 +114,13 @@ class Item {
Money $unit_amount,
int $quantity,
string $description = '',
Money $tax = null,
?Money $tax = null,
string $sku = '',
string $category = 'PHYSICAL_GOODS',
string $url = '',
string $image_url = '',
float $tax_rate = 0,
string $cart_item_key = null
?string $cart_item_key = null
) {
$this->name = $name;

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use DateTime;
/**
* Class Order
*/
@ -25,7 +27,7 @@ class Order {
/**
* The create time.
*
* @var \DateTime|null
* @var DateTime|null
*/
private $create_time;
@ -60,7 +62,7 @@ class Order {
/**
* The update time.
*
* @var \DateTime|null
* @var DateTime|null
*/
private $update_time;
@ -70,6 +72,10 @@ class Order {
* @var PaymentSource|null
*/
private $payment_source;
/**
* @var mixed|null
*/
private $links;
/**
* Order constructor.
@ -82,18 +88,19 @@ class Order {
* @param PaymentSource|null $payment_source The payment source.
* @param Payer|null $payer The payer.
* @param string $intent The intent.
* @param \DateTime|null $create_time The create time.
* @param \DateTime|null $update_time The update time.
* @param DateTime|null $create_time The create time.
* @param DateTime|null $update_time The update time.
*/
public function __construct(
string $id,
array $purchase_units,
OrderStatus $order_status,
PaymentSource $payment_source = null,
Payer $payer = null,
?PaymentSource $payment_source = null,
?Payer $payer = null,
string $intent = 'CAPTURE',
\DateTime $create_time = null,
\DateTime $update_time = null
?DateTime $create_time = null,
?DateTime $update_time = null,
$links = null
) {
$this->id = $id;
@ -104,6 +111,7 @@ class Order {
$this->create_time = $create_time;
$this->update_time = $update_time;
$this->payment_source = $payment_source;
$this->links = $links;
}
/**
@ -118,7 +126,7 @@ class Order {
/**
* Returns the create time.
*
* @return \DateTime|null
* @return DateTime|null
*/
public function create_time() {
return $this->create_time;
@ -127,7 +135,7 @@ class Order {
/**
* Returns the update time.
*
* @return \DateTime|null
* @return DateTime|null
*/
public function update_time() {
return $this->update_time;
@ -179,6 +187,15 @@ class Order {
return $this->payment_source;
}
/**
* Returns the links.
*
* @return mixed|null
*/
public function links() {
return $this->links;
}
/**
* Returns the object as array.
*
@ -206,6 +223,10 @@ class Order {
$order['update_time'] = $this->update_time()->format( 'Y-m-d\TH:i:sO' );
}
if ( $this->links ) {
$order['links'] = $this->links();
}
return $order;
}
}

View file

@ -51,8 +51,7 @@ class OrderStatus {
if ( ! in_array( $status, self::VALID_STATUS, true ) ) {
throw new RuntimeException(
sprintf(
// translators: %s is the current status.
__( '%s is not a valid status', 'woocommerce-paypal-payments' ),
'%s is not a valid status',
$status
)
);

View file

@ -9,6 +9,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
use DateTime;
/**
* Class Payer
* The customer who sends the money.
@ -39,7 +41,7 @@ class Payer {
/**
* The birth date.
*
* @var \DateTime|null
* @var DateTime|null
*/
private $birthdate;
@ -71,7 +73,7 @@ class Payer {
* @param string $email_address The email.
* @param string $payer_id The payer id.
* @param Address|null $address The address.
* @param \DateTime|null $birthdate The birth date.
* @param DateTime|null $birthdate The birth date.
* @param PhoneWithType|null $phone The phone.
* @param PayerTaxInfo|null $tax_info The tax info.
*/
@ -79,10 +81,10 @@ class Payer {
?PayerName $name,
string $email_address,
string $payer_id,
Address $address = null,
\DateTime $birthdate = null,
PhoneWithType $phone = null,
PayerTaxInfo $tax_info = null
?Address $address = null,
?DateTime $birthdate = null,
?PhoneWithType $phone = null,
?PayerTaxInfo $tax_info = null
) {
$this->name = $name;
@ -133,7 +135,7 @@ class Payer {
/**
* Returns the birth date.
*
* @return \DateTime|null
* @return DateTime|null
*/
public function birthdate() {
return $this->birthdate;

View file

@ -51,8 +51,7 @@ class PayerTaxInfo {
if ( ! in_array( $type, self::VALID_TYPES, true ) ) {
throw new RuntimeException(
sprintf(
// translators: %s is the current type.
__( '%s is not a valid tax type.', 'woocommerce-paypal-payments' ),
'%s is not a valid tax type.',
$type
)
);

View file

@ -50,9 +50,7 @@ class PaymentToken {
*/
public function __construct( string $id, stdClass $source, string $type = self::TYPE_PAYMENT_METHOD_TOKEN ) {
if ( ! in_array( $type, self::get_valid_types(), true ) ) {
throw new RuntimeException(
__( 'Not a valid payment source type.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'Not a valid payment source type.' );
}
$this->id = $id;
$this->type = $type;

View file

@ -109,13 +109,13 @@ class PurchaseUnit {
public function __construct(
Amount $amount,
array $items = array(),
Shipping $shipping = null,
?Shipping $shipping = null,
string $reference_id = 'default',
string $description = '',
string $custom_id = '',
string $invoice_id = '',
string $soft_descriptor = '',
Payments $payments = null
?Payments $payments = null
) {
$this->amount = $amount;

View file

@ -54,7 +54,7 @@ class RefundCapture {
Capture $capture,
string $invoice_id,
string $note_to_payer = '',
Amount $amount = null
?Amount $amount = null
) {
$this->capture = $capture;
$this->invoice_id = $invoice_id;

View file

@ -28,6 +28,16 @@ class Shipping {
*/
private $address;
/**
* Custom contact email address, usually added via the Contact Module.
*/
private ?string $email_address = null;
/**
* Custom contact phone number, usually added via the Contact Module.
*/
private ?Phone $phone_number = null;
/**
* Shipping methods.
*
@ -40,11 +50,21 @@ class Shipping {
*
* @param string $name The name.
* @param Address $address The address.
* @param string|null $email_address Contact email.
* @param Phone|null $phone_number Contact phone.
* @param ShippingOption[] $options Shipping methods.
*/
public function __construct( string $name, Address $address, array $options = array() ) {
public function __construct(
string $name,
Address $address,
?string $email_address = null,
?Phone $phone_number = null,
array $options = array()
) {
$this->name = $name;
$this->address = $address;
$this->email_address = $email_address;
$this->phone_number = $phone_number;
$this->options = $options;
}
@ -66,6 +86,24 @@ class Shipping {
return $this->address;
}
/**
* Returns the contact email address, or null.
*
* @return null|string
*/
public function email_address() : ?string {
return $this->email_address;
}
/**
* Returns the contact phone number, or null.
*
* @return null|Phone
*/
public function phone_number() : ?Phone {
return $this->phone_number;
}
/**
* Returns the shipping methods.
*
@ -87,6 +125,17 @@ class Shipping {
),
'address' => $this->address()->to_array(),
);
$contact_email = $this->email_address();
$contact_phone = $this->phone_number();
if ( $contact_email ) {
$result['email_address'] = $contact_email;
}
if ( $contact_phone ) {
$result['phone_number'] = $contact_phone->to_array();
}
if ( $this->options ) {
$result['options'] = array_map(
function ( ShippingOption $opt ): array {

View file

@ -36,9 +36,9 @@ class PayPalApiException extends RuntimeException {
* @param stdClass|null $response The JSON object.
* @param int $status_code The HTTP status code.
*/
public function __construct( stdClass $response = null, int $status_code = 0 ) {
public function __construct( ?stdClass $response = null, int $status_code = 0 ) {
if ( is_null( $response ) ) {
$response = new \stdClass();
$response = new stdClass();
}
if ( ! isset( $response->message ) ) {
$response->message = sprintf(
@ -63,7 +63,7 @@ class PayPalApiException extends RuntimeException {
/**
* The JSON response object.
*
* @var \stdClass $response
* @var stdClass $response
*/
$this->response = $response;
$this->status_code = $status_code;

View file

@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Item;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
use WooCommerce\PayPalCommerce\WcGateway\StoreApi\Entity\CartTotals;
use WooCommerce\PayPalCommerce\WcSubscriptions\FreeTrialHandlerTrait;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
@ -111,6 +112,24 @@ class AmountFactory {
return $amount;
}
/**
* Returns an Amount object based off a WooCommerce cart object from the Store API.
*/
public function from_store_api_cart( CartTotals $cart_totals ): Amount {
return new Amount(
$cart_totals->total_price()->to_paypal(),
new AmountBreakdown(
$cart_totals->total_items()->to_paypal(),
$cart_totals->total_shipping()->to_paypal(),
$cart_totals->total_tax()->to_paypal(),
null,
null,
null,
$cart_totals->total_discount()->to_paypal(),
)
);
}
/**
* Returns an Amount object based off a WooCommerce order.
*
@ -179,12 +198,15 @@ class AmountFactory {
/**
* Returns an Amount object based off a PayPal Response.
*
* @param \stdClass $data The JSON object.
* @param mixed $data The JSON object.
*
* @return Amount
* @throws RuntimeException When JSON object is malformed.
* @return Amount|null
*/
public function from_paypal_response( \stdClass $data ): Amount {
public function from_paypal_response( $data ) {
if ( null === $data || ! $data instanceof \stdClass ) {
return null;
}
$money = $this->money_factory->from_paypal_response( $data );
$breakdown = ( isset( $data->breakdown ) ) ? $this->break_down( $data->breakdown ) : null;
return new Amount( $money, $breakdown );
@ -223,8 +245,7 @@ class AmountFactory {
if ( ! isset( $item->value ) || ! is_numeric( $item->value ) ) {
throw new RuntimeException(
sprintf(
// translators: %s is the current breakdown key.
__( 'No value given for breakdown %s', 'woocommerce-paypal-payments' ),
'No value given for breakdown %s',
$key
)
);
@ -232,8 +253,7 @@ class AmountFactory {
if ( ! isset( $item->currency_code ) ) {
throw new RuntimeException(
sprintf(
// translators: %s is the current breakdown key.
__( 'No currency given for breakdown %s', 'woocommerce-paypal-payments' ),
'No currency given for breakdown %s',
$key
)
);

View file

@ -45,15 +45,11 @@ class AuthorizationFactory {
*/
public function from_paypal_response( \stdClass $data ): Authorization {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'Does not contain an id.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'Does not contain an id.' );
}
if ( ! isset( $data->status ) ) {
throw new RuntimeException(
__( 'Does not contain status.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'Does not contain status.' );
}
$reason = $data->status_details->reason ?? null;

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatusDetails;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class CaptureFactory
@ -63,6 +64,7 @@ class CaptureFactory {
* @param \stdClass $data The PayPal response.
*
* @return Capture
* @throws RuntimeException When capture amount data is invalid.
*/
public function from_paypal_response( \stdClass $data ) : Capture {
$reason = $data->status_details->reason ?? null;
@ -74,13 +76,18 @@ class CaptureFactory {
$this->fraud_processor_response_factory->from_paypal_response( $data->processor_response )
: null;
$amount = $this->amount_factory->from_paypal_response( $data->amount );
if ( null === $amount ) {
throw new RuntimeException( 'Invalid capture amount data.' );
}
return new Capture(
(string) $data->id,
new CaptureStatus(
(string) $data->status,
$reason ? new CaptureStatusDetails( $reason ) : null
),
$this->amount_factory->from_paypal_response( $data->amount ),
$amount,
(bool) $data->final_capture,
(string) $data->seller_protection->status,
(string) $data->invoice_id,

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

@ -11,9 +11,11 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WC_AJAX;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CallbackConfig;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ExperienceContext;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Shipping\ShippingCallbackUrlFactory;
/**
* Class ExperienceContextBuilder
@ -30,15 +32,16 @@ class ExperienceContextBuilder {
*/
private ContainerInterface $settings;
/**
* ExperienceContextBuilder constructor.
*
* @param ContainerInterface $settings The settings.
*/
public function __construct( ContainerInterface $settings ) {
private ShippingCallbackUrlFactory $shipping_callback_url_factory;
public function __construct(
ContainerInterface $settings,
ShippingCallbackUrlFactory $shipping_callback_url_factory
) {
$this->experience_context = new ExperienceContext();
$this->settings = $settings;
$this->shipping_callback_url_factory = $shipping_callback_url_factory;
}
/**
@ -97,6 +100,34 @@ class ExperienceContextBuilder {
return $builder;
}
/**
* Uses a custom return URL.
*
* @param string $url The return URL.
*/
public function with_custom_return_url( string $url ): ExperienceContextBuilder {
$builder = clone $this;
$builder->experience_context = $builder->experience_context
->with_return_url( $url );
return $builder;
}
/**
* Uses a custom cancel URL.
*
* @param string $url The cancel URL.
*/
public function with_custom_cancel_url( string $url ): ExperienceContextBuilder {
$builder = clone $this;
$builder->experience_context = $builder->experience_context
->with_cancel_url( $url );
return $builder;
}
/**
* Uses the current brand name from the settings.
*/
@ -161,6 +192,37 @@ class ExperienceContextBuilder {
return $builder;
}
/**
* Uses the server-side shipping callback configuration.
*/
public function with_shipping_callback(): ExperienceContextBuilder {
$builder = clone $this;
$builder->experience_context = $builder->experience_context
->with_order_update_callback_config(
new CallbackConfig(
array( CallbackConfig::EVENT_SHIPPING_ADDRESS, CallbackConfig::EVENT_SHIPPING_OPTIONS ),
$this->shipping_callback_url_factory->create()
)
);
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

@ -179,19 +179,13 @@ class ItemFactory {
*/
public function from_paypal_response( \stdClass $data ): Item {
if ( ! isset( $data->name ) ) {
throw new RuntimeException(
__( 'No name for item given', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No name for item given' );
}
if ( ! isset( $data->quantity ) || ! is_numeric( $data->quantity ) ) {
throw new RuntimeException(
__( 'No quantity for item given', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No quantity for item given' );
}
if ( ! isset( $data->unit_amount->value ) || ! isset( $data->unit_amount->currency_code ) ) {
throw new RuntimeException(
__( 'No money values for item given', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No money values for item given' );
}
$unit_amount = new Money( (float) $data->unit_amount->value, $data->unit_amount->currency_code );

View file

@ -68,7 +68,8 @@ class OrderFactory {
$order->payer(),
$order->intent(),
$order->create_time(),
$order->update_time()
$order->update_time(),
$order->links()
);
}
@ -81,70 +82,153 @@ class OrderFactory {
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( \stdClass $order_data ): Order {
if ( ! isset( $order_data->id ) ) {
throw new RuntimeException(
__( 'Order does not contain an id.', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $order_data->purchase_units ) || ! is_array( $order_data->purchase_units ) ) {
throw new RuntimeException(
__( 'Order does not contain items.', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $order_data->status ) ) {
throw new RuntimeException(
__( 'Order does not contain status.', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $order_data->intent ) ) {
throw new RuntimeException(
__( 'Order does not contain intent.', 'woocommerce-paypal-payments' )
);
}
$this->validate_order_id( $order_data );
$purchase_units = array_map(
function ( \stdClass $data ): PurchaseUnit {
return $this->purchase_unit_factory->from_paypal_response( $data );
},
$order_data->purchase_units
);
$create_time = ( isset( $order_data->create_time ) ) ?
\DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->create_time )
: null;
$update_time = ( isset( $order_data->update_time ) ) ?
\DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->update_time )
: null;
$payer = ( isset( $order_data->payer ) ) ?
$this->payer_factory->from_paypal_response( $order_data->payer )
: null;
$payment_source = null;
if ( isset( $order_data->payment_source ) ) {
$json_encoded_payment_source = wp_json_encode( $order_data->payment_source );
if ( $json_encoded_payment_source ) {
$payment_source_as_array = json_decode( $json_encoded_payment_source, true );
if ( $payment_source_as_array ) {
$name = array_key_first( $payment_source_as_array );
if ( $name ) {
$payment_source = new PaymentSource(
$name,
$order_data->payment_source->$name
);
}
}
}
}
$purchase_units = $this->create_purchase_units( $order_data );
$status = $this->create_order_status( $order_data );
$intent = $this->get_intent( $order_data );
$timestamps = $this->create_timestamps( $order_data );
$payer = $this->create_payer( $order_data );
$payment_source = $this->create_payment_source( $order_data );
$links = $order_data->links ?? null;
return new Order(
$order_data->id,
$purchase_units,
new OrderStatus( $order_data->status ),
$status,
$payment_source,
$payer,
$order_data->intent,
$create_time,
$update_time
$intent,
$timestamps['create_time'],
$timestamps['update_time'],
$links
);
}
/**
* Validates that the order data contains a required ID.
*
* @param \stdClass $order_data The order data.
*
* @throws RuntimeException When ID is missing.
*/
private function validate_order_id( \stdClass $order_data ): void {
if ( ! isset( $order_data->id ) ) {
throw new RuntimeException( 'Order does not contain an id.' );
}
}
/**
* Creates purchase units from order data.
*
* @param \stdClass $order_data The order data.
*
* @return array Array of PurchaseUnit objects.
*/
private function create_purchase_units( \stdClass $order_data ): array {
if ( ! isset( $order_data->purchase_units ) || ! is_array( $order_data->purchase_units ) ) {
return array();
}
$purchase_units = array();
foreach ( $order_data->purchase_units as $data ) {
$purchase_unit = $this->purchase_unit_factory->from_paypal_response( $data );
if ( null !== $purchase_unit ) {
$purchase_units[] = $purchase_unit;
}
}
return $purchase_units;
}
/**
* Creates order status from order data.
*
* @param \stdClass $order_data The order data.
*
* @return OrderStatus
*/
private function create_order_status( \stdClass $order_data ): OrderStatus {
$status_value = $order_data->status ?? 'PAYER_ACTION_REQUIRED';
return new OrderStatus( $status_value );
}
/**
* Gets the intent from order data.
*
* @param \stdClass $order_data The order data.
*
* @return string
*/
private function get_intent( \stdClass $order_data ): string {
return $order_data->intent ?? 'CAPTURE';
}
/**
* Creates timestamps from order data.
*
* @param \stdClass $order_data The order data.
*
* @return array Array with 'create_time' and 'update_time' keys.
*/
private function create_timestamps( \stdClass $order_data ): array {
$create_time = isset( $order_data->create_time ) ?
\DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->create_time ) :
null;
$update_time = isset( $order_data->update_time ) ?
\DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->update_time ) :
null;
return array(
'create_time' => $create_time,
'update_time' => $update_time,
);
}
/**
* Creates payer from order data.
*
* @param \stdClass $order_data The order data.
*
* @return mixed Payer object or null.
*/
private function create_payer( \stdClass $order_data ) {
return isset( $order_data->payer ) ?
$this->payer_factory->from_paypal_response( $order_data->payer ) :
null;
}
/**
* Creates payment source from order data.
*
* @param \stdClass $order_data The order data.
*
* @return PaymentSource|null
*/
private function create_payment_source( \stdClass $order_data ): ?PaymentSource {
if ( ! isset( $order_data->payment_source ) ) {
return null;
}
$json_encoded_payment_source = wp_json_encode( $order_data->payment_source );
if ( ! $json_encoded_payment_source ) {
return null;
}
$payment_source_as_array = json_decode( $json_encoded_payment_source, true );
if ( ! $payment_source_as_array ) {
return null;
}
$source_name = array_key_first( $payment_source_as_array );
if ( ! $source_name ) {
return null;
}
return new PaymentSource(
$source_name,
$order_data->payment_source->$source_name
);
}
}

View file

@ -27,9 +27,7 @@ class PaymentTokenFactory {
*/
public function from_paypal_response( $data ): PaymentToken {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'No id for payment token given', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No id for payment token given' );
}
return new PaymentToken(

View file

@ -57,24 +57,16 @@ class PlanFactory {
*/
public function from_paypal_response( stdClass $data ): Plan {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'No id for given plan', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No id for given plan' );
}
if ( ! isset( $data->name ) ) {
throw new RuntimeException(
__( 'No name for plan given', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No name for plan given' );
}
if ( ! isset( $data->product_id ) ) {
throw new RuntimeException(
__( 'No product id for given plan', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No product id for given plan' );
}
if ( ! isset( $data->billing_cycles ) ) {
throw new RuntimeException(
__( 'No billing cycles for given plan', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No billing cycles for given plan' );
}
$billing_cycles = array();

View file

@ -28,14 +28,10 @@ class ProductFactory {
*/
public function from_paypal_response( stdClass $data ): Product {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'No id for product given', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No id for product given' );
}
if ( ! isset( $data->name ) ) {
throw new RuntimeException(
__( 'No name for product given', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No name for product given' );
}
return new Product(

View file

@ -88,7 +88,7 @@ class PurchaseUnitFactory {
PaymentsFactory $payments_factory,
string $prefix = 'WC-',
string $soft_descriptor = '',
PurchaseUnitSanitizer $sanitizer = null
?PurchaseUnitSanitizer $sanitizer = null
) {
$this->amount_factory = $amount_factory;
@ -219,17 +219,20 @@ class PurchaseUnitFactory {
*
* @param \stdClass $data The JSON object.
*
* @return PurchaseUnit
* @return ?PurchaseUnit
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( \stdClass $data ): PurchaseUnit {
public function from_paypal_response( \stdClass $data ): ?PurchaseUnit {
if ( ! isset( $data->reference_id ) || ! is_string( $data->reference_id ) ) {
throw new RuntimeException(
__( 'No reference ID given.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No reference ID given.' );
}
$amount_data = $data->amount ?? null;
$amount = $this->amount_factory->from_paypal_response( $amount_data );
if ( null === $amount ) {
return null;
}
$amount = $this->amount_factory->from_paypal_response( $data->amount );
$description = ( isset( $data->description ) ) ? $data->description : '';
$custom_id = ( isset( $data->custom_id ) ) ? $data->custom_id : '';
$invoice_id = ( isset( $data->invoice_id ) ) ? $data->invoice_id : '';

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatusDetails;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class RefundFactory
@ -62,6 +63,7 @@ class RefundFactory {
* @param \stdClass $data The PayPal response.
*
* @return Refund
* @throws RuntimeException When refund amount data is invalid.
*/
public function from_paypal_response( \stdClass $data ) : Refund {
$reason = $data->status_details->reason ?? null;
@ -73,13 +75,18 @@ class RefundFactory {
$this->refund_payer_factory->from_paypal_response( $data->payer )
: null;
$amount = $this->amount_factory->from_paypal_response( $data->amount );
if ( null === $amount ) {
throw new RuntimeException( 'Invalid refund amount data.' );
}
return new Refund(
(string) $data->id,
new RefundStatus(
(string) $data->status,
$reason ? new RefundStatusDetails( $reason ) : null
),
$this->amount_factory->from_paypal_response( $data->amount ),
$amount,
(string) ( $data->invoice_id ?? '' ),
(string) ( $data->custom_id ?? '' ),
$seller_payable_breakdown,

View file

@ -0,0 +1,53 @@
<?php
/**
* Factory for determining the appropriate return URL based on context.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class ReturnUrlFactory
*/
class ReturnUrlFactory {
/**
* @throws RuntimeException When required data is missing for the context.
*/
public function from_context( string $context, array $request_data = array() ): string {
switch ( $context ) {
case 'cart':
case 'cart-block':
case 'mini-cart':
return wc_get_cart_url();
case 'product':
if ( ! empty( $request_data['purchase_units'] ) && is_array( $request_data['purchase_units'] ) ) {
$first_unit = reset( $request_data['purchase_units'] );
if ( ! empty( $first_unit['items'] ) && is_array( $first_unit['items'] ) ) {
$first_item = reset( $first_unit['items'] );
if ( ! empty( $first_item['url'] ) ) {
return $first_item['url'];
}
}
}
throw new RuntimeException( 'Product URL is required but not provided in the request data.' );
case 'pay-now':
if ( ! empty( $request_data['order_id'] ) ) {
$order = wc_get_order( $request_data['order_id'] );
if ( $order instanceof \WC_Order ) {
return $order->get_checkout_payment_url();
}
}
throw new RuntimeException( 'The order ID is invalid.' );
default:
return wc_get_checkout_url();
}
}
}

View file

@ -10,8 +10,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Shipping;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ShippingOption;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Phone;
/**
* Class ShippingFactory
@ -60,9 +60,12 @@ class ShippingFactory {
$customer->get_shipping_last_name()
);
$address = $this->address_factory->from_wc_customer( $customer );
return new Shipping(
$full_name,
$address,
null,
null,
$with_shipping_options ? $this->shipping_option_factory->from_wc_cart() : array()
);
}
@ -77,6 +80,7 @@ class ShippingFactory {
public function from_wc_order( \WC_Order $order ): Shipping {
$full_name = $order->get_formatted_shipping_full_name();
$address = $this->address_factory->from_wc_order( $order );
return new Shipping(
$full_name,
$address
@ -93,23 +97,32 @@ class ShippingFactory {
*/
public function from_paypal_response( \stdClass $data ): Shipping {
if ( ! isset( $data->name->full_name ) ) {
throw new RuntimeException(
__( 'No name was given for shipping.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No name was given for shipping.' );
}
if ( ! isset( $data->address ) ) {
throw new RuntimeException(
__( 'No address was given for shipping.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No address was given for shipping.' );
}
$contact_phone = null;
$contact_email = null;
$address = $this->address_factory->from_paypal_response( $data->address );
$options = array_map(
array( $this->shipping_option_factory, 'from_paypal_response' ),
$data->options ?? array()
);
if ( isset( $data->phone_number->national_number ) ) {
$contact_phone = new Phone( $data->phone_number->national_number );
}
if ( isset( $data->email_address ) ) {
$contact_email = $data->email_address;
}
return new Shipping(
$data->name->full_name,
$address,
$contact_email,
$contact_phone,
$options
);
}

View file

@ -40,14 +40,10 @@ class WebhookEventFactory {
*/
public function from_paypal_response( $data ): WebhookEvent {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'ID for webhook event not found.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'ID for webhook event not found.' );
}
if ( ! isset( $data->event_type ) ) {
throw new RuntimeException(
__( 'Event type for webhook event not found.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'Event type for webhook event not found.' );
}
$create_time = ( isset( $data->create_time ) ) ?

View file

@ -59,19 +59,13 @@ class WebhookFactory {
*/
public function from_paypal_response( $data ): Webhook {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'No id for webhook given.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No id for webhook given.' );
}
if ( ! isset( $data->url ) ) {
throw new RuntimeException(
__( 'No URL for webhook given.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No URL for webhook given.' );
}
if ( ! isset( $data->event_types ) ) {
throw new RuntimeException(
__( 'No event types for webhook given.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No event types for webhook given.' );
}
return new Webhook(

View file

@ -58,20 +58,22 @@ class PartnerAttribution {
}
/**
* Initializes the BN Code if not already set.
*
* This method ensures that the BN Code is only stored once during the initial setup.
* Initializes or updates the BN Code.
*
* @param string $installation_path The installation path used to determine the BN Code.
* @param bool $force_update Whether to force an update of the BN code if it already exists.
*/
public function initialize_bn_code( string $installation_path ) : void {
public function initialize_bn_code( string $installation_path, bool $force_update = false ) : void {
$selected_bn_code = $this->bn_codes[ $installation_path ] ?? '';
if ( ! $selected_bn_code || get_option( $this->bn_code_option_name ) ) {
if ( ! $selected_bn_code ) {
return;
}
$existing_bn_code = get_option( $this->bn_code_option_name );
if ( $existing_bn_code && ! $force_update ) {
return;
}
// This option is permanent and should not change.
update_option( $this->bn_code_option_name, $selected_bn_code );
}

View file

@ -114,7 +114,7 @@ abstract class ProductStatus {
* @param Settings|null $settings See description in {@see self::clear()}.
* @return void
*/
abstract protected function clear_state( Settings $settings = null ) : void;
abstract protected function clear_state( ?Settings $settings = null ) : void;
/**
* Whether the merchant has access to the feature.
@ -199,7 +199,7 @@ abstract class ProductStatus {
* @param Settings|null $settings The settings object.
* @return void
*/
public function clear( Settings $settings = null ) : void {
public function clear( ?Settings $settings = null ) : void {
$this->is_eligible = null;
$this->has_request_failure = false;

View file

@ -81,7 +81,7 @@ class PurchaseUnitSanitizer {
* @param string|null $mode The mismatch handling mode, ditch or extra_line.
* @param string|null $extra_line_name The name of the extra line.
*/
public function __construct( string $mode = null, string $extra_line_name = null ) {
public function __construct( ?string $mode = null, ?string $extra_line_name = null ) {
if ( ! in_array( $mode, self::VALID_MODES, true ) ) {
$mode = self::MODE_DITCH;
@ -193,10 +193,16 @@ class PurchaseUnitSanitizer {
$item_mismatch = $this->calculate_item_mismatch();
if ( $item_mismatch > 0 ) {
// Use appropriate category to preserve purely digital or physical goods baskets.
$rounding_item_category = $this->determine_rounding_item_category();
// Add extra line item with roundings.
$line_name = $this->extra_line_name;
$roundings_money = new Money( $item_mismatch, $this->currency_code() );
$this->purchase_unit['items'][] = ( new Item( $line_name, $roundings_money, 1 ) )->to_array();
$this->purchase_unit['items'][] = (
new Item( $line_name, $roundings_money, 1, '', null, '', $rounding_item_category )
)->to_array();
$this->set_last_message(
__( 'Item amount mismatch. Extra line added.', 'woocommerce-paypal-payments' )
@ -217,6 +223,24 @@ class PurchaseUnitSanitizer {
}
}
/**
* Determines the appropriate category for rounding items based on existing items.
*
* @return string The category (Item::DIGITAL_GOODS or Item::PHYSICAL_GOODS)
*/
private function determine_rounding_item_category(): string {
// Check if all items are digital goods.
foreach ( $this->items() as $item ) {
$category = $item['category'] ?? Item::PHYSICAL_GOODS;
if ( $category !== Item::DIGITAL_GOODS ) {
return Item::PHYSICAL_GOODS;
}
}
// All items are digital goods.
return Item::DIGITAL_GOODS;
}
/**
* The sanitizes the purchase_unit items tax.
*

View file

@ -0,0 +1,64 @@
<?php
/**
* Reference transaction status helper class.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class ReferenceTransactionStatus
*
* Helper class to check reference transaction capabilities for PayPal merchant accounts.
*/
class ReferenceTransactionStatus {
public const CACHE_KEY = 'ppcp_reference_transaction_enabled';
protected PartnersEndpoint $partners_endpoint;
protected Cache $cache;
public function __construct( PartnersEndpoint $partners_endpoint, Cache $cache ) {
$this->partners_endpoint = $partners_endpoint;
$this->cache = $cache;
}
/**
* Checks if reference transactions are enabled in the merchant account.
*
* This method verifies if the merchant has the PAYPAL_WALLET_VAULTING_ADVANCED
* capability active, which is required for processing reference transactions.
*
* @return bool True if reference transactions are enabled, false otherwise.
*/
public function reference_transaction_enabled(): bool {
if ( $this->cache->has( self::CACHE_KEY ) ) {
return (bool) $this->cache->get( self::CACHE_KEY );
}
try {
foreach ( $this->partners_endpoint->seller_status()->capabilities() as $capability ) {
if (
$capability->name() === 'PAYPAL_WALLET_VAULTING_ADVANCED' &&
$capability->status() === 'ACTIVE'
) {
$this->cache->set( self::CACHE_KEY, true, MONTH_IN_SECONDS );
return true;
}
}
} catch ( RuntimeException $exception ) {
$this->cache->set( self::CACHE_KEY, false, HOUR_IN_SECONDS );
return false;
}
$this->cache->set( self::CACHE_KEY, false, HOUR_IN_SECONDS );
return false;
}
}

View file

@ -54,7 +54,12 @@ class PartnerReferralsData {
* @param bool $use_card_payments If the merchant wants to process credit card payments.
* @return array
*/
public function data( array $products = array(), string $onboarding_token = '', bool $use_subscriptions = null, bool $use_card_payments = true ) : array {
public function data(
array $products = array(),
string $onboarding_token = '',
?bool $use_subscriptions = null,
bool $use_card_payments = true
) : array {
$in_acdc_country = $this->dcc_applies->for_country_currency();
if ( ! $products ) {

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Applepay;
use WC_Payment_Gateway;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder;
use WooCommerce\PayPalCommerce\Applepay\Assets\ApplePayButton;
use WooCommerce\PayPalCommerce\Applepay\Assets\AppleProductStatus;
use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary;
@ -54,7 +55,7 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
// Clears product status when appropriate.
add_action(
'woocommerce_paypal_payments_clear_apm_product_status',
function( Settings $settings = null ) use ( $c ): void {
function( ?Settings $settings = null ) use ( $c ): void {
$apm_status = $c->get( 'applepay.apple-product-status' );
assert( $apm_status instanceof AppleProductStatus );
$apm_status->clear( $settings );
@ -198,6 +199,31 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
}
);
add_filter(
'ppcp_create_order_request_body_data',
static function ( array $data, string $payment_method, array $request ) use ( $c ) : array {
if ( $payment_method !== ApplePayGateway::ID ) {
return $data;
}
$experience_context_builder = $c->get( 'wcgateway.builder.experience-context' );
assert( $experience_context_builder instanceof ExperienceContextBuilder );
$data['payment_source'] = array(
'apple_pay' => array(
'experience_context' => $experience_context_builder
->with_endpoint_return_urls()
->build()->to_array(),
),
);
return $data;
},
10,
3
);
return true;
}
@ -215,7 +241,8 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
$validation_string = $this->validation_string( $is_sandbox );
nocache_headers();
header( 'Content-Type: text/plain', true, 200 );
echo $validation_string;// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
echo $validation_string;
exit;
}
}

View file

@ -160,7 +160,7 @@ class ApplePayDataObjectHttp {
* @return void
*/
public function validation_data(): void {
$data = filter_input( INPUT_POST, 'validation', FILTER_VALIDATE_BOOL );
$data = filter_input( INPUT_POST, 'validation', FILTER_VALIDATE_BOOLEAN );
if ( ! $data ) {
return;
}

View file

@ -101,7 +101,7 @@ class AppleProductStatus extends ProductStatus {
}
/** {@inheritDoc} */
protected function clear_state( Settings $settings = null ) : void {
protected function clear_state( ?Settings $settings = null ) : void {
if ( null === $settings ) {
$settings = $this->settings;
}

View file

@ -99,7 +99,9 @@ return array(
$container->get( 'api.factory.shipping-preference' ),
$container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'settings.environment' ),
$container->get( 'woocommerce.logger.woocommerce' )
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'wcgateway.builder.experience-context' ),
$container->get( 'settings.data.settings' )
);
},
@ -156,12 +158,7 @@ return array(
* The matrix which countries and currency combinations can be used for AXO.
*/
'axo.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
/**
* Returns which countries and currency combinations can be used for AXO.
*/
return apply_filters(
'woocommerce_paypal_payments_axo_supported_country_currency_matrix',
array(
$matrix = array(
'US' => array(
'AUD',
'CAD',
@ -170,19 +167,25 @@ return array(
'JPY',
'USD',
),
)
);
if ( $container->get( 'axo.uk.enabled' ) ) {
$matrix['GB'] = array( 'GBP' );
}
/**
* Returns which countries and currency combinations can be used for AXO.
*/
return apply_filters(
'woocommerce_paypal_payments_axo_supported_country_currency_matrix',
$matrix
);
},
/**
* The matrix which countries and card type combinations can be used for AXO.
*/
'axo.supported-country-card-type-matrix' => static function ( ContainerInterface $container ) : array {
/**
* Returns which countries and card type combinations can be used for AXO.
*/
return apply_filters(
'woocommerce_paypal_payments_axo_supported_country_card_type_matrix',
array(
$matrix = array(
'US' => array(
'VISA',
'MASTERCARD',
@ -195,7 +198,23 @@ return array(
'AMEX',
'DISCOVER',
),
)
);
if ( $container->get( 'axo.uk.enabled' ) ) {
$matrix['GB'] = array(
'VISA',
'MASTERCARD',
'AMEX',
'DISCOVER',
);
}
/**
* Returns which countries and card type combinations can be used for AXO.
*/
return apply_filters(
'woocommerce_paypal_payments_axo_supported_country_card_type_matrix',
$matrix
);
},
'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string {
@ -379,4 +398,17 @@ return array(
)
);
},
'axo.uk.enabled' => static function ( ContainerInterface $container ): bool {
// phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
/**
* Filter to determine if Fastlane UK with 3D Secure should be enabled.
*
* @param bool $enabled Whether Fastlane UK is enabled.
*/
return apply_filters(
'woocommerce.feature-flags.woocommerce_paypal_payments.axo_uk_enabled',
getenv( 'PCP_AXO_UK_ENABLED' ) !== '0'
);
// phpcs:enable WordPress.NamingConventions.ValidHookName.UseUnderscores
},
);

View file

@ -11,13 +11,17 @@ namespace WooCommerce\PayPalCommerce\Axo\Gateway;
use Psr\Log\LoggerInterface;
use Exception;
use WC_AJAX;
use WC_Order;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewaySettingsRendererTrait;
@ -29,6 +33,8 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\ProcessPaymentTrait;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Helper\CardPaymentsConfiguration;
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
use DomainException;
/**
* Class AXOGateway.
@ -129,6 +135,20 @@ class AxoGateway extends WC_Payment_Gateway {
*/
protected $session_handler;
/**
* The experience context builder.
*
* @var ExperienceContextBuilder
*/
protected $experience_context_builder;
/**
* The settings model.
*
* @var SettingsModel
*/
protected $settings_model;
/**
* AXOGateway constructor.
*
@ -145,6 +165,8 @@ class AxoGateway extends WC_Payment_Gateway {
* @param TransactionUrlProvider $transaction_url_provider The transaction url provider.
* @param Environment $environment The environment.
* @param LoggerInterface $logger The logger.
* @param ExperienceContextBuilder $experience_context_builder The experience context builder.
* @param SettingsModel $settings_model The settings model.
*/
public function __construct(
SettingsRenderer $settings_renderer,
@ -159,7 +181,9 @@ class AxoGateway extends WC_Payment_Gateway {
ShippingPreferenceFactory $shipping_preference_factory,
TransactionUrlProvider $transaction_url_provider,
Environment $environment,
LoggerInterface $logger
LoggerInterface $logger,
ExperienceContextBuilder $experience_context_builder,
SettingsModel $settings_model
) {
$this->id = self::ID;
@ -170,6 +194,8 @@ class AxoGateway extends WC_Payment_Gateway {
$this->session_handler = $session_handler;
$this->order_processor = $order_processor;
$this->card_icons = $card_icons;
$this->experience_context_builder = $experience_context_builder;
$this->settings_model = $settings_model;
$this->method_title = __( 'Fastlane Debit & Credit Cards', 'woocommerce-paypal-payments' );
$this->method_description = __( 'Fastlane accelerates the checkout experience for guest shoppers and autofills their details so they can pay in seconds. When enabled, Fastlane is presented as the default payment method for guests.', 'woocommerce-paypal-payments' );
@ -234,12 +260,20 @@ class AxoGateway extends WC_Payment_Gateway {
if ( ! is_a( $wc_order, WC_Order::class ) ) {
return $this->handle_payment_failure(
null,
new GatewayGenericException( new Exception( 'WC order was not found.' ) )
new GatewayGenericException( new Exception( 'WC order was not found.' ) ),
);
}
// phpcs:disable WordPress.Security.NonceVerification
$axo_nonce = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) );
$token_param = wc_clean( wp_unslash( $_GET['token'] ?? '' ) );
if ( empty( $axo_nonce ) && ! empty( $token_param ) ) {
return $this->process_3ds_return( $wc_order, $token_param );
}
try {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$fastlane_member = wc_clean( wp_unslash( $_POST['fastlane_member'] ?? '' ) );
if ( $fastlane_member ) {
$payment_method_title = __( 'Debit & Credit Cards (via Fastlane by PayPal)', 'woocommerce-paypal-payments' );
@ -248,10 +282,37 @@ class AxoGateway extends WC_Payment_Gateway {
}
// The `axo_nonce` is not a WP nonce, but a card-token generated by the JS SDK.
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$token = wc_clean( wp_unslash( $_POST['axo_nonce'] ?? '' ) );
if ( empty( $axo_nonce ) ) {
return array(
'result' => 'failure',
'message' => __( 'No payment token provided. Please try again.', 'woocommerce-paypal-payments' ),
);
}
$order = $this->create_paypal_order( $wc_order, $token );
$order = $this->create_paypal_order( $wc_order, $axo_nonce );
// Check if 3DS verification is required.
$payer_action = $this->get_payer_action_url( $order );
// If 3DS verification is required, redirect with token in return URL.
if ( $payer_action ) {
$return_url = add_query_arg(
'token',
$order->id(),
home_url( WC_AJAX::get_endpoint( ReturnUrlEndpoint::ENDPOINT ) )
);
$redirect_url = add_query_arg(
'redirect_uri',
rawurlencode( $return_url ),
$payer_action
);
return array(
'result' => 'success',
'redirect' => $redirect_url,
);
}
/**
* This filter controls if the method 'process()' from OrderProcessor will be called.
@ -266,7 +327,11 @@ class AxoGateway extends WC_Payment_Gateway {
$this->order_processor->process_captured_and_authorized( $wc_order, $order );
}
} catch ( Exception $exception ) {
return $this->handle_payment_failure( $wc_order, $exception );
$this->logger->error( '[AXO] Payment processing failed: ' . $exception->getMessage() );
return array(
'result' => 'failure',
'message' => $this->get_user_friendly_error_message( $exception ),
);
}
WC()->cart->empty_cart();
@ -275,6 +340,104 @@ class AxoGateway extends WC_Payment_Gateway {
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
// phpcs:enable WordPress.Security.NonceVerification
}
/**
* Process 3DS return scenario.
*
* @param WC_Order $wc_order The WooCommerce order.
* @param string $token The PayPal order token.
*
* @return array
*/
protected function process_3ds_return( WC_Order $wc_order, string $token ) : array {
try {
$paypal_order = $this->order_endpoint->order( $token );
if ( ! $paypal_order->status()->is( OrderStatus::COMPLETED ) ) {
return array(
'result' => 'failure',
'message' => __( '3D Secure authentication was not completed successfully. Please try again.', 'woocommerce-paypal-payments' ),
);
}
/**
* This filter controls if the method 'process()' from OrderProcessor will be called.
* So you can implement your own for example on subscriptions
*
* - true bool controls execution of 'OrderProcessor::process()'
* - $this \WC_Payment_Gateway
* - $wc_order \WC_Order
*/
$process = apply_filters( 'woocommerce_paypal_payments_before_order_process', true, $this, $wc_order );
if ( $process ) {
$this->order_processor->process_captured_and_authorized( $wc_order, $paypal_order );
}
} catch ( Exception $exception ) {
$this->logger->error( '[AXO] 3DS return processing failed: ' . $exception->getMessage() );
return array(
'result' => 'failure',
'message' => $this->get_user_friendly_error_message( $exception ),
);
}
WC()->cart->empty_cart();
return array(
'result' => 'success',
'redirect' => $this->get_return_url( $wc_order ),
);
}
/**
* Convert exceptions to user-friendly messages.
*/
private function get_user_friendly_error_message( Exception $exception ) {
$error_message = $exception->getMessage();
// Handle specific error types with user-friendly messages.
if ( $exception instanceof DomainException ) {
if ( strpos( $error_message, 'Could not capture' ) !== false ) {
return __( '3D Secure authentication was unavailable or failed. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' );
}
}
if ( strpos( $error_message, 'declined' ) !== false ||
strpos( $error_message, 'PAYMENT_DENIED' ) !== false ||
strpos( $error_message, 'INSTRUMENT_DECLINED' ) !== false ||
strpos( $error_message, 'Payment provider declined' ) !== false ) {
return __( 'Your payment was declined after 3D Secure verification. Please try a different payment method or contact your bank.', 'woocommerce-paypal-payments' );
}
if ( strpos( $error_message, 'session' ) !== false ||
strpos( $error_message, 'expired' ) !== false ) {
return __( 'Payment session expired. Please try your payment again.', 'woocommerce-paypal-payments' );
}
return __( 'There was an error processing your payment. Please try again or contact support.', 'woocommerce-paypal-payments' );
}
/**
* Extract payer action URL from PayPal order.
*
* @param Order $order The PayPal order.
* @return string The payer action URL or an empty string if not found.
*/
private function get_payer_action_url( Order $order ) : string {
$links = $order->links();
if ( ! $links ) {
return '';
}
foreach ( $links as $link ) {
if ( isset( $link->rel ) && $link->rel === 'payer-action' ) {
return $link->href ?? '';
}
}
return '';
}
/**
@ -293,9 +456,8 @@ class AxoGateway extends WC_Payment_Gateway {
'checkout'
);
$payment_source_properties = (object) array(
'single_use_token' => $payment_token,
);
// Build payment source with 3DS verification if needed.
$payment_source_properties = $this->build_payment_source_properties( $payment_token );
$payment_source = new PaymentSource(
'card',
@ -306,12 +468,64 @@ class AxoGateway extends WC_Payment_Gateway {
array( $purchase_unit ),
$shipping_preference,
null,
'',
array(),
$payment_source
self::ID,
$this->build_order_data(),
$payment_source,
$wc_order
);
}
/**
* Build payment source properties.
*
* @param string $payment_token The payment token.
* @return object The payment source properties.
*/
protected function build_payment_source_properties( string $payment_token ): object {
$properties = array(
'single_use_token' => $payment_token,
);
$three_d_secure = $this->settings_model->get_three_d_secure_enum();
if ( 'SCA_ALWAYS' === $three_d_secure || 'SCA_WHEN_REQUIRED' === $three_d_secure ) {
$properties['attributes'] = array(
'verification' => array(
'method' => $three_d_secure,
),
);
}
return (object) $properties;
}
/**
* Build additional order data for experience context and 3DS verification.
*
* @return array The order data.
*/
protected function build_order_data(): array {
$data = array();
$experience_context = $this->experience_context_builder
->with_endpoint_return_urls()
->with_current_brand_name()
->with_current_locale()
->build();
$data['experience_context'] = $experience_context->to_array();
$three_d_secure = $this->settings_model->get_three_d_secure_enum();
if ( $three_d_secure === 'SCA_ALWAYS' || $three_d_secure === 'SCA_WHEN_REQUIRED' ) {
$data['transaction_context'] = array(
'soft_descriptor' => __( 'Card verification hold', 'woocommerce-paypal-payments' ),
);
}
return $data;
}
/**
* Returns the icons of the gateway.
*

View file

@ -20,6 +20,7 @@ import {
handleApproveSubscription,
onApproveSavePayment,
} from '../paypal-config';
import { useRef } from 'react';
const PAYPAL_GATEWAY_ID = 'ppcp-gateway';
@ -52,6 +53,8 @@ export const PayPalComponent = ( {
const [ paypalScriptLoaded, setPaypalScriptLoaded ] = useState( false );
const paypalButtonRef = useRef( null );
if ( ! paypalScriptLoaded ) {
if ( ! paypalScriptPromise ) {
// for editor, since canMakePayment was not called
@ -139,6 +142,16 @@ export const PayPalComponent = ( {
onClick();
};
const handleButtonInit = () => {
if ( fundingSource === 'paypal' ) {
const buttonInstance = paypalButtonRef.current?.state?.parent;
if ( buttonInstance?.hasReturned?.() ) {
buttonInstance.resume();
}
}
};
const shouldHandleShippingInPayPal = () => {
return shouldskipFinalConfirmation() && config.needShipping;
};
@ -352,6 +365,10 @@ export const PayPalComponent = ( {
);
const getOnShippingOptionsChange = ( fundingSource ) => {
if ( config.scriptData.server_side_shipping_callback.enabled ) {
return null;
}
if ( fundingSource === 'venmo' ) {
return null;
}
@ -364,6 +381,10 @@ export const PayPalComponent = ( {
};
const getOnShippingAddressChange = ( fundingSource ) => {
if ( config.scriptData.server_side_shipping_callback.enabled ) {
return null;
}
if ( fundingSource === 'venmo' ) {
return null;
}
@ -377,6 +398,15 @@ export const PayPalComponent = ( {
};
};
const shouldEnableAppSwitch = () => {
// AppSwitch should only be enabled in Pay Now flows with server side shipping callback.
return (
config.scriptData.appswitch.enabled &&
! config.scriptData.final_review_enabled &&
config.scriptData.server_side_shipping_callback.enabled
);
};
if (
cartHasSubscriptionProducts( config.scriptData ) &&
config.scriptData.is_free_trial_cart
@ -434,8 +464,11 @@ export const PayPalComponent = ( {
return (
<PayPalButton
ref={ paypalButtonRef }
appSwitchWhenAvailable={ shouldEnableAppSwitch() }
fundingSource={ fundingSource }
style={ style }
onInit={ handleButtonInit }
onClick={ handleClick }
onCancel={ onClose }
onError={ onClose }

View file

@ -88,7 +88,7 @@ export const paypalPayerToWc = ( payer ) => {
first_name: firstName,
last_name: lastName,
email: payer.email_address,
phone: phone
phone,
};
};
@ -115,7 +115,7 @@ export const paypalSubscriberToWc = ( subscriber ) => {
* @return {Object}
*/
export const paypalOrderToWcShippingAddress = ( order ) => {
const shipping = order.purchase_units[ 0 ].shipping;
const shipping = order.purchase_units?.[ 0 ]?.shipping;
if ( ! shipping ) {
return {};
}

View file

@ -2,6 +2,7 @@ import {
paypalOrderToWcAddresses,
paypalSubscriptionToWcAddresses,
} from './Helper/Address';
import ResumeFlowHelper from '../../../ppcp-button/resources/js/modules/Helper/ResumeFlowHelper';
export const createOrder = async ( data, config, onError, onClose ) => {
try {
@ -66,9 +67,38 @@ export const handleApprove = async (
onClose
) => {
try {
const order = await actions.order.get();
let order;
// actions.order.get is not available on the AppSwitch flow.
if ( ! ResumeFlowHelper.isResumeFlow() ) {
order = await actions.order.get();
} else {
const res = await fetch(
config.scriptData.ajax.get_order.endpoint,
{
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( {
nonce: config.scriptData.ajax.get_order.nonce,
order_id: data.orderID,
} ),
}
);
const json = await res.json();
if ( ! json.success ) {
throw new Error(
json.data?.message || config.scriptData.labels.error.generic
);
}
order = json.data;
}
if ( order ) {
const addresses = paypalOrderToWcAddresses( order );
const promises = [
@ -94,7 +124,6 @@ export const handleApprove = async (
}
}
await Promise.all( promises );
}
setPaypalOrder( order );

View file

@ -1,6 +1,7 @@
import onApprove from '../OnApproveHandler/onApproveForContinue.js';
import { payerData } from '../Helper/PayerData';
import { PaymentMethods } from '../Helper/CheckoutMethodState';
import ResumeFlowHelper from '../Helper/ResumeFlowHelper';
class CartActionHandler {
constructor( config, errorHandler ) {
@ -8,14 +9,14 @@ class CartActionHandler {
this.errorHandler = errorHandler;
}
subscriptionsConfiguration( subscription_plan_id ) {
subscriptionsConfiguration( subscriptionPlanId ) {
return {
createSubscription: ( data, actions ) => {
return actions.subscription.create( {
plan_id: subscription_plan_id,
plan_id: subscriptionPlanId,
} );
},
onApprove: ( data, actions ) => {
onApprove: ( data ) => {
fetch( this.config.ajax.approve_subscription.endpoint, {
method: 'POST',
credentials: 'same-origin',
@ -24,7 +25,7 @@ class CartActionHandler {
order_id: data.orderID,
subscription_id: data.subscriptionID,
should_create_wc_order:
! context.config.vaultingEnabled ||
! this.config.vaultingEnabled ||
data.paymentSource !== 'venmo',
} ),
} )
@ -33,7 +34,6 @@ class CartActionHandler {
} )
.then( ( data ) => {
if ( ! data.success ) {
console.log( data );
throw Error( data.data.message );
}
@ -41,7 +41,7 @@ class CartActionHandler {
location.href = orderReceivedUrl
? orderReceivedUrl
: context.config.redirect;
: this.config.redirect;
} );
},
onError: ( err ) => {
@ -51,7 +51,7 @@ class CartActionHandler {
}
configuration() {
const createOrder = ( data, actions ) => {
const createOrder = () => {
const payer = payerData();
const bnCode =
typeof this.config.bn_codes[ this.config.context ] !==
@ -89,8 +89,15 @@ class CartActionHandler {
return {
createOrder,
onApprove: onApprove( this, this.errorHandler ),
onError: ( error ) => {
onError: () => {
this.errorHandler.genericError();
if ( ResumeFlowHelper.isResumeFlow() ) {
ResumeFlowHelper.cleanHashParams();
jQuery( this.config.button.wrapper ).trigger(
'ppcp-reload-buttons'
);
}
},
};
}

View file

@ -3,6 +3,7 @@ import onApprove from '../OnApproveHandler/onApproveForPayNow.js';
import { payerData } from '../Helper/PayerData';
import { getCurrentPaymentMethod } from '../Helper/CheckoutMethodState';
import validateCheckoutForm from '../Helper/CheckoutFormValidation';
import ResumeFlowHelper from '../Helper/ResumeFlowHelper';
class CheckoutActionHandler {
constructor( config, errorHandler, spinner ) {
@ -170,6 +171,13 @@ class CheckoutActionHandler {
}
this.errorHandler.genericError();
if ( ResumeFlowHelper.isResumeFlow() ) {
ResumeFlowHelper.cleanHashParams();
jQuery( this.config.button.wrapper ).trigger(
'ppcp-reload-buttons'
);
}
},
};
}

View file

@ -5,6 +5,7 @@ import { payerData } from '../Helper/PayerData';
import { PaymentMethods } from '../Helper/CheckoutMethodState';
import CartHelper from '../Helper/CartHelper';
import FormHelper from '../Helper/FormHelper';
import ResumeFlowHelper from '../Helper/ResumeFlowHelper';
class SingleProductActionHandler {
constructor( config, updateCart, formElement, errorHandler ) {
@ -64,6 +65,13 @@ class SingleProductActionHandler {
},
onError: ( err ) => {
console.error( err );
if ( ResumeFlowHelper.isResumeFlow() ) {
ResumeFlowHelper.cleanHashParams();
jQuery( this.config.button.wrapper ).trigger(
'ppcp-reload-buttons'
);
}
},
};
}
@ -83,9 +91,16 @@ class SingleProductActionHandler {
if ( this.isBookingProduct() && error.message ) {
this.errorHandler.clear();
this.errorHandler.message( error.message );
return;
}
} else {
this.errorHandler.genericError();
}
if ( ResumeFlowHelper.isResumeFlow() ) {
ResumeFlowHelper.cleanHashParams();
jQuery( this.config.button.wrapper ).trigger(
'ppcp-reload-buttons'
);
}
},
onCancel: () => {
// Could be used for every product type,

View file

@ -0,0 +1,59 @@
class ResumeFlowHelper {
static PAYPAL_PARAMS = [
'onApprove',
'token',
'PayerID',
'payerID',
'button_session_id',
'billingToken',
'orderID',
'switch_initiated_time',
'onCancel',
'onError',
];
static cleanHashParams() {
if ( ! window.location.hash ) {
return;
}
const hashString = window.location.hash.substring( 1 );
const params = hashString.split( '&' );
const cleanedParams = params.filter( ( param ) => {
const paramName = param.split( '=' )[ 0 ];
return ! this.PAYPAL_PARAMS.includes( paramName );
} );
if ( cleanedParams.length > 0 ) {
const newHash = '#' + cleanedParams.join( '&' );
window.history.replaceState(
null,
'',
window.location.pathname + window.location.search + newHash
);
} else {
window.history.replaceState(
null,
'',
window.location.pathname + window.location.search
);
}
}
static isResumeFlow() {
if ( ! window.location.hash ) {
return false;
}
const hashString = window.location.hash.substring( 1 );
const params = hashString.split( '&' );
return params.some( ( param ) => {
const paramName = param.split( '=' )[ 0 ];
return paramName === 'switch_initiated_time';
} );
}
}
export default ResumeFlowHelper;

View file

@ -222,6 +222,7 @@ export default class PaymentButton {
* @param {Object} ppcpConfig - Plugin wide configuration object.
* @param {unknown} contextHandler - Handler object.
* @param {Object} buttonAttributes - Button attributes.
* @param {Function} onClick - Event from content component registered in registerExpressPaymentMethod.
* @return {PaymentButton} The button instance.
*/
static createButton(
@ -230,7 +231,8 @@ export default class PaymentButton {
buttonConfig,
ppcpConfig,
contextHandler,
buttonAttributes
buttonAttributes,
onClick = null
) {
const buttonInstances = getInstances();
const instanceKey = `${ this.methodId }.${ context }`;
@ -242,7 +244,8 @@ export default class PaymentButton {
buttonConfig,
ppcpConfig,
contextHandler,
buttonAttributes
buttonAttributes,
onClick
);
buttonInstances.set( instanceKey, button );
@ -292,6 +295,7 @@ export default class PaymentButton {
* @param {Object} ppcpConfig - Plugin wide configuration object.
* @param {Object} contextHandler - Handler object.
* @param {Object} buttonAttributes - Button attributes.
* @param {Function} onClick - Event from content component registered in registerExpressPaymentMethod.
*/
constructor(
context,
@ -299,7 +303,8 @@ export default class PaymentButton {
buttonConfig = {},
ppcpConfig = {},
contextHandler = null,
buttonAttributes = {}
buttonAttributes = {},
onClick = null
) {
if ( this.methodId === PaymentButton.methodId ) {
throw new Error( 'Cannot initialize the PaymentButton base class' );
@ -318,6 +323,7 @@ export default class PaymentButton {
this.#externalHandler = externalHandler;
this.#contextHandler = contextHandler;
this.#buttonAttributes = buttonAttributes;
this.onClick = onClick;
this.#logger = new ConsoleLogger( methodName, context );

View file

@ -144,7 +144,10 @@ class Renderer {
};
// Check the condition and add the handler if needed
if ( this.shouldEnableShippingCallback() ) {
if (
this.shouldEnableShippingCallback() &&
! this.defaultSettings.server_side_shipping_callback.enabled
) {
options.onShippingOptionsChange = ( data, actions ) => {
const shippingOptionsChange =
! this.isVenmoButtonClickedWhenVaultingIsEnabled(
@ -175,6 +178,10 @@ class Renderer {
};
}
if ( this.shouldEnableAppSwitch() ) {
options.appSwitchWhenAvailable = true;
}
return options;
};
@ -242,6 +249,15 @@ class Renderer {
);
};
shouldEnableAppSwitch = () => {
// AppSwitch should only be enabled in Pay Now flows with server side shipping callback.
return (
this.defaultSettings.appswitch.enabled &&
! this.defaultSettings.final_review_enabled &&
this.defaultSettings.server_side_shipping_callback.enabled
);
};
isAlreadyRendered( wrapper, fundingSource ) {
return this.renderedSources.has( wrapper + ( fundingSource ?? '' ) );
}

View file

@ -64,8 +64,12 @@ class WidgetBuilder {
return;
}
if ( btn.hasReturned() ) {
btn.resume();
} else {
btn.render( target );
}
}
renderAllButtons() {
for ( const [ wrapper ] of this.buttons ) {

View file

@ -29,6 +29,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
@ -167,9 +168,12 @@ return array(
$container->get( 'api.endpoint.payment-tokens' ),
$container->get( 'woocommerce.logger.woocommerce' ),
$container->get( 'button.handle-shipping-in-paypal' ),
$container->get( 'wcgateway.server-side-shipping-callback-enabled' ),
$container->get( 'wcgateway.appswitch-enabled' ),
$container->get( 'button.helper.disabled-funding-sources' ),
$container->get( 'wcgateway.configuration.card-configuration' ),
$container->get( 'api.helper.partner-attribution' )
$container->get( 'api.helper.partner-attribution' ),
$container->get( 'blocks.settings.final_review_enabled' )
);
},
'button.url' => static function ( ContainerInterface $container ): string {
@ -227,6 +231,8 @@ return array(
$request_data,
$purchase_unit_factory,
$container->get( 'api.factory.shipping-preference' ),
$container->get( 'api.factory.return-url' ),
$container->get( 'api.factory.contact-preference' ),
$container->get( 'wcgateway.builder.experience-context' ),
$order_endpoint,
$payer_factory,
@ -238,6 +244,7 @@ return array(
$container->get( 'button.early-wc-checkout-validation-enabled' ),
$container->get( 'button.pay-now-contexts' ),
$container->get( 'button.handle-shipping-in-paypal' ),
$container->get( 'wcgateway.server-side-shipping-callback-enabled' ),
$container->get( 'wcgateway.funding-sources-without-redirect' ),
$logger
);
@ -327,6 +334,16 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'button.endpoint.get-order' => static function ( ContainerInterface $container ): GetOrderEndpoint {
$request_data = $container->get( 'button.request-data' );
$order_endpoint = $container->get( 'api.endpoint.order' );
$logger = $container->get( 'woocommerce.logger.woocommerce' );
return new GetOrderEndpoint(
$request_data,
$order_endpoint,
$logger
);
},
'button.helper.cart-products' => static function ( ContainerInterface $container ): CartProductsHelper {
$data_store = \WC_Data_Store::load( 'product' );
return new CartProductsHelper( $data_store );

View file

@ -29,6 +29,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
@ -253,6 +254,21 @@ class SmartButton implements SmartButtonInterface {
*/
protected PartnerAttribution $partner_attribution;
/**
* Whether the server-side shipping callback is enabled (feature flag).
*/
private bool $server_side_shipping_callback_enabled;
/**
* Whether the AppSwitch is enabled (feature flag).
*/
private bool $appswitch_enabled;
/**
* Whether the final review is enabled in blocks settings.
*/
private bool $final_review_enabled;
/**
* SmartButton constructor.
*
@ -279,9 +295,12 @@ class SmartButton implements SmartButtonInterface {
* @param PaymentTokensEndpoint $payment_tokens_endpoint Payment tokens endpoint.
* @param LoggerInterface $logger The logger.
* @param bool $should_handle_shipping_in_paypal Whether the shipping should be handled in PayPal.
* @param bool $server_side_shipping_callback_enabled Whether the server-side shipping callback is enabled (feature flag).
* @param bool $appswitch_enabled Whether the AppSwitch is enabled (feature flag).
* @param DisabledFundingSources $disabled_funding_sources List of funding sources to be disabled.
* @param CardPaymentsConfiguration $dcc_configuration The DCC Gateway Configuration.
* @param PartnerAttribution $partner_attribution The PayPal Partner Attribution Helper.
* @param bool $final_review_enabled Whether the final review is enabled in blocks settings.
*/
public function __construct(
string $module_url,
@ -307,9 +326,12 @@ class SmartButton implements SmartButtonInterface {
PaymentTokensEndpoint $payment_tokens_endpoint,
LoggerInterface $logger,
bool $should_handle_shipping_in_paypal,
bool $server_side_shipping_callback_enabled,
bool $appswitch_enabled,
DisabledFundingSources $disabled_funding_sources,
CardPaymentsConfiguration $dcc_configuration,
PartnerAttribution $partner_attribution
PartnerAttribution $partner_attribution,
bool $final_review_enabled
) {
$this->module_url = $module_url;
$this->version = $version;
@ -334,9 +356,12 @@ class SmartButton implements SmartButtonInterface {
$this->logger = $logger;
$this->payment_tokens_endpoint = $payment_tokens_endpoint;
$this->should_handle_shipping_in_paypal = $should_handle_shipping_in_paypal;
$this->server_side_shipping_callback_enabled = $server_side_shipping_callback_enabled;
$this->appswitch_enabled = $appswitch_enabled;
$this->disabled_funding_sources = $disabled_funding_sources;
$this->dcc_configuration = $dcc_configuration;
$this->partner_attribution = $partner_attribution;
$this->final_review_enabled = $final_review_enabled;
}
/**
@ -793,7 +818,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
* @param string $gateway_id The gateway ID, like 'ppcp-gateway'.
* @param string|null $action_name The action name to be called.
*/
public function button_renderer( string $gateway_id, string $action_name = null ) {
public function button_renderer( string $gateway_id, ?string $action_name = null ) {
$available_gateways = WC()->payment_gateways->get_available_payment_gateways();
@ -1173,6 +1198,10 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ),
),
'get_order' => array(
'endpoint' => \WC_AJAX::get_endpoint( GetOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( GetOrderEndpoint::nonce() ),
),
'approve_subscription' => array(
'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ),
@ -1341,10 +1370,17 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
'has_wc_card_payment_tokens' => $this->user_has_wc_card_payment_tokens( get_current_user_id() ),
),
'should_handle_shipping_in_paypal' => $this->should_handle_shipping_in_paypal && ! $this->is_checkout(),
'server_side_shipping_callback' => array(
'enabled' => $this->server_side_shipping_callback_enabled,
),
'appswitch' => array(
'enabled' => $this->appswitch_enabled,
),
'needShipping' => $this->need_shipping(),
'vaultingEnabled' => $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ),
'productType' => null,
'manualRenewalEnabled' => $this->subscription_helper->accept_manual_renewals(),
'final_review_enabled' => $this->final_review_enabled,
);
if ( is_product() ) {
@ -1901,8 +1937,10 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
$variations = $product->get_available_variations( 'objects' );
$in_stock = $this->has_in_stock_variation( $variations );
}
// phpcs:disable WordPress.Security.NonceVerification.Recommended
$enable_button = ! $product->is_type( array( 'external', 'grouped' ) ) && $in_stock &&
! ( ( $product->is_type( 'subscription' ) || $product->is_type( 'variable-subscription' ) ) && ! empty( $_GET['switch-subscription'] ) );
// phpcs:enable WordPress.Security.NonceVerification.Recommended
/**
* Allows to filter if PayPal buttons/messages can be rendered for the given product.
@ -1941,7 +1979,7 @@ document.querySelector("#payment").before(document.querySelector(".ppcp-messages
* @param array $context_data The context data for this filter.
* @return bool
*/
public function is_button_disabled( string $context = null, array $context_data = array() ): bool {
public function is_button_disabled( ?string $context = null, array $context_data = array() ): bool {
if ( null === $context ) {
$context = $this->context();
}

View file

@ -19,6 +19,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\GetOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
@ -217,5 +218,14 @@ class ButtonModule implements ServiceModule, ExtendingModule, ExecutableModule {
$endpoint->handle_request();
}
);
add_action(
'wc_ajax_' . GetOrderEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.get-order' );
assert( $endpoint instanceof GetOrderEndpoint );
$endpoint->handle_request();
}
);
}
}

View file

@ -173,9 +173,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
try {
$data = $this->request_data->read_request( self::nonce() );
if ( ! isset( $data['order_id'] ) ) {
throw new RuntimeException(
__( 'No order id given', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No order id given' );
}
$order = $this->api_endpoint->order( $data['order_id'] );

View file

@ -111,9 +111,7 @@ class ApproveSubscriptionEndpoint implements EndpointInterface {
public function handle_request(): bool {
$data = $this->request_data->read_request( $this->nonce() );
if ( ! isset( $data['order_id'] ) ) {
throw new RuntimeException(
__( 'No order id given', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'No order id given' );
}
$order = $this->order_endpoint->order( $data['order_id'] );

View file

@ -76,7 +76,8 @@ class CartScriptParamsEndpoint implements EndpointInterface {
wc_maybe_define_constant( 'WOOCOMMERCE_CART', true );
}
$include_shipping = (bool) wc_clean( wp_unslash( $_GET['shipping'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$include_shipping = (bool) wc_clean( wp_unslash( $_GET['shipping'] ?? '' ) );
$script_data = $this->smart_button->script_data();
if ( ! $script_data ) {
@ -155,7 +156,9 @@ class CartScriptParamsEndpoint implements EndpointInterface {
'description' => html_entity_decode(
wp_strip_all_tags(
wc_price( (float) $rate->get_cost(), array( 'currency' => get_woocommerce_currency() ) )
)
),
ENT_QUOTES,
'UTF-8'
),
);
}

View file

@ -13,6 +13,7 @@ use Exception;
use Psr\Log\LoggerInterface;
use stdClass;
use Throwable;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Amount;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ExperienceContext;
@ -26,6 +27,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ReturnUrlFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\Button\Exception\ValidationException;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
@ -37,6 +39,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 +71,13 @@ class CreateOrderEndpoint implements EndpointInterface {
*/
private $shipping_preference_factory;
private ReturnUrlFactory $return_url_factory;
/**
* The contact_preference factors.
*/
private ContactPreferenceFactory $contact_preference_factory;
/**
* The ExperienceContextBuilder.
*/
@ -157,6 +167,11 @@ class CreateOrderEndpoint implements EndpointInterface {
*/
private $handle_shipping_in_paypal;
/**
* Whether the server-side shipping callback is enabled (feature flag).
*/
private bool $server_side_shipping_callback_enabled;
/**
* The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back.
*
@ -184,6 +199,8 @@ 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 ReturnUrlFactory $return_url_factory The return URL 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.
@ -195,6 +212,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* @param bool $early_validation_enabled Whether to execute WC validation of the checkout form.
* @param string[] $pay_now_contexts The contexts that should have the Pay Now button.
* @param bool $handle_shipping_in_paypal If true, the shipping methods are sent to PayPal allowing the customer to select it inside the popup.
* @param bool $server_side_shipping_callback_enabled Whether the server-side shipping callback is enabled (feature flag).
* @param string[] $funding_sources_without_redirect The sources that do not cause issues about redirecting (on mobile, ...) and sometimes not returning back.
* @param LoggerInterface $logger The logger.
*/
@ -202,6 +220,8 @@ class CreateOrderEndpoint implements EndpointInterface {
RequestData $request_data,
PurchaseUnitFactory $purchase_unit_factory,
ShippingPreferenceFactory $shipping_preference_factory,
ReturnUrlFactory $return_url_factory,
ContactPreferenceFactory $contact_preference_factory,
ExperienceContextBuilder $experience_context_builder,
OrderEndpoint $order_endpoint,
PayerFactory $payer_factory,
@ -213,6 +233,7 @@ class CreateOrderEndpoint implements EndpointInterface {
bool $early_validation_enabled,
array $pay_now_contexts,
bool $handle_shipping_in_paypal,
bool $server_side_shipping_callback_enabled,
array $funding_sources_without_redirect,
LoggerInterface $logger
) {
@ -220,6 +241,8 @@ class CreateOrderEndpoint implements EndpointInterface {
$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->return_url_factory = $return_url_factory;
$this->experience_context_builder = $experience_context_builder;
$this->api_endpoint = $order_endpoint;
$this->payer_factory = $payer_factory;
@ -231,6 +254,7 @@ class CreateOrderEndpoint implements EndpointInterface {
$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;
}
@ -259,7 +283,7 @@ class CreateOrderEndpoint implements EndpointInterface {
$wc_order = null;
if ( 'pay-now' === $data['context'] ) {
$wc_order = wc_get_order( (int) $data['order_id'] );
if ( ! is_a( $wc_order, \WC_Order::class ) ) {
if ( ! is_a( $wc_order, WC_Order::class ) ) {
wp_send_json_error(
array(
'name' => 'order-not-found',
@ -336,7 +360,7 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->early_order_handler->register_for_order( $order );
}
if ( 'pay-now' === $data['context'] && is_a( $wc_order, \WC_Order::class ) ) {
if ( 'pay-now' === $data['context'] && is_a( $wc_order, WC_Order::class ) ) {
$wc_order->update_meta_data( PayPalGateway::ORDER_ID_META_KEY, $order->id() );
$wc_order->update_meta_data( PayPalGateway::INTENT_META_KEY, $order->intent() );
@ -392,54 +416,10 @@ class CreateOrderEndpoint implements EndpointInterface {
return false;
}
/**
* Once the checkout has been validated we execute this method.
*
* @param array $data The data.
* @param \WP_Error $errors The errors, which occurred.
*
* @return array
* @throws Exception On Error.
*/
public function after_checkout_validation( array $data, \WP_Error $errors ): array {
if ( ! $errors->errors ) {
try {
$order = $this->create_paypal_order();
} catch ( Exception $exception ) {
$this->logger->error( 'Order creation failed: ' . $exception->getMessage() );
throw $exception;
}
/**
* In case we are onboarded and everything is fine with the \WC_Order
* we want this order to be created. We will intercept it and leave it
* in the "Pending payment" status though, which than later will change
* during the "onApprove"-JS callback or the webhook listener.
*/
if ( ! $this->early_order_handler->should_create_early_order() ) {
wp_send_json_success( $this->make_response( $order ) );
}
$this->early_order_handler->register_for_order( $order );
return $data;
}
$this->logger->error( 'Checkout validation failed: ' . $errors->get_error_message() );
wp_send_json_error(
array(
'name' => '',
'message' => $errors->get_error_message(),
'code' => (int) $errors->get_error_code(),
'details' => array(),
)
);
return $data;
}
/**
* Creates the order in the PayPal, uses data from WC order if provided.
*
* @param \WC_Order|null $wc_order WC order to get data from.
* @param WC_Order|null $wc_order WC order to get data from.
* @param string $payment_method WC payment method.
* @param array $data Request data.
*
@ -450,7 +430,7 @@ class CreateOrderEndpoint implements EndpointInterface {
*
* phpcs:disable Squiz.Commenting.FunctionCommentThrowTag.WrongNumber
*/
private function create_paypal_order( \WC_Order $wc_order = null, string $payment_method = '', array $data = array() ): Order {
private function create_paypal_order( ?WC_Order $wc_order = null, string $payment_method = '', array $data = array() ): Order {
assert( $this->purchase_unit instanceof PurchaseUnit );
$funding_source = $this->parsed_request_data['funding_source'] ?? '';
@ -485,12 +465,38 @@ class CreateOrderEndpoint implements EndpointInterface {
}
}
$payment_source = new PaymentSource(
'paypal',
(object) array(
'experience_context' => $this->experience_context_builder
if ( 'venmo' === $funding_source ) {
$payment_source_key = 'venmo';
} else {
$payment_source_key = 'paypal';
}
$contact_preference = $this->contact_preference_factory->from_state(
$payment_source_key
);
$experience_context = $this->experience_context_builder
->with_default_paypal_config( $shipping_preference, $action )
->build()->to_array(),
->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();
}
$return_url = $this->return_url_factory->from_context(
$this->parsed_request_data['context'],
$this->parsed_request_data
);
$payment_source = new PaymentSource(
$payment_source_key,
(object) array(
'experience_context' => $experience_context
->with_custom_return_url( $return_url )
->with_custom_cancel_url( $return_url )
->build()
->to_array(),
)
);
@ -536,11 +542,11 @@ class CreateOrderEndpoint implements EndpointInterface {
* Returns the Payer entity based on the request data.
*
* @param array $data The request data.
* @param \WC_Order|null $wc_order The order.
* @param WC_Order|null $wc_order The order.
*
* @return Payer|null
*/
private function payer( array $data, \WC_Order $wc_order = null ) {
private function payer( array $data, ?WC_Order $wc_order = null ) {
if ( 'pay-now' === $data['context'] ) {
$payer = $this->payer_factory->from_wc_order( $wc_order );
return $payer;

View file

@ -0,0 +1,84 @@
<?php
/**
* The endpoint to get a PayPal order.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class GetOrderEndpoint
*/
class GetOrderEndpoint implements EndpointInterface {
public const ENDPOINT = 'ppc-get-order';
private RequestData $request_data;
private OrderEndpoint $api_endpoint;
private LoggerInterface $logger;
public function __construct(
RequestData $request_data,
OrderEndpoint $order_endpoint,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->api_endpoint = $order_endpoint;
$this->logger = $logger;
}
public static function nonce(): string {
return self::ENDPOINT;
}
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
$order_id = $data['order_id'] ?? '';
if ( empty( $order_id ) ) {
wp_send_json_error(
array(
'message' => __( 'Order ID is required', 'woocommerce-paypal-payments' ),
)
);
return false;
}
$order = $this->api_endpoint->order( $order_id );
wp_send_json_success( $order->to_array() );
return true;
} catch ( RuntimeException $error ) {
$this->logger->error( 'Get order failed: ' . $error->getMessage() );
wp_send_json_error(
array(
'name' => is_a( $error, PayPalApiException::class ) ? $error->name() : '',
'message' => $error->getMessage(),
'code' => $error->getCode(),
'details' => is_a( $error, PayPalApiException::class ) ? $error->details() : array(),
)
);
} catch ( Exception $exception ) {
$this->logger->error( 'Get order failed: ' . $exception->getMessage() );
wp_send_json_error(
array(
'message' => $exception->getMessage(),
)
);
}
return false;
}
}

View file

@ -47,9 +47,7 @@ class RequestData {
|| ! wp_verify_nonce( $json['nonce'], $nonce )
) {
remove_filter( 'nonce_user_logged_out', array( $this, 'nonce_fix' ), 100 );
throw new RuntimeException(
__( 'Could not validate nonce.', 'woocommerce-paypal-payments' )
);
throw new RuntimeException( 'Could not validate nonce.' );
}
$this->dequeue_nonce_fix();

View file

@ -301,8 +301,9 @@ trait ContextTrait {
* @return bool
*/
private function is_subscription_change_payment_method_page(): bool {
if ( isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return wcs_is_subscription( wc_clean( wp_unslash( $_GET['change_payment_method'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification
// phpcs:disable WordPress.Security.NonceVerification
if ( isset( $_GET['change_payment_method'] ) ) {
return wcs_is_subscription( wc_clean( wp_unslash( $_GET['change_payment_method'] ) ) );
}
return false;
@ -325,12 +326,14 @@ trait ContextTrait {
* @return bool
*/
protected function is_wc_settings_payments_tab(): bool {
if ( ! is_admin() || isset( $_GET['section'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
// phpcs:disable WordPress.Security.NonceVerification
if ( ! is_admin() || isset( $_GET['section'] ) ) {
return false;
}
$page = wc_clean( wp_unslash( $_GET['page'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification
$tab = wc_clean( wp_unslash( $_GET['tab'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification
$page = wc_clean( wp_unslash( $_GET['page'] ?? '' ) );
$tab = wc_clean( wp_unslash( $_GET['tab'] ?? '' ) );
// phpcs:enable WordPress.Security.NonceVerification
return $page === 'wc-settings' && $tab === 'checkout';
}

View file

@ -78,7 +78,7 @@ class EarlyOrderHandler {
*
* @return int|null
*/
public function determine_wc_order_id( int $value = null ) {
public function determine_wc_order_id( ?int $value = null ) {
if ( ! isset( $_REQUEST['ppcp-resume-order'] ) ) {
return $value;

View file

@ -90,7 +90,8 @@ class WooCommerceOrderCreator {
try {
$payer = $order->payer();
$shipping = $order->purchase_units()[0]->shipping();
$purchase_units = $order->purchase_units();
$shipping = ! empty( $purchase_units ) ? $purchase_units[0]->shipping() : null;
$this->configure_payment_source( $wc_order );
$this->configure_customer( $wc_order );
@ -133,6 +134,16 @@ class WooCommerceOrderCreator {
$item->set_product_id( $product_id );
$item->set_quantity( $quantity );
if ( isset( $cart_item['bundled_by'] ) ) {
$item->add_meta_data( '_bundled_by', $cart_item['bundled_by'], true );
}
if ( isset( $cart_item['bundled_item_id'] ) ) {
$item->add_meta_data( '_bundled_item_id', $cart_item['bundled_item_id'], true );
}
if ( isset( $cart_item['key'] ) ) {
$item->add_meta_data( '_bundle_cart_key', $cart_item['key'], true );
}
if ( $variation_id ) {
$item->set_variation_id( $variation_id );
$item->set_variation( $variation_attributes );
@ -143,14 +154,13 @@ class WooCommerceOrderCreator {
return;
}
$subtotal = wc_get_price_excluding_tax( $product, array( 'qty' => $quantity ) );
$subtotal = apply_filters( 'woocommerce_paypal_payments_shipping_callback_cart_line_item_total', $subtotal, $cart_item );
$subtotal = apply_filters( 'woocommerce_paypal_payments_shipping_callback_cart_line_item_total', $cart_item['line_subtotal'], $cart_item );
$item->set_name( $product->get_name() );
$item->set_subtotal( $subtotal );
$item->set_total( $subtotal );
$item->set_total( $cart_item['line_total'] );
$this->configure_taxes( $product, $item, $subtotal );
$this->configure_taxes( $product, $item, $item->get_total() );
$product_id = $product->get_id();

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\CardFields;
use DomainException;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder;
use WooCommerce\PayPalCommerce\CardFields\Service\CardCaptureValidator;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
@ -150,6 +151,15 @@ class CardFieldsModule implements ServiceModule, ExtendingModule, ExecutableModu
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$experience_context_builder = $c->get( 'wcgateway.builder.experience-context' );
assert( $experience_context_builder instanceof ExperienceContextBuilder );
$payment_source_data = array(
'experience_context' => $experience_context_builder
->with_endpoint_return_urls()
->build()->to_array(),
);
$three_d_secure_contingency =
$settings->has( '3d_secure_contingency' )
? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) )
@ -159,15 +169,15 @@ class CardFieldsModule implements ServiceModule, ExtendingModule, ExecutableModu
$three_d_secure_contingency === 'SCA_ALWAYS'
|| $three_d_secure_contingency === 'SCA_WHEN_REQUIRED'
) {
$data['payment_source']['card'] = array(
'attributes' => array(
$payment_source_data['attributes'] = array(
'verification' => array(
'method' => $three_d_secure_contingency,
),
),
);
}
$data['payment_source'] = array( 'card' => $payment_source_data );
return $data;
},
10,

View file

@ -20,7 +20,8 @@ trait AdminContextTrait {
* @return bool
*/
private function is_paypal_order_edit_page(): bool {
$post_id = wc_clean( wp_unslash( $_GET['id'] ?? $_GET['post'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// phpcs:ignore WordPress.Security.NonceVerification
$post_id = wc_clean( wp_unslash( $_GET['id'] ?? $_GET['post'] ?? '' ) );
if ( ! $post_id ) {
return false;
}

View file

@ -71,6 +71,7 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule {
$this->migrate_pay_later_settings( $c );
$this->migrate_smart_button_settings( $c );
$this->migrate_three_d_secure_setting();
$this->fix_page_builders();
$this->exclude_cache_plugins_js_minification( $c );
@ -274,6 +275,35 @@ class CompatModule implements ServiceModule, ExtendingModule, ExecutableModule {
);
}
/**
* Migrates the old Three D Secure setting located in PaymentSettings to the new location in SettingsModel.
*
* The migration will be done on plugin update if it hasn't already done.
*/
protected function migrate_three_d_secure_setting(): void {
add_action(
'woocommerce_paypal_payments_gateway_migrate_on_update',
function () {
$payment_settings = get_option( 'woocommerce-ppcp-data-payment' ) ?: array();
$data_settings = get_option( 'woocommerce-ppcp-data-settings' ) ?: array();
// Skip if payment settings don't have the setting but data settings do.
if ( ! isset( $payment_settings['three_d_secure'] ) || isset( $data_settings['three_d_secure'] ) ) {
return;
}
// Move the setting.
$data_settings['three_d_secure'] = $payment_settings['three_d_secure'];
unset( $payment_settings['three_d_secure'] );
// Save both.
update_option( 'woocommerce-ppcp-data-settings', $data_settings );
update_option( 'woocommerce-ppcp-data-payment', $payment_settings );
}
);
}
/**
* Changes the button rendering place for page builders
* that do not work well with our default places.

View file

@ -150,6 +150,8 @@ class SubscriptionsHandler {
return true;
}
// phpcs:disable WordPress.Security.NonceVerification
// Checks that require Subscriptions.
if ( class_exists( \WC_Subscriptions::class ) ) {
// My Account > Subscriptions > (Subscription).
@ -160,15 +162,15 @@ class SubscriptionsHandler {
}
// Changing payment method?
if ( is_wc_endpoint_url( 'order-pay' ) && isset( $_GET['change_payment_method'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( is_wc_endpoint_url( 'order-pay' ) && isset( $_GET['change_payment_method'] ) ) {
$subscription = wcs_get_subscription( absint( get_query_var( 'order-pay' ) ) );
return ( $subscription && PPECHelper::PPEC_GATEWAY_ID === $subscription->get_payment_method() );
}
// Early renew (via modal).
if ( isset( $_GET['process_early_renewal'], $_GET['subscription_id'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$subscription = wcs_get_subscription( absint( $_GET['subscription_id'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['process_early_renewal'], $_GET['subscription_id'] ) ) {
$subscription = wcs_get_subscription( absint( $_GET['subscription_id'] ) );
return ( $subscription && PPECHelper::PPEC_GATEWAY_ID === $subscription->get_payment_method() );
}
@ -185,7 +187,6 @@ class SubscriptionsHandler {
}
// Are we editing an order or subscription tied to PPEC?
// phpcs:ignore WordPress.Security.NonceVerification
$order_id = wc_clean( wp_unslash( $_GET['id'] ?? $_GET['post'] ?? $_POST['post_ID'] ?? '' ) );
if ( $order_id ) {
$order = wc_get_order( $order_id );
@ -199,9 +200,7 @@ class SubscriptionsHandler {
* @psalm-suppress UndefinedClass
*/
$post_type_or_page = class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled()
// phpcs:ignore WordPress.Security.NonceVerification
? wc_clean( wp_unslash( $_GET['page'] ?? '' ) )
// phpcs:ignore WordPress.Security.NonceVerification
: wc_clean( wp_unslash( $_GET['post_type'] ?? $_POST['post_type'] ?? '' ) );
if ( $post_type_or_page === 'shop_subscription' || $post_type_or_page === 'wc-orders--shop_subscription' ) {
return true;

View file

@ -11,7 +11,6 @@ namespace WooCommerce\PayPalCommerce\Compat\Settings;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Settings\Data\AbstractDataModel;
use WooCommerce\PayPalCommerce\Settings\Data\PaymentSettings;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
/**
@ -22,15 +21,6 @@ use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
*/
class PaymentMethodSettingsMapHelper {
/**
* A map of new to old 3d secure values.
*/
protected const THREE_D_SECURE_VALUES_MAP = array(
'no-3d-secure' => 'NO_3D_SECURE',
'only-required-3d-secure' => 'SCA_WHEN_REQUIRED',
'always-3d-secure' => 'SCA_ALWAYS',
);
/**
* Maps old setting keys to new payment method settings names.
*
@ -40,7 +30,6 @@ class PaymentMethodSettingsMapHelper {
return array(
'dcc_enabled' => CreditCardGateway::ID,
'axo_enabled' => AxoGateway::ID,
'3d_secure_contingency' => 'three_d_secure',
);
}
@ -52,17 +41,6 @@ class PaymentMethodSettingsMapHelper {
* @return mixed The value of the mapped setting, (null if not found).
*/
public function mapped_value( string $old_key, ?AbstractDataModel $payment_settings ) {
switch ( $old_key ) {
case '3d_secure_contingency':
if ( is_null( $payment_settings ) ) {
return null;
}
assert( $payment_settings instanceof PaymentSettings );
$selected_three_d_secure = $payment_settings->get_three_d_secure();
return self::THREE_D_SECURE_VALUES_MAP[ $selected_three_d_secure ] ?? null;
default:
$payment_method = $this->map()[ $old_key ] ?? false;
if ( ! $payment_method ) {
@ -71,7 +49,6 @@ class PaymentMethodSettingsMapHelper {
return $this->is_gateway_enabled( $payment_method );
}
}
/**
* Checks if the payment gateway with the given name is enabled.

View file

@ -23,6 +23,17 @@ class SettingsTabMapHelper {
use ContextTrait;
/**
* A map of new to old 3d secure values.
*
* @var array<string, string>
*/
public const THREE_D_SECURE_VALUES_MAP = array(
'no-3d-secure' => 'NO_3D_SECURE',
'only-required-3d-secure' => 'SCA_WHEN_REQUIRED',
'always-3d-secure' => 'SCA_ALWAYS',
);
/**
* Maps old setting keys to new setting keys.
*
@ -43,6 +54,7 @@ class SettingsTabMapHelper {
'blocks_final_review_enabled' => 'enable_pay_now',
'logging_enabled' => 'enable_logging',
'vault_enabled' => 'save_paypal_and_venmo',
'3d_secure_contingency' => 'three_d_secure',
);
}
@ -69,11 +81,30 @@ class SettingsTabMapHelper {
case 'blocks_final_review_enabled':
return $this->mapped_pay_now_value( $settings_model );
case '3d_secure_contingency':
return $this->mapped_3d_secure_value( $settings_model );
default:
return $settings_model[ $new_key ] ?? null;
}
}
/**
* Retrieves the mapped value for the '3d_secure_contingency' from the new settings.
*
* @param array $settings_model The new settings model data.
* @return string|null The mapped '3d_secure_contingency' setting value.
*/
protected function mapped_3d_secure_value( array $settings_model ): ?string {
$three_d_secure = $settings_model['three_d_secure'] ?? null;
if ( ! is_string( $three_d_secure ) ) {
return null;
}
return self::THREE_D_SECURE_VALUES_MAP[ $three_d_secure ] ?? null;
}
/**
* Retrieves the mapped value for the 'mismatch_behavior' from the new settings.
*

View file

@ -189,7 +189,8 @@ class GooglepayButton extends PaymentButton {
buttonConfig,
ppcpConfig,
contextHandler,
buttonAttributes
buttonAttributes,
onClick = null
) {
// Disable debug output in the browser console:
// buttonConfig.is_debug = false;
@ -200,12 +201,14 @@ class GooglepayButton extends PaymentButton {
buttonConfig,
ppcpConfig,
contextHandler,
buttonAttributes
buttonAttributes,
onClick
);
this.init = this.init.bind( this );
this.onPaymentDataChanged = this.onPaymentDataChanged.bind( this );
this.onButtonClick = this.onButtonClick.bind( this );
this.onClick = onClick;
this.log( 'Create instance' );
}
@ -552,6 +555,10 @@ class GooglepayButton extends PaymentButton {
const initiatePaymentRequest = async () => {
window.ppcpFundingSource = 'googlepay';
// Sets paymentMethodId from registerExpressPaymentMethod as active payment method.
this.onClick?.();
const paymentDataRequest = this.paymentDataRequest();
this.log(

View file

@ -3,11 +3,19 @@ import GooglepayButton from './GooglepayButton';
import ContextHandlerFactory from './Context/ContextHandlerFactory';
class GooglepayManager {
constructor( namespace, buttonConfig, ppcpConfig, buttonAttributes = {} ) {
constructor(
namespace,
buttonConfig,
ppcpConfig,
buttonAttributes = {},
onClick = null
) {
this.namespace = namespace;
this.buttonConfig = buttonConfig;
this.ppcpConfig = ppcpConfig;
this.buttonAttributes = buttonAttributes;
this.onClick = onClick;
this.googlePayConfig = null;
this.transactionInfo = null;
this.contextHandler = null;
@ -28,7 +36,8 @@ class GooglepayManager {
buttonConfig,
ppcpConfig,
this.contextHandler,
this.buttonAttributes
this.buttonAttributes,
this.onClick
);
this.buttons.push( button );

View file

@ -20,7 +20,7 @@ if ( typeof window.PayPalCommerceGateway === 'undefined' ) {
window.PayPalCommerceGateway = ppcpConfig;
}
const GooglePayComponent = ( { isEditing, buttonAttributes } ) => {
const GooglePayComponent = ( { isEditing, buttonAttributes, onClick } ) => {
const [ paypalLoaded, setPaypalLoaded ] = useState( false );
const [ googlePayLoaded, setGooglePayLoaded ] = useState( false );
const [ manager, setManager ] = useState( null );
@ -49,7 +49,8 @@ const GooglePayComponent = ( { isEditing, buttonAttributes } ) => {
namespace,
buttonConfig,
ppcpConfig,
buttonAttributes
buttonAttributes,
onClick
);
setManager( newManager );
}
@ -90,6 +91,7 @@ if ( buttonConfig?.is_enabled ) {
'woocommerce-paypal-payments'
),
gatewayId: 'ppcp-gateway',
paymentMethodId: 'ppcp-gateway',
label: <div dangerouslySetInnerHTML={ { __html: buttonData.title } } />,
content: <GooglePayComponent isEditing={ false } />,
edit: <GooglePayComponent isEditing={ true } />,

View file

@ -145,7 +145,9 @@ class UpdatePaymentDataEndpoint {
'description' => html_entity_decode(
wp_strip_all_tags(
wc_price( (float) $rate->get_cost(), array( 'currency' => get_woocommerce_currency() ) )
)
),
ENT_QUOTES,
'UTF-8'
),
'cost' => $rate->get_cost(),
);

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Googlepay;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExperienceContextBuilder;
use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint;
@ -52,7 +53,7 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
// Clears product status when appropriate.
add_action(
'woocommerce_paypal_payments_clear_apm_product_status',
function( Settings $settings = null ) use ( $c ): void {
function( ?Settings $settings = null ) use ( $c ): void {
$apm_status = $c->get( 'googlepay.helpers.apm-product-status' );
assert( $apm_status instanceof ApmProductStatus );
$apm_status->clear( $settings );
@ -261,6 +262,15 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$experience_context_builder = $c->get( 'wcgateway.builder.experience-context' );
assert( $experience_context_builder instanceof ExperienceContextBuilder );
$payment_source_data = array(
'experience_context' => $experience_context_builder
->with_endpoint_return_urls()
->build()->to_array(),
);
$three_d_secure_contingency =
$settings->has( '3d_secure_contingency' )
? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) )
@ -270,15 +280,15 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
$three_d_secure_contingency === 'SCA_ALWAYS'
|| $three_d_secure_contingency === 'SCA_WHEN_REQUIRED'
) {
$data['payment_source']['google_pay'] = array(
'attributes' => array(
$payment_source_data['attributes'] = array(
'verification' => array(
'method' => $three_d_secure_contingency,
),
),
);
}
$data['payment_source'] = array( 'google_pay' => $payment_source_data );
return $data;
},
10,

View file

@ -101,7 +101,7 @@ class ApmProductStatus extends ProductStatus {
}
/** {@inheritDoc} */
protected function clear_state( Settings $settings = null ) : void {
protected function clear_state( ?Settings $settings = null ) : void {
if ( null === $settings ) {
$settings = $this->settings;
}

View file

@ -86,7 +86,7 @@ class LocalApmProductStatus extends ProductStatus {
}
/** {@inheritDoc} */
protected function clear_state( Settings $settings = null ) : void {
protected function clear_state( ?Settings $settings = null ) : void {
if ( null === $settings ) {
$settings = $this->settings;
}

View file

@ -21,6 +21,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Helper\ConnectionState;
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
use WooCommerce\PayPalCommerce\WcGateway\Helper\MerchantDetails;
return array(
'api.paypal-host' => function( ContainerInterface $container ) : string {
@ -102,7 +103,12 @@ return array(
return $state->get_environment();
},
'settings.merchant-details' => static function ( ContainerInterface $container ) : MerchantDetails {
$woo_country = $container->get( 'api.shop.country' );
$eligibility_checks = $container->get( 'wcgateway.feature-eligibility.list' );
return new MerchantDetails( $woo_country, $woo_country, $eligibility_checks );
},
'onboarding.assets' => function( ContainerInterface $container ) : OnboardingAssets {
$state = $container->get( 'onboarding.state' );
$login_seller_endpoint = $container->get( 'onboarding.endpoint.login-seller' );

View file

@ -81,7 +81,7 @@ class OnboardingRenderer {
PartnerReferrals $sandbox_partner_referrals,
PartnerReferralsData $partner_referrals_data,
Cache $cache,
LoggerInterface $logger = null
?LoggerInterface $logger = null
) {
$this->settings = $settings;
$this->production_partner_referrals = $production_partner_referrals;

View file

@ -24,7 +24,8 @@ trait TrackingAvailabilityTrait {
* @return bool
*/
protected function is_tracking_enabled( Bearer $bearer ): bool {
$post_id = (int) wc_clean( wp_unslash( $_GET['id'] ?? $_GET['post'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
// phpcs:ignore WordPress.Security.NonceVerification
$post_id = (int) wc_clean( wp_unslash( $_GET['id'] ?? $_GET['post'] ?? '' ) );
if ( ! $post_id ) {
return false;
}

View file

@ -180,7 +180,18 @@ document.addEventListener( 'DOMContentLoaded', () => {
}
} );
jQuery( '.wc_input_subscription_price' ).trigger( 'change' );
const $productType = jQuery( '#product-type' );
const $subscriptionInput = jQuery( '.wc_input_subscription_price' );
if (
$productType.length &&
$subscriptionInput.length &&
[ 'subscription', 'variable-subscription' ].includes(
$productType.val()
)
) {
$subscriptionInput.trigger( 'change' );
}
const variationProductIds = [
PayPalCommerceGatewayPayPalSubscriptionProducts.product_id,

View file

@ -167,6 +167,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
return;
}
// phpcs:ignore WordPress.Security.NonceVerification
$nonce = wc_clean( wp_unslash( $_POST['_wcsnonce'] ?? '' ) );
if (
$subscriptions_mode !== 'subscriptions_api'
@ -250,6 +251,7 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
* @psalm-suppress MissingClosureParamType
*/
function( $variation_id ) use ( $c ) {
// phpcs:ignore WordPress.Security.NonceVerification
$wcsnonce_save_variations = wc_clean( wp_unslash( $_POST['_wcsnonce_save_variations'] ?? '' ) );
if (
@ -501,9 +503,10 @@ class PayPalSubscriptionsModule implements ServiceModule, ExtendingModule, Execu
if ( ! is_string( $hook ) || wcs_is_manual_renewal_enabled() ) {
return;
}
$settings = $c->get( 'wcgateway.settings' );
$subscription_mode = $settings->has( 'subscriptions_mode' ) ? $settings->get( 'subscriptions_mode' ) : '';
if ( $hook !== 'post.php' && $hook !== 'post-new.php' && $subscription_mode !== 'subscriptions_api' ) {
if ( ! in_array( $hook, array( 'post.php', 'post-new.php' ), true ) || $subscription_mode !== 'subscriptions_api' ) {
return;
}

View file

@ -78,6 +78,11 @@ return array(
'SE',
'GB',
'US',
'YT',
'RE',
'GP',
'GF',
'MQ',
)
);
},

View file

@ -12,21 +12,21 @@ namespace WooCommerce\PayPalCommerce\SavePaymentMethods;
use Psr\Log\LoggerInterface;
use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentSource;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentToken;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreatePaymentTokenForGuest;
use WooCommerce\PayPalCommerce\SavePaymentMethods\Endpoint\CreateSetupToken;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vaulting\WooCommercePaymentTokens;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
@ -69,10 +69,10 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
$billing_agreements_endpoint = $c->get( 'api.endpoint.billing-agreements' );
assert( $billing_agreements_endpoint instanceof BillingAgreementsEndpoint );
$reference_transaction_enabled = $billing_agreements_endpoint->reference_transaction_enabled();
if ( $reference_transaction_enabled !== true ) {
$reference_transaction_status = $c->get( 'api.reference-transaction-status' );
assert( $reference_transaction_status instanceof ReferenceTransactionStatus );
if ( ! $reference_transaction_status->reference_transaction_enabled() ) {
$settings->set( 'vault_enabled', false );
$settings->persist();
}
@ -115,87 +115,67 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
function ( array $data, string $payment_method, array $request_data ) use ( $c ): array {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if ( $payment_method === CreditCardGateway::ID ) {
if ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) ) {
return $data;
}
$save_payment_method = $request_data['save_payment_method'] ?? false;
if ( $save_payment_method ) {
$data['payment_source'] = array(
'card' => array(
'attributes' => array(
$new_attributes = array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
),
),
),
);
$target_customer_id = get_user_meta( get_current_user_id(), '_ppcp_target_customer_id', true );
if ( ! $target_customer_id ) {
$target_customer_id = get_user_meta( get_current_user_id(), 'ppcp_customer_id', true );
}
if ( $target_customer_id ) {
$data['payment_source']['card']['attributes']['customer'] = array(
$new_attributes['customer'] = array(
'id' => $target_customer_id,
);
}
}
$funding_source = (string) ( $request_data['funding_source'] ?? '' );
if ( $payment_method === CreditCardGateway::ID ) {
if ( ! $settings->has( 'vault_enabled_dcc' ) || ! $settings->get( 'vault_enabled_dcc' ) ) {
return $data;
}
if ( $payment_method === PayPalGateway::ID ) {
$save_payment_method = $request_data['save_payment_method'] ?? false;
if ( ! $save_payment_method ) {
return $data;
}
} elseif ( $payment_method === PayPalGateway::ID ) {
if ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) {
return $data;
}
$funding_source = $request_data['funding_source'] ?? null;
if ( ! in_array( $funding_source, array( 'paypal', 'venmo' ), true ) ) {
return $data;
}
if ( $funding_source && $funding_source === 'venmo' ) {
$data['payment_source'] = array(
'venmo' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
'usage_type' => 'MERCHANT',
'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ),
),
),
),
);
} elseif ( $funding_source && $funding_source === 'apple_pay' ) {
$data['payment_source'] = array(
'apple_pay' => array(
'stored_credential' => array(
'payment_initiator' => 'CUSTOMER',
'payment_type' => 'RECURRING',
),
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
),
),
),
);
$new_attributes['vault']['usage_type'] = 'MERCHANT';
$new_attributes['vault']['permit_multiple_payment_tokens'] = apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false );
} else {
$data['payment_source'] = array(
'paypal' => array(
'attributes' => array(
'vault' => array(
'store_in_vault' => 'ON_SUCCESS',
'usage_type' => 'MERCHANT',
'permit_multiple_payment_tokens' => apply_filters( 'woocommerce_paypal_payments_permit_multiple_payment_tokens', false ),
),
),
),
);
return $data;
}
$payment_source = (array) ( $data['payment_source'] ?? array() );
$key = array_key_first( $payment_source );
if ( ! is_string( $key ) || empty( $key ) ) {
$key = $payment_method;
if ( $payment_method === PayPalGateway::ID && $funding_source ) {
$key = $funding_source;
}
$payment_source[ $key ] = array();
}
$payment_source[ $key ] = (array) $payment_source[ $key ];
$attributes = (array) ( $payment_source[ $key ]['attributes'] ?? array() );
$payment_source[ $key ]['attributes'] = array_merge( $attributes, $new_attributes );
$data['payment_source'] = $payment_source;
return $data;
},
10,
20,
3
);
@ -305,7 +285,8 @@ class SavePaymentMethodsModule implements ServiceModule, ExtendingModule, Execut
? apply_filters( 'woocommerce_paypal_payments_three_d_secure_contingency', $settings->get( '3d_secure_contingency' ) )
: '';
$change_payment_method = wc_clean( wp_unslash( $_GET['change_payment_method'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification
// phpcs:ignore WordPress.Security.NonceVerification
$change_payment_method = wc_clean( wp_unslash( $_GET['change_payment_method'] ?? '' ) );
wp_localize_script(
'ppcp-add-payment-method',

View file

@ -69,3 +69,23 @@
margin-top: var(--block-action-gap, 16px);
}
}
.ppcp--notice {
display: block;
padding: 10px;
margin: 10px 0;
line-height: 1.5714285714;
font-size: 0.8125rem;
background: var(--notice-background);
color: var(--notice-text);
&.type--info {
--notice-background: var(--color-success-background);
--notice-text: var(--color-success-text);
}
&.type--error {
--notice-background: var(--color-failure-background);
--notice-text: var(--color-failure-text);
}
}

View file

@ -12,6 +12,10 @@
.ppcp-r-inner-container {
max-width: var(--max-width-onboarding-content);
&.ppcp--wide {
--max-width-onboarding-content: none;
}
}
.ppcp-r-payment-method--separator {

View file

@ -8,6 +8,8 @@ import OnboardingScreen from './Screens/Onboarding';
import SettingsScreen from './Screens/Settings';
import { getQuery, cleanUrlQueryParams } from '../utils/navigation';
import { initializeTracking } from '../services/tracking';
const SettingsApp = () => {
const { isReady: onboardingIsReady, completed: onboardingCompleted } =
OnboardingHooks.useSteps();
@ -16,6 +18,10 @@ const SettingsApp = () => {
merchant: { isSendOnlyCountry },
} = CommonHooks.useMerchantInfo();
useEffect( () => {
initializeTracking();
}, [] );
// Disable the "Changes you made might not be saved" browser warning.
useEffect( () => {
const suppressBeforeUnload = ( event ) => {

Some files were not shown because too many files have changed in this diff Show more