Merge branch 'trunk' into PCP-1544-pay-order-currency

This commit is contained in:
Alex P 2023-06-06 15:31:46 +03:00
commit d2a5ecf3b8
No known key found for this signature in database
GPG key ID: 54487A734A204D71
125 changed files with 9483 additions and 789 deletions

View file

@ -18,7 +18,7 @@ hooks:
pre-start: pre-start:
- exec-host: "mkdir -p .ddev/wordpress/wp-content/plugins/${DDEV_PROJECT}" - exec-host: "mkdir -p .ddev/wordpress/wp-content/plugins/${DDEV_PROJECT}"
web_environment: web_environment:
- WP_VERSION=5.9.3 - WP_VERSION=6.2.2
- WP_LOCALE=en_US - WP_LOCALE=en_US
- WP_TITLE=WooCommerce PayPal Payments - WP_TITLE=WooCommerce PayPal Payments
- WP_MULTISITE=true - WP_MULTISITE=true
@ -26,7 +26,8 @@ web_environment:
- ADMIN_USER=admin - ADMIN_USER=admin
- ADMIN_PASS=admin - ADMIN_PASS=admin
- ADMIN_EMAIL=admin@example.com - ADMIN_EMAIL=admin@example.com
- WC_VERSION=6.1.0 - WC_VERSION=7.7.2
- PCP_BLOCKS_ENABLED=1
# Key features of ddev's config.yaml: # Key features of ddev's config.yaml:

View file

@ -1,11 +1,35 @@
PPCP_E2E_WP_DIR=${ROOT_DIR}/.ddev/wordpress PPCP_E2E_WP_DIR=${ROOT_DIR}/.ddev/wordpress
BASEURL="https://woocommerce-paypal-payments.ddev.site" BASEURL="https://woocommerce-paypal-payments.ddev.site"
AUTHORIZATION="Bearer ABC123"
CHECKOUT_URL="/checkout"
CHECKOUT_PAGE_ID=7
CART_URL="/cart"
BLOCK_CHECKOUT_URL="/checkout-block"
BLOCK_CHECKOUT_PAGE_ID=22
BLOCK_CART_URL="/cart-block"
PRODUCT_URL="/product/prod" PRODUCT_URL="/product/prod"
PRODUCT_ID=123
SUBSCRIPTION_URL="/product/sub"
WP_MERCHANT_USER="admin"
WP_MERCHANT_PASSWORD="admin"
WP_CUSTOMER_USER="customer"
WP_CUSTOMER_PASSWORD="password"
CUSTOMER_EMAIL="customer@example.com" CUSTOMER_EMAIL="customer@example.com"
CUSTOMER_PASSWORD="password" CUSTOMER_PASSWORD="password"
CUSTOMER_FIRST_NAME="John"
CUSTOMER_LAST_NAME="Doe"
CUSTOMER_COUNTRY="DE"
CUSTOMER_ADDRESS="street 1"
CUSTOMER_POSTCODE="12345"
CUSTOMER_CITY="city"
CUSTOMER_PHONE="1234567890"
CREDIT_CARD_NUMBER="1234567890" CREDIT_CARD_NUMBER="1234567890"
CREDIT_CARD_EXPIRATION="01/2042" CREDIT_CARD_EXPIRATION="01/2042"

View file

@ -7,11 +7,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
php-versions: ['7.2', '7.4', '8.1'] php-versions: ['7.3', '7.4', '8.1']
wc-versions: ['5.9.5', '7.1.0'] wc-versions: ['5.9.5', '7.7.2']
exclude:
- php-versions: 7.2
wc-versions: 7.1.0
name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }} name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }}
steps: steps:
@ -24,8 +21,10 @@ jobs:
- uses: actions/checkout@v1 - uses: actions/checkout@v1
- name: Configure DDEV - name: Configure DDEV PHP
run: ddev config --php-version ${{ matrix.php-versions }} --web-environment-add="WC_VERSION=${{ matrix.wc-versions }}" run: ddev config --php-version ${{ matrix.php-versions }}
- name: Configure DDEV WC
run: ddev config --web-environment-add="WC_VERSION=${{ matrix.wc-versions }}"
- name: Start DDEV - name: Start DDEV
run: ddev start run: ddev start

381
.psalm/wcblocks.php Normal file
View file

@ -0,0 +1,381 @@
<?php
namespace Automattic\WooCommerce\Blocks\Integrations {
/**
* Integration.Interface
*
* Integrations must use this interface when registering themselves with blocks,
*/
interface IntegrationInterface
{
/**
* The name of the integration.
*
* @return string
*/
public function get_name();
/**
* When called invokes any initialization/setup for the integration.
*/
public function initialize();
/**
* Returns an array of script handles to enqueue in the frontend context.
*
* @return string[]
*/
public function get_script_handles();
/**
* Returns an array of script handles to enqueue in the editor context.
*
* @return string[]
*/
public function get_editor_script_handles();
/**
* An array of key, value pairs of data made available to the block on the client side.
*
* @return array
*/
public function get_script_data();
}
}
namespace Automattic\WooCommerce\Blocks\Payments {
use Automattic\WooCommerce\Blocks\Integrations\IntegrationInterface;
interface PaymentMethodTypeInterface extends IntegrationInterface
{
/**
* Returns if this payment method should be active. If false, the scripts will not be enqueued.
*
* @return boolean
*/
public function is_active();
/**
* Returns an array of script handles to enqueue for this payment method in
* the frontend context
*
* @return string[]
*/
public function get_payment_method_script_handles();
/**
* Returns an array of script handles to enqueue for this payment method in
* the admin context
*
* @return string[]
*/
public function get_payment_method_script_handles_for_admin();
/**
* An array of key, value pairs of data made available to payment methods
* client side.
*
* @return array
*/
public function get_payment_method_data();
/**
* Get array of supported features.
*
* @return string[]
*/
public function get_supported_features();
}
}
namespace Automattic\WooCommerce\Blocks\Payments\Integrations
{
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodTypeInterface;
/**
* AbstractPaymentMethodType class.
*
* @since 2.6.0
*/
abstract class AbstractPaymentMethodType implements PaymentMethodTypeInterface
{
/**
* Payment method name defined by payment methods extending this class.
*
* @var string
*/
protected $name = '';
/**
* Settings from the WP options table
*
* @var array
*/
protected $settings = [];
/**
* Get a setting from the settings array if set.
*
* @param string $name Setting name.
* @param mixed $default Value that is returned if the setting does not exist.
* @return mixed
*/
protected function get_setting($name, $default = '')
{
}
/**
* Returns the name of the payment method.
*/
public function get_name()
{
}
/**
* Returns if this payment method should be active. If false, the scripts will not be enqueued.
*
* @return boolean
*/
public function is_active()
{
}
/**
* Returns an array of script handles to enqueue for this payment method in
* the frontend context
*
* @return string[]
*/
public function get_payment_method_script_handles()
{
}
/**
* Returns an array of script handles to enqueue for this payment method in
* the admin context
*
* @return string[]
*/
public function get_payment_method_script_handles_for_admin()
{
}
/**
* Returns an array of supported features.
*
* @return string[]
*/
public function get_supported_features()
{
}
/**
* An array of key, value pairs of data made available to payment methods
* client side.
*
* @return array
*/
public function get_payment_method_data()
{
}
/**
* Returns an array of script handles to enqueue in the frontend context.
*
* Alias of get_payment_method_script_handles. Defined by IntegrationInterface.
*
* @return string[]
*/
public function get_script_handles()
{
}
/**
* Returns an array of script handles to enqueue in the admin context.
*
* Alias of get_payment_method_script_handles_for_admin. Defined by IntegrationInterface.
*
* @return string[]
*/
public function get_editor_script_handles()
{
}
/**
* An array of key, value pairs of data made available to the block on the client side.
*
* Alias of get_payment_method_data. Defined by IntegrationInterface.
*
* @return array
*/
public function get_script_data()
{
}
}
}
namespace Automattic\WooCommerce\Blocks\Integrations {
/**
* Class used for tracking registered integrations with various Block types.
*/
class IntegrationRegistry
{
/**
* Integration identifier is used to construct hook names and is given when the integration registry is initialized.
*
* @var string
*/
protected $registry_identifier = '';
/**
* Registered integrations, as `$name => $instance` pairs.
*
* @var IntegrationInterface[]
*/
protected $registered_integrations = [];
/**
* Initializes all registered integrations.
*
* Integration identifier is used to construct hook names and is given when the integration registry is initialized.
*
* @param string $registry_identifier Identifier for this registry.
*/
public function initialize($registry_identifier = '')
{
}
/**
* Registers an integration.
*
* @param IntegrationInterface $integration An instance of IntegrationInterface.
*
* @return boolean True means registered successfully.
*/
public function register(IntegrationInterface $integration)
{
}
/**
* Checks if an integration is already registered.
*
* @param string $name Integration name.
* @return bool True if the integration is registered, false otherwise.
*/
public function is_registered($name)
{
}
/**
* Un-register an integration.
*
* @param string|IntegrationInterface $name Integration name, or alternatively a IntegrationInterface instance.
* @return boolean|IntegrationInterface Returns the unregistered integration instance if unregistered successfully.
*/
public function unregister($name)
{
}
/**
* Retrieves a registered Integration by name.
*
* @param string $name Integration name.
* @return IntegrationInterface|null The registered integration, or null if it is not registered.
*/
public function get_registered($name)
{
}
/**
* Retrieves all registered integrations.
*
* @return IntegrationInterface[]
*/
public function get_all_registered()
{
}
/**
* Gets an array of all registered integration's script handles for the editor.
*
* @return string[]
*/
public function get_all_registered_editor_script_handles()
{
}
/**
* Gets an array of all registered integration's script handles.
*
* @return string[]
*/
public function get_all_registered_script_handles()
{
}
/**
* Gets an array of all registered integration's script data.
*
* @return array
*/
public function get_all_registered_script_data()
{
}
}
}
namespace Automattic\WooCommerce\Blocks\Payments {
use Automattic\WooCommerce\Blocks\Integrations\IntegrationRegistry;
/**
* Class used for interacting with payment method types.
*
* @since 2.6.0
*/
final class PaymentMethodRegistry extends IntegrationRegistry
{
/**
* Integration identifier is used to construct hook names and is given when the integration registry is initialized.
*
* @var string
*/
protected $registry_identifier = 'payment_method_type';
/**
* Retrieves all registered payment methods that are also active.
*
* @return PaymentMethodTypeInterface[]
*/
public function get_all_active_registered()
{
}
/**
* Gets an array of all registered payment method script handles, but only for active payment methods.
*
* @return string[]
*/
public function get_all_active_payment_method_script_dependencies()
{
}
/**
* Gets an array of all registered payment method script data, but only for active payment methods.
*
* @return array
*/
public function get_all_registered_script_data()
{
}
}
}
/**
* Registers and validates payment requirements callbacks.
*
* @see Automattic\WooCommerce\StoreApi\Schemas\ExtendSchema::register_payment_requirements()
*
* @param array $args Args to pass to register_payment_requirements.
* @returns boolean|\WP_Error True on success, WP_Error on fail.
*/
function woocommerce_store_api_register_payment_requirements( $args ) {
}

View file

@ -1631,7 +1631,7 @@ function wcs_get_order_items_product_id($item_id)
* *
* When acting on cart items or order items, Subscriptions often needs to use an item's canonical product ID. For * When acting on cart items or order items, Subscriptions often needs to use an item's canonical product ID. For
* items representing a variation, that means the 'variation_id' value, if the item is not a variation, that means * items representing a variation, that means the 'variation_id' value, if the item is not a variation, that means
* the 'product_id value. This function helps save keystrokes on the idiom to check if an item is to a variation or not. * the product_id value. This function helps save keystrokes on the idiom to check if an item is to a variation or not.
* *
* @param array or object $item Either a cart item, order/subscription line item, or a product. * @param array or object $item Either a cart item, order/subscription line item, or a product.
*/ */

651
.psalm/wpcli.php Normal file
View file

@ -0,0 +1,651 @@
<?php
/**
* Various utilities for WP-CLI commands.
*/
class WP_CLI {
/**
* Set the logger instance.
*
* @param object $logger Logger instance to set.
*/
public static function set_logger( $logger ) {
}
/**
* Get the logger instance.
*
* @return object $logger Logger instance.
*/
public static function get_logger() {
}
/**
* Get the Configurator instance
*
* @return Configurator
*/
public static function get_configurator() {
}
public static function get_root_command() {
}
public static function get_runner() {
}
/**
* @return FileCache
*/
public static function get_cache() {
}
/**
* Set the context in which WP-CLI should be run
*/
public static function set_url( $url ) {
}
/**
* @return WpHttpCacheManager
*/
public static function get_http_cache_manager() {
}
/**
* Colorize a string for output.
*
* Yes, you can change the color of command line text too. For instance,
* here's how `WP_CLI::success()` colorizes "Success: "
*
* ```
* WP_CLI::colorize( "%GSuccess:%n " )
* ```
*
* Uses `\cli\Colors::colorize()` to transform color tokens to display
* settings. Choose from the following tokens (and note 'reset'):
*
* * %y => ['color' => 'yellow'],
* * %g => ['color' => 'green'],
* * %b => ['color' => 'blue'],
* * %r => ['color' => 'red'],
* * %p => ['color' => 'magenta'],
* * %m => ['color' => 'magenta'],
* * %c => ['color' => 'cyan'],
* * %w => ['color' => 'grey'],
* * %k => ['color' => 'black'],
* * %n => ['color' => 'reset'],
* * %Y => ['color' => 'yellow', 'style' => 'bright'],
* * %G => ['color' => 'green', 'style' => 'bright'],
* * %B => ['color' => 'blue', 'style' => 'bright'],
* * %R => ['color' => 'red', 'style' => 'bright'],
* * %P => ['color' => 'magenta', 'style' => 'bright'],
* * %M => ['color' => 'magenta', 'style' => 'bright'],
* * %C => ['color' => 'cyan', 'style' => 'bright'],
* * %W => ['color' => 'grey', 'style' => 'bright'],
* * %K => ['color' => 'black', 'style' => 'bright'],
* * %N => ['color' => 'reset', 'style' => 'bright'],
* * %3 => ['background' => 'yellow'],
* * %2 => ['background' => 'green'],
* * %4 => ['background' => 'blue'],
* * %1 => ['background' => 'red'],
* * %5 => ['background' => 'magenta'],
* * %6 => ['background' => 'cyan'],
* * %7 => ['background' => 'grey'],
* * %0 => ['background' => 'black'],
* * %F => ['style' => 'blink'],
* * %U => ['style' => 'underline'],
* * %8 => ['style' => 'inverse'],
* * %9 => ['style' => 'bright'],
* * %_ => ['style' => 'bright']
*
* @access public
* @category Output
*
* @param string $string String to colorize for output, with color tokens.
* @return string Colorized string.
*/
public static function colorize( $string ) {
}
/**
* Schedule a callback to be executed at a certain point.
*
* Hooks conceptually are very similar to WordPress actions. WP-CLI hooks
* are typically called before WordPress is loaded.
*
* WP-CLI hooks include:
*
* * `before_add_command:<command>` - Before the command is added.
* * `after_add_command:<command>` - After the command was added.
* * `before_invoke:<command>` (1) - Just before a command is invoked.
* * `after_invoke:<command>` (1) - Just after a command is invoked.
* * `find_command_to_run_pre` - Just before WP-CLI finds the command to run.
* * `before_registering_contexts` (1) - Before the contexts are registered.
* * `before_wp_load` - Just before the WP load process begins.
* * `before_wp_config_load` - After wp-config.php has been located.
* * `after_wp_config_load` - After wp-config.php has been loaded into scope.
* * `after_wp_load` - Just after the WP load process has completed.
* * `before_run_command` (3) - Just before the command is executed.
*
* The parentheses behind the hook name denote the number of arguments
* being passed into the hook. For such hooks, the callback should return
* the first argument again, making them work like a WP filter.
*
* WP-CLI commands can create their own hooks with `WP_CLI::do_hook()`.
*
* If additional arguments are passed through the `WP_CLI::do_hook()` call,
* these will be passed on to the callback provided by `WP_CLI::add_hook()`.
*
* ```
* # `wp network meta` confirms command is executing in multisite context.
* WP_CLI::add_command( 'network meta', 'Network_Meta_Command', array(
* 'before_invoke' => function ( $name ) {
* if ( !is_multisite() ) {
* WP_CLI::error( 'This is not a multisite installation.' );
* }
* }
* ) );
* ```
*
* @access public
* @category Registration
*
* @param string $when Identifier for the hook.
* @param mixed $callback Callback to execute when hook is called.
* @return null
*/
public static function add_hook( $when, $callback ) {
}
/**
* Execute callbacks registered to a given hook.
*
* See `WP_CLI::add_hook()` for details on WP-CLI's internal hook system.
* Commands can provide and call their own hooks.
*
* @access public
* @category Registration
*
* @param string $when Identifier for the hook.
* @param mixed ...$args Optional. Arguments that will be passed onto the
* callback provided by `WP_CLI::add_hook()`.
* @return null|mixed Returns the first optional argument if optional
* arguments were passed, otherwise returns null.
*/
public static function do_hook( $when, ...$args ) {
}
/**
* Add a callback to a WordPress action or filter.
*
* `add_action()` without needing access to `add_action()`. If WordPress is
* already loaded though, you should use `add_action()` (and `add_filter()`)
* instead.
*
* @access public
* @category Registration
*
* @param string $tag Named WordPress action or filter.
* @param mixed $function_to_add Callable to execute when the action or filter is evaluated.
* @param integer $priority Priority to add the callback as.
* @param integer $accepted_args Number of arguments to pass to callback.
* @return true
*/
public static function add_wp_hook( $tag, $function_to_add, $priority = 10, $accepted_args = 1 ) {
}
/**
* Register a command to WP-CLI.
*
* WP-CLI supports using any callable class, function, or closure as a
* command. `WP_CLI::add_command()` is used for both internal and
* third-party command registration.
*
* Command arguments are parsed from PHPDoc by default, but also can be
* supplied as an optional third argument during registration.
*
* ```
* # Register a custom 'foo' command to output a supplied positional param.
* #
* # $ wp foo bar --append=qux
* # Success: bar qux
*
* /**
* * My awesome closure command
* *
* * <message>
* * : An awesome message to display
* *
* * --append=<message>
* * : An awesome message to append to the original message.
* *
* * @when before_wp_load
* *\/
* $foo = function( $args, $assoc_args ) {
* WP_CLI::success( $args[0] . ' ' . $assoc_args['append'] );
* };
* WP_CLI::add_command( 'foo', $foo );
* ```
*
* @access public
* @category Registration
*
* @param string $name Name for the command (e.g. "post list" or "site empty").
* @param callable $callable Command implementation as a class, function or closure.
* @param array $args {
* Optional. An associative array with additional registration parameters.
*
* @type callable $before_invoke Callback to execute before invoking the command.
* @type callable $after_invoke Callback to execute after invoking the command.
* @type string $shortdesc Short description (80 char or less) for the command.
* @type string $longdesc Description of arbitrary length for examples, etc.
* @type string $synopsis The synopsis for the command (string or array).
* @type string $when Execute callback on a named WP-CLI hook (e.g. before_wp_load).
* @type bool $is_deferred Whether the command addition had already been deferred.
* }
* @return bool True on success, false if deferred, hard error if registration failed.
*/
public static function add_command( $name, $callable, $args = [] ) {
}
/**
* Get the list of outstanding deferred command additions.
*
* @return array Array of outstanding command additions.
*/
public static function get_deferred_additions() {
}
/**
* Remove a command addition from the list of outstanding deferred additions.
*/
public static function remove_deferred_addition( $name ) {
}
/**
* Display informational message without prefix, and ignore `--quiet`.
*
* Message is written to STDOUT. `WP_CLI::log()` is typically recommended;
* `WP_CLI::line()` is included for historical compat.
*
* @access public
* @category Output
*
* @param string $message Message to display to the end user.
* @return null
*/
public static function line( $message = '' ) {
}
/**
* Display informational message without prefix.
*
* Message is written to STDOUT, or discarded when `--quiet` flag is supplied.
*
* ```
* # `wp cli update` lets user know of each step in the update process.
* WP_CLI::log( sprintf( 'Downloading from %s...', $download_url ) );
* ```
*
* @access public
* @category Output
*
* @param string $message Message to write to STDOUT.
*/
public static function log( $message ) {
}
/**
* Display success message prefixed with "Success: ".
*
* Success message is written to STDOUT.
*
* Typically recommended to inform user of successful script conclusion.
*
* ```
* # wp rewrite flush expects 'rewrite_rules' option to be set after flush.
* flush_rewrite_rules( \WP_CLI\Utils\get_flag_value( $assoc_args, 'hard' ) );
* if ( ! get_option( 'rewrite_rules' ) ) {
* WP_CLI::warning( "Rewrite rules are empty." );
* } else {
* WP_CLI::success( 'Rewrite rules flushed.' );
* }
* ```
*
* @access public
* @category Output
*
* @param string $message Message to write to STDOUT.
* @return null
*/
public static function success( $message ) {
}
/**
* Display debug message prefixed with "Debug: " when `--debug` is used.
*
* Debug message is written to STDERR, and includes script execution time.
*
* Helpful for optionally showing greater detail when needed. Used throughout
* WP-CLI bootstrap process for easier debugging and profiling.
*
* ```
* # Called in `WP_CLI\Runner::set_wp_root()`.
* private static function set_wp_root( $path ) {
* define( 'ABSPATH', Utils\trailingslashit( $path ) );
* WP_CLI::debug( 'ABSPATH defined: ' . ABSPATH );
* $_SERVER['DOCUMENT_ROOT'] = realpath( $path );
* }
*
* # Debug details only appear when `--debug` is used.
* # $ wp --debug
* # [...]
* # Debug: ABSPATH defined: /srv/www/wordpress-develop.dev/src/ (0.225s)
* ```
*
* @access public
* @category Output
*
* @param string|WP_Error|Exception|Throwable $message Message to write to STDERR.
* @param string|bool $group Organize debug message to a specific group.
* Use `false` to not group the message.
* @return null
*/
public static function debug( $message, $group = false ) {
}
/**
* Display warning message prefixed with "Warning: ".
*
* Warning message is written to STDERR.
*
* Use instead of `WP_CLI::debug()` when script execution should be permitted
* to continue.
*
* ```
* # `wp plugin activate` skips activation when plugin is network active.
* $status = $this->get_status( $plugin->file );
* // Network-active is the highest level of activation status
* if ( 'active-network' === $status ) {
* WP_CLI::warning( "Plugin '{$plugin->name}' is already network active." );
* continue;
* }
* ```
*
* @access public
* @category Output
*
* @param string|WP_Error|Exception|Throwable $message Message to write to STDERR.
* @return null
*/
public static function warning( $message ) {
}
/**
* Display error message prefixed with "Error: " and exit script.
*
* Error message is written to STDERR. Defaults to halting script execution
* with return code 1.
*
* Use `WP_CLI::warning()` instead when script execution should be permitted
* to continue.
*
* ```
* # `wp cache flush` considers flush failure to be a fatal error.
* if ( false === wp_cache_flush() ) {
* WP_CLI::error( 'The object cache could not be flushed.' );
* }
* ```
*
* @access public
* @category Output
*
* @param string|WP_Error|Exception|Throwable $message Message to write to STDERR.
* @param boolean|integer $exit True defaults to exit(1).
* @return null
*/
public static function error( $message, $exit = true ) {
}
/**
* Halt script execution with a specific return code.
*
* Permits script execution to be overloaded by `WP_CLI::runcommand()`
*
* @access public
* @category Output
*
* @param integer $return_code
* @return never
*/
public static function halt( $return_code ) {
}
/**
* Display a multi-line error message in a red box. Doesn't exit script.
*
* Error message is written to STDERR.
*
* @access public
* @category Output
*
* @param array $message_lines Multi-line error message to be displayed.
*/
public static function error_multi_line( $message_lines ) {
}
/**
* Ask for confirmation before running a destructive operation.
*
* If 'y' is provided to the question, the script execution continues. If
* 'n' or any other response is provided to the question, script exits.
*
* ```
* # `wp db drop` asks for confirmation before dropping the database.
*
* WP_CLI::confirm( "Are you sure you want to drop the database?", $assoc_args );
* ```
*
* @access public
* @category Input
*
* @param string $question Question to display before the prompt.
* @param array $assoc_args Skips prompt if 'yes' is provided.
*/
public static function confirm( $question, $assoc_args = [] ) {
}
/**
* Read value from a positional argument or from STDIN.
*
* @param array $args The list of positional arguments.
* @param int $index At which position to check for the value.
*
* @return string
*/
public static function get_value_from_arg_or_stdin( $args, $index ) {
}
/**
* Read a value, from various formats.
*
* @access public
* @category Input
*
* @param mixed $raw_value
* @param array $assoc_args
*/
public static function read_value( $raw_value, $assoc_args = [] ) {
}
/**
* Display a value, in various formats
*
* @param mixed $value Value to display.
* @param array $assoc_args Arguments passed to the command, determining format.
*/
public static function print_value( $value, $assoc_args = [] ) {
}
/**
* Convert a WP_Error or Exception into a string
*
* @param string|WP_Error|Exception|Throwable $errors
* @throws InvalidArgumentException
*
* @return string
*/
public static function error_to_string( $errors ) {
}
/**
* Launch an arbitrary external process that takes over I/O.
*
* ```
* # `wp core download` falls back to the `tar` binary when PharData isn't available
* if ( ! class_exists( 'PharData' ) ) {
* $cmd = "tar xz --strip-components=1 --directory=%s -f $tarball";
* WP_CLI::launch( Utils\esc_cmd( $cmd, $dest ) );
* return;
* }
* ```
*
* @access public
* @category Execution
*
* @param string $command External process to launch.
* @param boolean $exit_on_error Whether to exit if the command returns an elevated return code.
* @param boolean $return_detailed Whether to return an exit status (default) or detailed execution results.
* @return int|ProcessRun The command exit status, or a ProcessRun object for full details.
*/
public static function launch( $command, $exit_on_error = true, $return_detailed = false ) {
}
/**
* Run a WP-CLI command in a new process reusing the current runtime arguments.
*
* Use `WP_CLI::runcommand()` instead, which is easier to use and works better.
*
* Note: While this command does persist a limited set of runtime arguments,
* it *does not* persist environment variables. Practically speaking, WP-CLI
* packages won't be loaded when using WP_CLI::launch_self() because the
* launched process doesn't have access to the current process $HOME.
*
* @access public
* @category Execution
*
* @param string $command WP-CLI command to call.
* @param array $args Positional arguments to include when calling the command.
* @param array $assoc_args Associative arguments to include when calling the command.
* @param bool $exit_on_error Whether to exit if the command returns an elevated return code.
* @param bool $return_detailed Whether to return an exit status (default) or detailed execution results.
* @param array $runtime_args Override one or more global args (path,url,user,allow-root)
* @return int|ProcessRun The command exit status, or a ProcessRun instance
*/
public static function launch_self( $command, $args = [], $assoc_args = [], $exit_on_error = true, $return_detailed = false, $runtime_args = [] ) {
}
/**
* Get the path to the PHP binary used when executing WP-CLI.
*
* Environment values permit specific binaries to be indicated.
*
* Note: moved to Utils, left for BC.
*
* @access public
* @category System
*
* @return string
*/
public static function get_php_binary() {
}
/**
* Confirm that a global configuration parameter does exist.
*
* @access public
* @category Input
*
* @param string $key Config parameter key to check.
*
* @return bool
*/
public static function has_config( $key ) {
}
/**
* Get values of global configuration parameters.
*
* Provides access to `--path=<path>`, `--url=<url>`, and other values of
* the [global configuration parameters](https://wp-cli.org/config/).
*
* ```
* WP_CLI::log( 'The --url=<url> value is: ' . WP_CLI::get_config( 'url' ) );
* ```
*
* @access public
* @category Input
*
* @param string $key Get value for a specific global configuration parameter.
* @return mixed
*/
public static function get_config( $key = null ) {
}
/**
* Run a WP-CLI command.
*
* Launches a new child process to run a specified WP-CLI command.
* Optionally:
*
* * Run the command in an existing process.
* * Prevent halting script execution on error.
* * Capture and return STDOUT, or full details about command execution.
* * Parse JSON output if the command rendered it.
*
* ```
* $options = array(
* 'return' => true, // Return 'STDOUT'; use 'all' for full object.
* 'parse' => 'json', // Parse captured STDOUT to JSON array.
* 'launch' => false, // Reuse the current process.
* 'exit_error' => true, // Halt script execution on error.
* );
* $plugins = WP_CLI::runcommand( 'plugin list --format=json', $options );
* ```
*
* @access public
* @category Execution
*
* @param string $command WP-CLI command to run, including arguments.
* @param array $options Configuration options for command execution.
* @return mixed
*/
public static function runcommand( $command, $options = [] ) {
}
/**
* Run a given command within the current process using the same global
* parameters.
*
* Use `WP_CLI::runcommand()` instead, which is easier to use and works better.
*
* To run a command using a new process with the same global parameters,
* use WP_CLI::launch_self(). To run a command using a new process with
* different global parameters, use WP_CLI::launch().
*
* ```
* ob_start();
* WP_CLI::run_command( array( 'cli', 'cmd-dump' ) );
* $ret = ob_get_clean();
* ```
*
* @access public
* @category Execution
*
* @param array $args Positional arguments including command name.
* @param array $assoc_args
*/
public static function run_command( $args, $assoc_args = [] ) {
}
}

View file

@ -1,5 +1,22 @@
*** Changelog *** *** Changelog ***
= 2.1.0 - TBD =
* Fix - Performance issue #1182
* Fix - Webhooks not registered when onboarding with manual credentials #1223
* Fix - Boolean false type sent as empty value when setting cache #1313
* Fix - Ajax vulnerabilities #1411
* Enhancement - Save and display vaulted payment methods in WC Payment Token API #1059
* Enhancement - Cache webhook verification results #1379
* Enhancement - Refresh checkout totals after validation if needed #1294
* Enhancement - Improve Divi and Elementor Pro compatibility #1254
* Enhancement - Add MX and JP to ACDC #1415
* Enhancement - Add fraudnet script to SGO filter #1366
* Feature preview - Add express cart/checkout block #1346
* Feature preview - Integrate PayPal Subscriptions API #1217
= 2.0.5 - 2023-05-31 =
* Fix - Potential invalidation of merchant credentials #1339
= 2.0.4 - 2023-04-03 = = 2.0.4 - 2023-04-03 =
* Fix - Allow Pay Later in mini-cart #1221 * Fix - Allow Pay Later in mini-cart #1221
* Fix - Duplicated auth error when credentials become wrong #1229 * Fix - Duplicated auth error when credentials become wrong #1229

View file

@ -28,5 +28,12 @@ return function ( string $root_dir ): iterable {
( require "$modules_dir/ppcp-uninstall/module.php" )(), ( require "$modules_dir/ppcp-uninstall/module.php" )(),
); );
if ( apply_filters(
'woocommerce_paypal_payments_blocks_enabled',
getenv( 'PCP_BLOCKS_ENABLED' ) === '1'
) ) {
$modules[] = ( require "$modules_dir/ppcp-blocks/module.php" )();
}
return $modules; return $modules;
}; };

View file

@ -9,6 +9,14 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient; namespace WooCommerce\PayPalCommerce\ApiClient;
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\PlanFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
@ -209,6 +217,30 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
'api.endpoint.catalog-products' => static function ( ContainerInterface $container ): CatalogProducts {
return new CatalogProducts(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'api.factory.product' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.billing-plans' => static function( ContainerInterface $container ): BillingPlans {
return new BillingPlans(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'api.factory.billing-cycle' ),
$container->get( 'api.factory.plan' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.endpoint.billing-subscriptions' => static function( ContainerInterface $container ): BillingSubscriptions {
return new BillingSubscriptions(
$container->get( 'api.host' ),
$container->get( 'api.bearer' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'api.repository.application-context' => static function( ContainerInterface $container ) : ApplicationContextRepository { 'api.repository.application-context' => static function( ContainerInterface $container ) : ApplicationContextRepository {
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
@ -289,12 +321,19 @@ return array(
); );
}, },
'api.factory.shipping' => static function ( ContainerInterface $container ): ShippingFactory { 'api.factory.shipping' => static function ( ContainerInterface $container ): ShippingFactory {
$address_factory = $container->get( 'api.factory.address' ); return new ShippingFactory(
return new ShippingFactory( $address_factory ); $container->get( 'api.factory.address' ),
$container->get( 'api.factory.shipping-option' )
);
}, },
'api.factory.shipping-preference' => static function ( ContainerInterface $container ): ShippingPreferenceFactory { 'api.factory.shipping-preference' => static function ( ContainerInterface $container ): ShippingPreferenceFactory {
return new ShippingPreferenceFactory(); return new ShippingPreferenceFactory();
}, },
'api.factory.shipping-option' => static function ( ContainerInterface $container ): ShippingOptionFactory {
return new ShippingOptionFactory(
$container->get( 'api.factory.money' )
);
},
'api.factory.amount' => static function ( ContainerInterface $container ): AmountFactory { 'api.factory.amount' => static function ( ContainerInterface $container ): AmountFactory {
$item_factory = $container->get( 'api.factory.item' ); $item_factory = $container->get( 'api.factory.item' );
return new AmountFactory( return new AmountFactory(
@ -357,6 +396,21 @@ return array(
'api.factory.fraud-processor-response' => static function ( ContainerInterface $container ): FraudProcessorResponseFactory { 'api.factory.fraud-processor-response' => static function ( ContainerInterface $container ): FraudProcessorResponseFactory {
return new FraudProcessorResponseFactory(); return new FraudProcessorResponseFactory();
}, },
'api.factory.product' => static function( ContainerInterface $container ): ProductFactory {
return new ProductFactory();
},
'api.factory.billing-cycle' => static function( ContainerInterface $container ): BillingCycleFactory {
return new BillingCycleFactory( $container->get( 'api.shop.currency' ) );
},
'api.factory.payment-preferences' => static function( ContainerInterface $container ):PaymentPreferencesFactory {
return new PaymentPreferencesFactory( $container->get( 'api.shop.currency' ) );
},
'api.factory.plan' => static function( ContainerInterface $container ): PlanFactory {
return new PlanFactory(
$container->get( 'api.factory.billing-cycle' ),
$container->get( 'api.factory.payment-preferences' )
);
},
'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies { 'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies {
return new DccApplies( return new DccApplies(
$container->get( 'api.dcc-supported-country-currency-matrix' ), $container->get( 'api.dcc-supported-country-currency-matrix' ),
@ -631,6 +685,27 @@ return array(
'SGD', 'SGD',
'USD', 'USD',
), ),
'MX' => array(
'MXN',
),
'JP' => array(
'AUD',
'CAD',
'CHF',
'CZK',
'DKK',
'EUR',
'GBP',
'HKD',
'HUF',
'JPY',
'NOK',
'NZD',
'PLN',
'SEK',
'SGD',
'USD',
),
) )
); );
}, },
@ -687,6 +762,17 @@ return array(
'amex' => array( 'CAD' ), 'amex' => array( 'CAD' ),
'jcb' => array( 'CAD' ), 'jcb' => array( 'CAD' ),
), ),
'MX' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array(),
),
'JP' => array(
'mastercard' => array(),
'visa' => array(),
'amex' => array( 'JPY' ),
'jcb' => array( 'JPY' ),
),
) )
); );
}, },

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Exception;
use stdClass; use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
@ -120,6 +121,10 @@ class BillingAgreementsEndpoint {
*/ */
public function reference_transaction_enabled(): bool { public function reference_transaction_enabled(): bool {
try { try {
if ( get_transient( 'ppcp_reference_transaction_enabled' ) === true ) {
return true;
}
$this->is_request_logging_enabled = false; $this->is_request_logging_enabled = false;
try { try {
@ -130,10 +135,12 @@ class BillingAgreementsEndpoint {
); );
} finally { } finally {
$this->is_request_logging_enabled = true; $this->is_request_logging_enabled = true;
set_transient( 'ppcp_reference_transaction_enabled', true, 3 * MONTH_IN_SECONDS );
} }
return true; return true;
} catch ( PayPalApiException $exception ) { } catch ( Exception $exception ) {
delete_transient( 'ppcp_reference_transaction_enabled' );
return false; return false;
} }
} }

View file

@ -0,0 +1,229 @@
<?php
/**
* The Billing Plans endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\BillingCycle;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Plan;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory;
/**
* Class BillingPlans
*/
class BillingPlans {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* Billing cycle factory
*
* @var BillingCycleFactory
*/
private $billing_cycle_factory;
/**
* Plan factory
*
* @var PlanFactory
*/
private $plan_factory;
/**
* The logger.
*
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* BillingPlans constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param BillingCycleFactory $billing_cycle_factory Billing cycle factory.
* @param PlanFactory $plan_factory Plan factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
BillingCycleFactory $billing_cycle_factory,
PlanFactory $plan_factory,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->billing_cycle_factory = $billing_cycle_factory;
$this->plan_factory = $plan_factory;
$this->logger = $logger;
}
/**
* Creates a subscription plan.
*
* @param string $name Product name.
* @param string $product_id Product ID.
* @param array $billing_cycles Billing cycles.
* @param array $payment_preferences Payment preferences.
*
* @return Plan
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function create(
string $name,
string $product_id,
array $billing_cycles,
array $payment_preferences
): Plan {
$data = array(
'name' => $name,
'product_id' => $product_id,
'billing_cycles' => $billing_cycles,
'payment_preferences' => $payment_preferences,
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/plans';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
'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 plan.' );
}
$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 $this->plan_factory->from_paypal_response( $json );
}
/**
* Returns a plan,
*
* @param string $id Plan ID.
*
* @return Plan
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function plan( string $id ): Plan {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/plans/' . $id;
$args = array(
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to get plan.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $this->plan_factory->from_paypal_response( $json );
}
/**
* Updates pricing.
*
* @param string $id Plan ID.
* @param BillingCycle $billing_cycle Billing cycle.
*
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function update_pricing( string $id, BillingCycle $billing_cycle ): void {
$data = array(
'pricing_schemes' => array(
(object) array(
'billing_cycle_sequence' => 1,
'pricing_scheme' => $billing_cycle->pricing_scheme(),
),
),
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/plans/' . $id . '/update-pricing-schemes';
$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( 'Could not update pricing.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
}
}

View file

@ -0,0 +1,217 @@
<?php
/**
* The Billing Subscriptions endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class BillingSubscriptions
*/
class BillingSubscriptions {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* BillingSubscriptions 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;
}
/**
* Suspends a subscription.
*
* @param string $id Subscription ID.
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function suspend( string $id ):void {
$data = array(
'reason' => 'Suspended by customer',
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id . '/suspend';
$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 suspend subscription.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
}
/**
* Activates a subscription.
*
* @param string $id Subscription ID.
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function activate( string $id ): void {
$data = array(
'reason' => 'Reactivated by customer',
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id . '/activate';
$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 reactivate subscription.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
}
/**
* Cancels a Subscription.
*
* @param string $id Subscription ID.
*
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function cancel( string $id ): void {
$data = array(
'reason' => 'Cancelled by customer',
);
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id . '/cancel';
$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 cancel subscription.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
}
/**
* Returns a Subscription object from the given ID.
*
* @param string $id Subscription ID.
*
* @return stdClass
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function subscription( string $id ): stdClass {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id;
$args = array(
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to get subscription.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $json;
}
}

View file

@ -0,0 +1,199 @@
<?php
/**
* The Catalog Products endpoint.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Product;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory;
/**
* Class CatalogProduct
*/
class CatalogProducts {
use RequestTrait;
/**
* The host.
*
* @var string
*/
private $host;
/**
* The bearer.
*
* @var Bearer
*/
private $bearer;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* Product factory.
*
* @var ProductFactory
*/
private $product_factory;
/**
* CatalogProducts constructor.
*
* @param string $host The host.
* @param Bearer $bearer The bearer.
* @param ProductFactory $product_factory Product factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
string $host,
Bearer $bearer,
ProductFactory $product_factory,
LoggerInterface $logger
) {
$this->host = $host;
$this->bearer = $bearer;
$this->product_factory = $product_factory;
$this->logger = $logger;
}
/**
* Creates a product.
*
* @param string $name Product name.
* @param string $description Product description.
*
* @return Product
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function create( string $name, string $description ): Product {
$data = array(
'name' => $name,
);
if ( $description ) {
$data['description'] = $description;
}
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/catalogs/products';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
'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 product.' );
}
$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 $this->product_factory->from_paypal_response( $json );
}
/**
* Updates a Product.
*
* @param string $id Product ID.
* @param array $data Data to update.
*
* @return void
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function update( string $id, array $data ): void {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/catalogs/products/' . $id;
$args = array(
'method' => 'PATCH',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
'body' => wp_json_encode( $data ),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to update product.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 204 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
}
/**
* Return a Product from the given ID.
*
* @param string $id Product ID.
*
* @return Product
*
* @throws RuntimeException If the request fails.
* @throws PayPalApiException If the request fails.
*/
public function product( string $id ): Product {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v1/catalogs/products/' . $id;
$args = array(
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'Prefer' => 'return=representation',
),
);
$response = $this->request( $url, $args );
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
throw new RuntimeException( 'Not able to get product.' );
}
$json = json_decode( $response['body'] );
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
throw new PayPalApiException(
$json,
$status_code
);
}
return $this->product_factory->from_paypal_response( $json );
}
}

View file

@ -103,7 +103,8 @@ class IdentityToken {
); );
if ( if (
( $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ) ) ( $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ) )
&& defined( 'PPCP_FLAG_SUBSCRIPTION' ) && PPCP_FLAG_SUBSCRIPTION || ( $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ) )
|| ( $this->settings->has( 'subscriptions_mode' ) && $this->settings->get( 'subscriptions_mode' ) === 'vaulting_api' )
) { ) {
$customer_id = $this->customer_repository->customer_id_for_user( ( $user_id ) ); $customer_id = $this->customer_repository->customer_id_for_user( ( $user_id ) );
update_user_meta( $user_id, 'ppcp_customer_id', $customer_id ); update_user_meta( $user_id, 'ppcp_customer_id', $customer_id );

View file

@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PatchCollection;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentMethod; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentMethod;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
@ -180,6 +181,8 @@ class OrderEndpoint {
* @param Payer|null $payer The payer off the order. * @param Payer|null $payer The payer off the order.
* @param PaymentToken|null $payment_token The payment token. * @param PaymentToken|null $payment_token The payment token.
* @param PaymentMethod|null $payment_method The payment method. * @param PaymentMethod|null $payment_method The payment method.
* @param string $paypal_request_id The paypal request id.
* @param string $user_action The user action.
* *
* @return Order * @return Order
* @throws RuntimeException If the request fails. * @throws RuntimeException If the request fails.
@ -189,19 +192,28 @@ class OrderEndpoint {
string $shipping_preference, string $shipping_preference,
Payer $payer = null, Payer $payer = null,
PaymentToken $payment_token = null, PaymentToken $payment_token = null,
PaymentMethod $payment_method = null PaymentMethod $payment_method = null,
string $paypal_request_id = '',
string $user_action = ApplicationContext::USER_ACTION_CONTINUE
): Order { ): Order {
$bearer = $this->bearer->bearer(); $bearer = $this->bearer->bearer();
$data = array( $data = array(
'intent' => ( $this->subscription_helper->cart_contains_subscription() || $this->subscription_helper->current_product_is_subscription() ) ? 'AUTHORIZE' : $this->intent, 'intent' => ( $this->subscription_helper->cart_contains_subscription() || $this->subscription_helper->current_product_is_subscription() ) ? 'AUTHORIZE' : $this->intent,
'purchase_units' => array_map( 'purchase_units' => array_map(
static function ( PurchaseUnit $item ): array { static function ( PurchaseUnit $item ) use ( $shipping_preference ): array {
return $item->to_array(); $data = $item->to_array();
if ( $shipping_preference !== ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE ) {
// Shipping options are not allowed to be sent when not getting the address from PayPal.
unset( $data['shipping']['options'] );
}
return $data;
}, },
$items $items
), ),
'application_context' => $this->application_context_repository 'application_context' => $this->application_context_repository
->current_context( $shipping_preference )->to_array(), ->current_context( $shipping_preference, $user_action )->to_array(),
); );
if ( $payer && ! empty( $payer->email_address() ) ) { if ( $payer && ! empty( $payer->email_address() ) ) {
$data['payer'] = $payer->to_array(); $data['payer'] = $payer->to_array();
@ -510,13 +522,22 @@ class OrderEndpoint {
return $order_to_update; return $order_to_update;
} }
$this->patch( $order_to_update->id(), $patches );
$new_order = $this->order( $order_to_update->id() );
return $new_order;
}
/**
* Patches an order.
*
* @param string $order_id The PayPal order ID.
* @param PatchCollection $patches The patches.
*
* @throws RuntimeException If the request fails.
*/
public function patch( string $order_id, PatchCollection $patches ): void {
$patches_array = $patches->to_array(); $patches_array = $patches->to_array();
if ( ! isset( $patches_array[0]['value']['shipping'] ) ) {
$shipping = isset( $order_to_update->purchase_units()[0] ) && null !== $order_to_update->purchase_units()[0]->shipping() ? $order_to_update->purchase_units()[0]->shipping() : null;
if ( $shipping ) {
$patches_array[0]['value']['shipping'] = $shipping->to_array();
}
}
/** /**
* The filter can be used to modify the order patching request body data (the final prices, items). * The filter can be used to modify the order patching request body data (the final prices, items).
@ -524,7 +545,7 @@ class OrderEndpoint {
$patches_array = apply_filters( 'ppcp_patch_order_request_body_data', $patches_array ); $patches_array = apply_filters( 'ppcp_patch_order_request_body_data', $patches_array );
$bearer = $this->bearer->bearer(); $bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $order_to_update->id(); $url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $order_id;
$args = array( $args = array(
'method' => 'PATCH', 'method' => 'PATCH',
'headers' => array( 'headers' => array(
@ -540,11 +561,8 @@ class OrderEndpoint {
$response = $this->request( $url, $args ); $response = $this->request( $url, $args );
if ( is_wp_error( $response ) ) { if ( is_wp_error( $response ) ) {
$error = new RuntimeException( $error = new RuntimeException( 'Could not patch order.' );
__( 'Could not retrieve order.', 'woocommerce-paypal-payments' ) $this->logger->warning(
);
$this->logger->log(
'warning',
$error->getMessage(), $error->getMessage(),
array( array(
'args' => $args, 'args' => $args,
@ -560,8 +578,7 @@ class OrderEndpoint {
$json, $json,
$status_code $status_code
); );
$this->logger->log( $this->logger->warning(
'warning',
$error->getMessage(), $error->getMessage(),
array( array(
'args' => $args, 'args' => $args,
@ -570,9 +587,6 @@ class OrderEndpoint {
); );
throw $error; throw $error;
} }
$new_order = $this->order( $order_to_update->id() );
return $new_order;
} }
/** /**

View file

@ -0,0 +1,134 @@
<?php
/**
* The Billing Cycle object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class BillingCycle
*/
class BillingCycle {
/**
* Frequency.
*
* @var array
*/
private $frequency;
/**
* Sequence.
*
* @var int
*/
private $sequence;
/**
* Tenure Type.
*
* @var string
*/
private $tenure_type;
/**
* Pricing scheme.
*
* @var array
*/
private $pricing_scheme;
/**
* Total cycles.
*
* @var int
*/
private $total_cycles;
/**
* BillingCycle constructor.
*
* @param array $frequency Frequency.
* @param int $sequence Sequence.
* @param string $tenure_type Tenure type.
* @param array $pricing_scheme Pricing scheme.
* @param int $total_cycles Total cycles.
*/
public function __construct(
array $frequency,
int $sequence,
string $tenure_type,
array $pricing_scheme = array(),
int $total_cycles = 1
) {
$this->frequency = $frequency;
$this->sequence = $sequence;
$this->tenure_type = $tenure_type;
$this->pricing_scheme = $pricing_scheme;
$this->total_cycles = $total_cycles;
}
/**
* Returns frequency.
*
* @return array
*/
public function frequency(): array {
return $this->frequency;
}
/**
* Returns sequence.
*
* @return int
*/
public function sequence(): int {
return $this->sequence;
}
/**
* Returns tenure type.
*
* @return string
*/
public function tenure_type(): string {
return $this->tenure_type;
}
/**
* Returns pricing scheme.
*
* @return array
*/
public function pricing_scheme(): array {
return $this->pricing_scheme;
}
/**
* Return total cycles.
*
* @return int
*/
public function total_cycles(): int {
return $this->total_cycles;
}
/**
* Returns Billing Cycle as array.
*
* @return array
*/
public function to_array(): array {
return array(
'frequency' => $this->frequency(),
'sequence' => $this->sequence(),
'tenure_type' => $this->tenure_type(),
'pricing_scheme' => $this->pricing_scheme(),
'total_cycles' => $this->total_cycles(),
);
}
}

View file

@ -83,8 +83,8 @@ class Patch {
public function to_array(): array { public function to_array(): array {
return array( return array(
'op' => $this->op(), 'op' => $this->op(),
'value' => $this->value(),
'path' => $this->path(), 'path' => $this->path(),
'value' => $this->value(),
); );
} }

View file

@ -0,0 +1,115 @@
<?php
/**
* The Payment Preferences object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class PaymentPreferences
*/
class PaymentPreferences {
/**
* Setup fee.
*
* @var array
*/
private $setup_fee;
/**
* Auto bill outstanding.
*
* @var bool
*/
private $auto_bill_outstanding;
/**
* Setup fee failure action.
*
* @var string
*/
private $setup_fee_failure_action;
/**
* Payment failure threshold.
*
* @var int
*/
private $payment_failure_threshold;
/**
* PaymentPreferences constructor.
*
* @param array $setup_fee Setup fee.
* @param bool $auto_bill_outstanding Auto bill outstanding.
* @param string $setup_fee_failure_action Setup fee failure action.
* @param int $payment_failure_threshold payment failure threshold.
*/
public function __construct(
array $setup_fee,
bool $auto_bill_outstanding = true,
string $setup_fee_failure_action = 'CONTINUE',
int $payment_failure_threshold = 3
) {
$this->setup_fee = $setup_fee;
$this->auto_bill_outstanding = $auto_bill_outstanding;
$this->setup_fee_failure_action = $setup_fee_failure_action;
$this->payment_failure_threshold = $payment_failure_threshold;
}
/**
* Setup fee.
*
* @return array
*/
public function setup_fee(): array {
return $this->setup_fee;
}
/**
* Auto bill outstanding.
*
* @return bool
*/
public function auto_bill_outstanding(): bool {
return $this->auto_bill_outstanding;
}
/**
* Setup fee failure action.
*
* @return string
*/
public function setup_fee_failure_action(): string {
return $this->setup_fee_failure_action;
}
/**
* Payment failure threshold.
*
* @return int
*/
public function payment_failure_threshold(): int {
return $this->payment_failure_threshold;
}
/**
* Returns Payment Preferences as array.
*
* @return array
*/
public function to_array():array {
return array(
'setup_fee' => $this->setup_fee(),
'auto_bill_outstanding' => $this->auto_bill_outstanding(),
'setup_fee_failure_action' => $this->setup_fee_failure_action(),
'payment_failure_threshold' => $this->payment_failure_threshold(),
);
}
}

View file

@ -0,0 +1,154 @@
<?php
/**
* The Plan object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class Plan
*/
class Plan {
/**
* Plan ID.
*
* @var string
*/
private $id;
/**
* Plan name.
*
* @var string
*/
private $name;
/**
* Product ID.
*
* @var string
*/
private $product_id;
/**
* Billing cycles.
*
* @var array
*/
private $billing_cycles;
/**
* Payment preferences.
*
* @var PaymentPreferences
*/
private $payment_preferences;
/**
* Plan status.
*
* @var string
*/
private $status;
/**
* Plan constructor.
*
* @param string $id Plan ID.
* @param string $name Plan name.
* @param string $product_id Product ID.
* @param array $billing_cycles Billing cycles.
* @param PaymentPreferences $payment_preferences Payment preferences.
* @param string $status Plan status.
*/
public function __construct(
string $id,
string $name,
string $product_id,
array $billing_cycles,
PaymentPreferences $payment_preferences,
string $status = ''
) {
$this->id = $id;
$this->name = $name;
$this->product_id = $product_id;
$this->billing_cycles = $billing_cycles;
$this->payment_preferences = $payment_preferences;
$this->status = $status;
}
/**
* Returns Plan ID.
*
* @return string
*/
public function id(): string {
return $this->id;
}
/**
* Returns Plan name.
*
* @return string
*/
public function name(): string {
return $this->name;
}
/**
* Returns Product ID.
*
* @return string
*/
public function product_id(): string {
return $this->product_id;
}
/**
* Returns Billing cycles.
*
* @return array
*/
public function billing_cycles(): array {
return $this->billing_cycles;
}
/**
* Returns Payment preferences.
*
* @return PaymentPreferences
*/
public function payment_preferences(): PaymentPreferences {
return $this->payment_preferences;
}
/**
* Returns Plan status.
*
* @return string
*/
public function status(): string {
return $this->status;
}
/**
* Returns Plan as array.
*
* @return array
*/
public function to_array():array {
return array(
'id' => $this->id(),
'name' => $this->name(),
'product_id' => $this->product_id(),
'billing_cycles' => $this->billing_cycles(),
'payment_preferences' => $this->payment_preferences(),
'status' => $this->status(),
);
}
}

View file

@ -0,0 +1,90 @@
<?php
/**
* The Product object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class Product
*/
class Product {
/**
* Product ID.
*
* @var string
*/
private $id;
/**
* Product name.
*
* @var string
*/
private $name;
/**
* Product description.
*
* @var string
*/
private $description;
/**
* Product constructor.
*
* @param string $id Product ID.
* @param string $name Product name.
* @param string $description Product description.
*/
public function __construct( string $id, string $name, string $description = '' ) {
$this->id = $id;
$this->name = $name;
$this->description = $description;
}
/**
* Returns the product ID.
*
* @return string
*/
public function id(): string {
return $this->id;
}
/**
* Returns the product name.
*
* @return string
*/
public function name(): string {
return $this->name;
}
/**
* Returns the product description.
*
* @return string
*/
public function description(): string {
return $this->description;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array() {
return array(
'id' => $this->id(),
'name' => $this->name(),
'description' => $this->description(),
);
}
}

View file

@ -28,15 +28,24 @@ class Shipping {
*/ */
private $address; private $address;
/**
* Shipping methods.
*
* @var ShippingOption[]
*/
private $options;
/** /**
* Shipping constructor. * Shipping constructor.
* *
* @param string $name The name. * @param string $name The name.
* @param Address $address The address. * @param Address $address The address.
* @param ShippingOption[] $options Shipping methods.
*/ */
public function __construct( string $name, Address $address ) { public function __construct( string $name, Address $address, array $options = array() ) {
$this->name = $name; $this->name = $name;
$this->address = $address; $this->address = $address;
$this->options = $options;
} }
/** /**
@ -57,17 +66,35 @@ class Shipping {
return $this->address; return $this->address;
} }
/**
* Returns the shipping methods.
*
* @return ShippingOption[]
*/
public function options(): array {
return $this->options;
}
/** /**
* Returns the object as array. * Returns the object as array.
* *
* @return array * @return array
*/ */
public function to_array(): array { public function to_array(): array {
return array( $result = array(
'name' => array( 'name' => array(
'full_name' => $this->name(), 'full_name' => $this->name(),
), ),
'address' => $this->address()->to_array(), 'address' => $this->address()->to_array(),
); );
if ( $this->options ) {
$result['options'] = array_map(
function ( ShippingOption $opt ): array {
return $opt->to_array();
},
$this->options
);
}
return $result;
} }
} }

View file

@ -0,0 +1,139 @@
<?php
/**
* The ShippingOption object.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Entity
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
/**
* Class ShippingOption
*/
class ShippingOption {
const TYPE_SHIPPING = 'SHIPPING';
const TYPE_PICKUP = 'PICKUP';
/**
* The name.
*
* @var string
*/
private $id;
/**
* The label.
*
* @var string
*/
private $label;
/**
* Whether the method is selected by default.
*
* @var bool
*/
private $selected;
/**
* The price.
*
* @var Money
*/
private $amount;
/**
* SHIPPING or PICKUP.
*
* @var string
*/
private $type;
/**
* ShippingOption constructor.
*
* @param string $id The name.
* @param string $label The label.
* @param bool $selected Whether the method is selected by default.
* @param Money $amount The price.
* @param string $type SHIPPING or PICKUP.
*/
public function __construct( string $id, string $label, bool $selected, Money $amount, string $type ) {
$this->id = $id;
$this->label = $label;
$this->selected = $selected;
$this->amount = $amount;
$this->type = $type;
}
/**
* The name.
*
* @return string
*/
public function id(): string {
return $this->id;
}
/**
* The label.
*
* @return string
*/
public function label(): string {
return $this->label;
}
/**
* Whether the method is selected by default.
*
* @return bool
*/
public function selected(): bool {
return $this->selected;
}
/**
* Sets whether the method is selected by default.
*
* @param bool $selected The value to be set.
*/
public function set_selected( bool $selected ): void {
$this->selected = $selected;
}
/**
* The price.
*
* @return Money
*/
public function amount(): Money {
return $this->amount;
}
/**
* SHIPPING or PICKUP.
*
* @return string
*/
public function type(): string {
return $this->type;
}
/**
* Returns the object as array.
*
* @return array
*/
public function to_array(): array {
return array(
'id' => $this->id,
'label' => $this->label,
'selected' => $this->selected,
'amount' => $this->amount->to_array(),
'type' => $this->type,
);
}
}

View file

@ -0,0 +1,84 @@
<?php
/**
* The Billing Cycle factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WC_Product;
use WooCommerce\PayPalCommerce\ApiClient\Entity\BillingCycle;
/**
* Class BillingCycleFactory
*/
class BillingCycleFactory {
/**
* The currency.
*
* @var string
*/
private $currency;
/**
* BillingCycleFactory constructor.
*
* @param string $currency The currency.
*/
public function __construct( string $currency ) {
$this->currency = $currency;
}
/**
* Returns a BillingCycle object from the given WC product.
*
* @param WC_Product $product WC product.
* @return BillingCycle
*/
public function from_wc_product( WC_Product $product ): BillingCycle {
return new BillingCycle(
array(
'interval_unit' => $product->get_meta( '_subscription_period' ),
'interval_count' => $product->get_meta( '_subscription_period_interval' ),
),
1,
'REGULAR',
array(
'fixed_price' => array(
'value' => $product->get_meta( '_subscription_price' ),
'currency_code' => $this->currency,
),
),
(int) $product->get_meta( '_subscription_length' )
);
}
/**
* Returns a BillingCycle object based off a PayPal response.
*
* @param stdClass $data the data.
* @return BillingCycle
*/
public function from_paypal_response( stdClass $data ): BillingCycle {
return new BillingCycle(
array(
'interval_unit' => $data->frequency->interval_unit,
'interval_count' => $data->frequency->interval_count,
),
$data->sequence,
$data->tenure_type,
array(
'fixed_price' => array(
'value' => $data->pricing_scheme->fixed_price->value,
'currency_code' => $data->pricing_scheme->fixed_price->currency_code,
),
),
$data->total_cycles
);
}
}

View file

@ -71,7 +71,15 @@ class PatchCollectionFactory {
); );
$operation = $purchase_unit_from ? 'replace' : 'add'; $operation = $purchase_unit_from ? 'replace' : 'add';
$value = $purchase_unit_to->to_array(); $value = $purchase_unit_to->to_array();
$patches[] = new Patch(
if ( ! isset( $value['shipping'] ) ) {
$shipping = $purchase_unit_from && null !== $purchase_unit_from->shipping() ? $purchase_unit_from->shipping() : null;
if ( $shipping ) {
$value['shipping'] = $shipping->to_array();
}
}
$patches[] = new Patch(
$operation, $operation,
$path . "/@reference_id=='" . $purchase_unit_to->reference_id() . "'", $path . "/@reference_id=='" . $purchase_unit_to->reference_id() . "'",
$value $value

View file

@ -0,0 +1,69 @@
<?php
/**
* The Payment Preferences factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WC_Product;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentPreferences;
/**
* Class PaymentPreferencesFactory
*/
class PaymentPreferencesFactory {
/**
* The currency.
*
* @var string
*/
private $currency;
/**
* PaymentPreferencesFactory constructor.
*
* @param string $currency The currency.
*/
public function __construct( string $currency ) {
$this->currency = $currency;
}
/**
* Returns a PaymentPreferences object from the given WC product.
*
* @param WC_Product $product WC product.
* @return PaymentPreferences
*/
public function from_wc_product( WC_Product $product ):PaymentPreferences {
return new PaymentPreferences(
array(
'value' => $product->get_meta( '_subscription_sign_up_fee' ) ?: '0',
'currency_code' => $this->currency,
)
);
}
/**
* Returns a PaymentPreferences object based off a PayPal response.
*
* @param stdClass $data The data.
* @return PaymentPreferences
*/
public function from_paypal_response( stdClass $data ) {
return new PaymentPreferences(
array(
'value' => $data->setup_fee->value,
'currency_code' => $data->setup_fee->currency_code,
),
$data->auto_bill_outstanding,
$data->setup_fee_failure_action,
$data->payment_failure_threshold
);
}
}

View file

@ -0,0 +1,96 @@
<?php
/**
* Plan Factory.
*
* @package WooCommerce\PayPalCommerce\Webhooks\Handler
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Plan;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class PlanFactory
*/
class PlanFactory {
/**
* Billing cycle factory.
*
* @var BillingCycleFactory
*/
private $billing_cycle_factory;
/**
* Payment preferences factory.
*
* @var PaymentPreferencesFactory
*/
private $payment_preferences_factory;
/**
* PlanFactory constructor.
*
* @param BillingCycleFactory $billing_cycle_factory Billing cycle factory.
* @param PaymentPreferencesFactory $payment_preferences_factory Payment preferences factory.
*/
public function __construct(
BillingCycleFactory $billing_cycle_factory,
PaymentPreferencesFactory $payment_preferences_factory
) {
$this->billing_cycle_factory = $billing_cycle_factory;
$this->payment_preferences_factory = $payment_preferences_factory;
}
/**
* Returns a Plan from PayPal response.
*
* @param stdClass $data The data.
*
* @return Plan
*
* @throws RuntimeException If it could not create Plan.
*/
public function from_paypal_response( stdClass $data ): Plan {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'No id for given plan', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $data->name ) ) {
throw new RuntimeException(
__( 'No name for plan given', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $data->product_id ) ) {
throw new RuntimeException(
__( 'No product id for given plan', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $data->billing_cycles ) ) {
throw new RuntimeException(
__( 'No billing cycles for given plan', 'woocommerce-paypal-payments' )
);
}
$billing_cycles = array();
foreach ( $data->billing_cycles as $billing_cycle ) {
$billing_cycles[] = $this->billing_cycle_factory->from_paypal_response( $billing_cycle );
}
$payment_preferences = $this->payment_preferences_factory->from_paypal_response( $data->payment_preferences );
return new Plan(
$data->id,
$data->name,
$data->product_id,
$billing_cycles,
$payment_preferences,
$data->status ?? ''
);
}
}

View file

@ -0,0 +1,47 @@
<?php
/**
* The Product factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Product;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class ProductFactory
*/
class ProductFactory {
/**
* Creates a Product based off a PayPal response.
*
* @param stdClass $data The JSON object.
*
* @return Product
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): Product {
if ( ! isset( $data->id ) ) {
throw new RuntimeException(
__( 'No id for product given', 'woocommerce-paypal-payments' )
);
}
if ( ! isset( $data->name ) ) {
throw new RuntimeException(
__( 'No name for product given', 'woocommerce-paypal-payments' )
);
}
return new Product(
$data->id,
$data->name,
$data->description ?? ''
);
}
}

View file

@ -153,10 +153,11 @@ class PurchaseUnitFactory {
* Creates a PurchaseUnit based off a WooCommerce cart. * Creates a PurchaseUnit based off a WooCommerce cart.
* *
* @param \WC_Cart|null $cart The cart. * @param \WC_Cart|null $cart The cart.
* @param bool $with_shipping_options Include WC shipping methods.
* *
* @return PurchaseUnit * @return PurchaseUnit
*/ */
public function from_wc_cart( ?\WC_Cart $cart = null ): PurchaseUnit { public function from_wc_cart( ?\WC_Cart $cart = null, bool $with_shipping_options = false ): PurchaseUnit {
if ( ! $cart ) { if ( ! $cart ) {
$cart = WC()->cart ?? new \WC_Cart(); $cart = WC()->cart ?? new \WC_Cart();
} }
@ -172,7 +173,7 @@ class PurchaseUnitFactory {
$shipping = null; $shipping = null;
$customer = \WC()->customer; $customer = \WC()->customer;
if ( $this->shipping_needed( ... array_values( $items ) ) && is_a( $customer, \WC_Customer::class ) ) { if ( $this->shipping_needed( ... array_values( $items ) ) && is_a( $customer, \WC_Customer::class ) ) {
$shipping = $this->shipping_factory->from_wc_customer( \WC()->customer ); $shipping = $this->shipping_factory->from_wc_customer( \WC()->customer, $with_shipping_options );
if ( if (
2 !== strlen( $shipping->address()->country_code() ) || 2 !== strlen( $shipping->address()->country_code() ) ||
( ! $shipping->address()->postal_code() && ! $this->country_without_postal_code( $shipping->address()->country_code() ) ) ( ! $shipping->address()->postal_code() && ! $this->country_without_postal_code( $shipping->address()->country_code() ) )

View file

@ -10,6 +10,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory; namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Shipping; use WooCommerce\PayPalCommerce\ApiClient\Entity\Shipping;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ShippingOption;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/** /**
@ -24,23 +25,33 @@ class ShippingFactory {
*/ */
private $address_factory; private $address_factory;
/**
* The shipping option factory.
*
* @var ShippingOptionFactory
*/
private $shipping_option_factory;
/** /**
* ShippingFactory constructor. * ShippingFactory constructor.
* *
* @param AddressFactory $address_factory The address factory. * @param AddressFactory $address_factory The address factory.
* @param ShippingOptionFactory $shipping_option_factory The shipping option factory.
*/ */
public function __construct( AddressFactory $address_factory ) { public function __construct( AddressFactory $address_factory, ShippingOptionFactory $shipping_option_factory ) {
$this->address_factory = $address_factory; $this->address_factory = $address_factory;
$this->shipping_option_factory = $shipping_option_factory;
} }
/** /**
* Creates a shipping object based off a WooCommerce customer. * Creates a shipping object based off a WooCommerce customer.
* *
* @param \WC_Customer $customer The WooCommerce customer. * @param \WC_Customer $customer The WooCommerce customer.
* @param bool $with_shipping_options Include WC shipping methods.
* *
* @return Shipping * @return Shipping
*/ */
public function from_wc_customer( \WC_Customer $customer ): Shipping { public function from_wc_customer( \WC_Customer $customer, bool $with_shipping_options = false ): Shipping {
// Replicates the Behavior of \WC_Order::get_formatted_shipping_full_name(). // Replicates the Behavior of \WC_Order::get_formatted_shipping_full_name().
$full_name = sprintf( $full_name = sprintf(
// translators: %1$s is the first name and %2$s is the second name. wc translation. // translators: %1$s is the first name and %2$s is the second name. wc translation.
@ -51,7 +62,8 @@ class ShippingFactory {
$address = $this->address_factory->from_wc_customer( $customer ); $address = $this->address_factory->from_wc_customer( $customer );
return new Shipping( return new Shipping(
$full_name, $full_name,
$address $address,
$with_shipping_options ? $this->shipping_option_factory->from_wc_cart() : array()
); );
} }
@ -91,9 +103,14 @@ class ShippingFactory {
); );
} }
$address = $this->address_factory->from_paypal_response( $data->address ); $address = $this->address_factory->from_paypal_response( $data->address );
$options = array_map(
array( $this->shipping_option_factory, 'from_paypal_response' ),
$data->options ?? array()
);
return new Shipping( return new Shipping(
$data->name->full_name, $data->name->full_name,
$address $address,
$options
); );
} }
} }

View file

@ -0,0 +1,111 @@
<?php
/**
* The shipping options factory.
*
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
use stdClass;
use WC_Cart;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Money;
use WooCommerce\PayPalCommerce\ApiClient\Entity\ShippingOption;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
/**
* Class ShippingOptionFactory
*/
class ShippingOptionFactory {
/**
* The Money factory.
*
* @var MoneyFactory
*/
private $money_factory;
/**
* ShippingOptionFactory constructor.
*
* @param MoneyFactory $money_factory The Money factory.
*/
public function __construct( MoneyFactory $money_factory ) {
$this->money_factory = $money_factory;
}
/**
* Creates an array of ShippingOption objects for the shipping methods available in the cart.
*
* @param WC_Cart|null $cart The cart.
* @return ShippingOption[]
*/
public function from_wc_cart( ?WC_Cart $cart = null ): array {
if ( ! $cart ) {
$cart = WC()->cart ?? new WC_Cart();
}
$cart->calculate_shipping();
$chosen_shipping_methods = WC()->session->get( 'chosen_shipping_methods', array() );
if ( ! is_array( $chosen_shipping_methods ) ) {
$chosen_shipping_methods = array();
}
$packages = WC()->shipping()->get_packages();
$options = array();
foreach ( $packages as $package ) {
$rates = $package['rates'] ?? array();
foreach ( $rates as $rate ) {
if ( ! $rate instanceof \WC_Shipping_Rate ) {
continue;
}
$options[] = new ShippingOption(
$rate->get_id(),
$rate->get_label(),
in_array( $rate->get_id(), $chosen_shipping_methods, true ),
new Money(
(float) $rate->get_cost(),
get_woocommerce_currency()
),
ShippingOption::TYPE_SHIPPING
);
}
}
if ( ! $chosen_shipping_methods && $options ) {
$options[0]->set_selected( true );
}
return $options;
}
/**
* Creates a ShippingOption object from the PayPal JSON object.
*
* @param stdClass $data The JSON object.
*
* @return ShippingOption
* @throws RuntimeException When JSON object is malformed.
*/
public function from_paypal_response( stdClass $data ): ShippingOption {
if ( ! isset( $data->id ) ) {
throw new RuntimeException( 'No id was given for shipping option.' );
}
if ( ! isset( $data->amount ) ) {
throw new RuntimeException( 'Shipping option amount not found' );
}
$amount = $this->money_factory->from_paypal_response( $data->amount );
return new ShippingOption(
$data->id,
$data->label ?? '',
isset( $data->selected ) ? (bool) $data->selected : false,
$amount,
$data->type ?? ShippingOption::TYPE_SHIPPING
);
}
}

View file

@ -38,11 +38,13 @@ class ApplicationContextRepository {
* Returns the current application context. * Returns the current application context.
* *
* @param string $shipping_preferences The shipping preferences. * @param string $shipping_preferences The shipping preferences.
* @param string $user_action The user action.
* *
* @return ApplicationContext * @return ApplicationContext
*/ */
public function current_context( public function current_context(
string $shipping_preferences = ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING string $shipping_preferences = ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING,
string $user_action = ApplicationContext::USER_ACTION_CONTINUE
): ApplicationContext { ): ApplicationContext {
$brand_name = $this->settings->has( 'brand_name' ) ? $this->settings->get( 'brand_name' ) : ''; $brand_name = $this->settings->has( 'brand_name' ) ? $this->settings->get( 'brand_name' ) : '';
@ -55,7 +57,8 @@ class ApplicationContextRepository {
(string) $brand_name, (string) $brand_name,
$locale, $locale,
(string) $landingpage, (string) $landingpage,
$shipping_preferences $shipping_preferences,
$user_action
); );
return $context; return $context;
} }

View file

@ -0,0 +1,14 @@
{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": "3.25.0"
}
],
[
"@babel/preset-react"
]
]
}

3
modules/ppcp-blocks/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
node_modules
assets/js
assets/css

View file

@ -0,0 +1,17 @@
{
"name": "woocommerce/ppcp-blocks",
"type": "dhii-mod",
"description": "Blocks module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {
"psr-4": {
"WooCommerce\\PayPalCommerce\\Blocks\\": "src"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,74 @@
<?php
/**
* The blocks module extensions.
*
* @package WooCommerce\PayPalCommerce\Blocks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Blocks;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'wcgateway.button.locations' => function ( ContainerInterface $container, array $locations ): array {
return array_merge(
$locations,
array(
'checkout-block-express' => _x( 'Block Express Checkout', 'Name of Buttons Location', 'woocommerce-paypal-payments' ),
'cart-block' => _x( 'Block Cart', 'Name of Buttons Location', 'woocommerce-paypal-payments' ),
)
);
},
'wcgateway.settings.pay-later.messaging-locations' => function ( ContainerInterface $container, array $locations ): array {
unset( $locations['checkout-block-express'] );
unset( $locations['cart-block'] );
return $locations;
},
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
$insert_after = function( array $array, string $key, array $new ): array {
$keys = array_keys( $array );
$index = array_search( $key, $keys, true );
$pos = false === $index ? count( $array ) : $index + 1;
return array_merge( array_slice( $array, 0, $pos ), $new, array_slice( $array, $pos ) );
};
return $insert_after(
$fields,
'smart_button_locations',
array(
'blocks_final_review_enabled' => array(
'title' => __( 'Require final confirmation on checkout', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'label' => __(
'Require customers to confirm express payments from the Block Cart and Block Express Checkout on the checkout page.
<p class="description">If this setting is not enabled, <a href="https://woocommerce.com/document/woocommerce-paypal-payments/#blocks-faq" target="_blank">payment confirmation on the checkout will be skipped</a>.
Skipping the final confirmation on the checkout page may impact the buyer experience during the PayPal payment process.</p>',
'woocommerce-paypal-payments'
),
'default' => true,
'screens' => array( State::STATE_START, State::STATE_ONBOARDED ),
'requirements' => array(),
'gateway' => 'paypal',
),
)
);
},
'button.pay-now-contexts' => function ( ContainerInterface $container, array $contexts ): array {
if ( ! $container->get( 'blocks.settings.final_review_enabled' ) ) {
$contexts[] = 'checkout-block';
$contexts[] = 'cart-block';
}
return $contexts;
},
'button.handle-shipping-in-paypal' => function ( ContainerInterface $container ): bool {
return ! $container->get( 'blocks.settings.final_review_enabled' );
},
);

View file

@ -0,0 +1,16 @@
<?php
/**
* The blocks module.
*
* @package WooCommerce\PayPalCommerce\Blocks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Blocks;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return new BlocksModule();
};

View file

@ -0,0 +1,33 @@
{
"name": "ppcp-blocks",
"version": "1.0.0",
"license": "GPL-3.0-or-later",
"browserslist": [
"> 0.5%",
"Safari >= 8",
"Chrome >= 41",
"Firefox >= 43",
"Edge >= 14"
],
"dependencies": {
"core-js": "^3.25.0"
},
"devDependencies": {
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-react": "^7.18.6",
"@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",
"file-loader": "^6.2.0",
"sass": "^1.42.1",
"sass-loader": "^12.1.0",
"webpack": "^5.76",
"webpack-cli": "^4.10"
},
"scripts": {
"build": "cross-env BABEL_ENV=default NODE_ENV=production webpack",
"watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch",
"dev": "cross-env BABEL_ENV=default webpack --watch"
}
}

View file

@ -0,0 +1,126 @@
/**
* @param {String} fullName
* @returns {Array}
*/
export const splitFullName = (fullName) => {
fullName = fullName.trim()
if (!fullName.includes(' ')) {
return [fullName, ''];
}
const parts = fullName.split(' ');
const firstName = parts[0];
parts.shift();
const lastName = parts.join(' ');
return [firstName, lastName];
}
/**
* @param {Object} address
* @returns {Object}
*/
export const paypalAddressToWc = (address) => {
let map = {
country_code: 'country',
address_line_1: 'address_1',
address_line_2: 'address_2',
admin_area_1: 'state',
admin_area_2: 'city',
postal_code: 'postcode',
};
if (address.city) { // address not from API, such as onShippingChange
map = {
country_code: 'country',
state: 'state',
city: 'city',
postal_code: 'postcode',
};
}
const result = {};
Object.entries(map).forEach(([paypalKey, wcKey]) => {
if (address[paypalKey]) {
result[wcKey] = address[paypalKey];
}
});
const defaultAddress = {
first_name: '',
last_name: '',
company: '',
address_1: '',
address_2: '',
city: '',
state: '',
postcode: '',
country: '',
phone: '',
};
return {...defaultAddress, ...result};
}
/**
* @param {Object} shipping
* @returns {Object}
*/
export const paypalShippingToWc = (shipping) => {
const [firstName, lastName] = splitFullName(shipping.name.full_name);
return {
...paypalAddressToWc(shipping.address),
first_name: firstName,
last_name: lastName,
}
}
/**
* @param {Object} payer
* @returns {Object}
*/
export const paypalPayerToWc = (payer) => {
const firstName = payer.name.given_name;
const lastName = payer.name.surname;
const address = payer.address ? paypalAddressToWc(payer.address) : {};
return {
...address,
first_name: firstName,
last_name: lastName,
email: payer.email_address,
}
}
/**
* @param {Object} order
* @returns {Object}
*/
export const paypalOrderToWcShippingAddress = (order) => {
const shipping = order.purchase_units[0].shipping;
if (!shipping) {
return {};
}
const res = paypalShippingToWc(shipping);
// use the name from billing if the same, to avoid possible mistakes when splitting full_name
const billingAddress = paypalPayerToWc(order.payer);
if (`${res.first_name} ${res.last_name}` === `${billingAddress.first_name} ${billingAddress.last_name}`) {
res.first_name = billingAddress.first_name;
res.last_name = billingAddress.last_name;
}
return res;
}
/**
*
* @param order
* @returns {{shippingAddress: Object, billingAddress: Object}}
*/
export const paypalOrderToWcAddresses = (order) => {
const shippingAddress = paypalOrderToWcShippingAddress(order);
let billingAddress = paypalPayerToWc(order.payer);
// no billing address, such as if billing address retrieval is not allowed in the merchant account
if (!billingAddress.address_line_1) {
billingAddress = {...shippingAddress, ...paypalPayerToWc(order.payer)};
}
return {billingAddress, shippingAddress};
}

View file

@ -0,0 +1,269 @@
import {useEffect, useState} from '@wordpress/element';
import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry';
import {paypalAddressToWc, paypalOrderToWcAddresses} from "./Helper/Address";
import {loadPaypalScript} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading'
const config = wc.wcSettings.getSetting('ppcp-gateway_data');
window.ppcpFundingSource = config.fundingSource;
const PayPalComponent = ({
onClick,
onClose,
onSubmit,
onError,
eventRegistration,
emitResponse,
activePaymentMethod,
shippingData,
}) => {
const {onPaymentSetup, onCheckoutAfterProcessingWithError} = eventRegistration;
const {responseTypes} = emitResponse;
const [paypalOrder, setPaypalOrder] = useState(null);
const [loaded, setLoaded] = useState(false);
useEffect(() => {
if (!loaded) {
loadPaypalScript(config.scriptData, () => {
setLoaded(true);
});
}
}, [loaded]);
const createOrder = async () => {
try {
const res = await fetch(config.scriptData.ajax.create_order.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.scriptData.ajax.create_order.nonce,
bn_code: '',
context: config.scriptData.context,
payment_method: 'ppcp-gateway',
createaccount: false
}),
});
const json = await res.json();
if (!json.success) {
if (json.data?.details?.length > 0) {
throw new Error(json.data.details.map(d => `${d.issue} ${d.description}`).join('<br/>'));
} else if (json.data?.message) {
throw new Error(json.data.message);
}
throw new Error(config.scriptData.labels.error.generic);
}
return json.data.id;
} catch (err) {
console.error(err);
onError(err.message);
onClose();
throw err;
}
};
const handleApprove = async (data, actions) => {
try {
const res = await fetch(config.scriptData.ajax.approve_order.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.scriptData.ajax.approve_order.nonce,
order_id: data.orderID,
funding_source: window.ppcpFundingSource ?? 'paypal',
})
});
const json = await res.json();
if (!json.success) {
if (typeof actions !== 'undefined' && typeof actions.restart !== 'undefined') {
return actions.restart();
}
if (json.data?.message) {
throw new Error(json.data.message);
}
throw new Error(config.scriptData.labels.error.generic)
}
const order = await actions.order.get();
setPaypalOrder(order);
if (config.finalReviewEnabled) {
const addresses = paypalOrderToWcAddresses(order);
await wp.data.dispatch('wc/store/cart').updateCustomerData({
billing_address: addresses.billingAddress,
shipping_address: addresses.shippingAddress,
});
const checkoutUrl = new URL(config.scriptData.redirect);
// sometimes some browsers may load some kind of cached version of the page,
// so adding a parameter to avoid that
checkoutUrl.searchParams.append('ppcp-continuation-redirect', (new Date()).getTime().toString());
location.href = checkoutUrl.toString();
} else {
onSubmit();
}
} catch (err) {
console.error(err);
onError(err.message);
onClose();
throw err;
}
};
const handleClick = (data, actions) => {
window.ppcpFundingSource = data.fundingSource;
onClick();
};
let handleShippingChange = null;
if (shippingData.needsShipping && !config.finalReviewEnabled) {
handleShippingChange = async (data, actions) => {
try {
const shippingOptionId = data.selected_shipping_option?.id;
if (shippingOptionId) {
await shippingData.setSelectedRates(shippingOptionId);
}
const address = paypalAddressToWc(data.shipping_address);
await wp.data.dispatch('wc/store/cart').updateCustomerData({
shipping_address: address,
});
await shippingData.setShippingAddress(address);
const res = await fetch(config.ajax.update_shipping.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: config.ajax.update_shipping.nonce,
order_id: data.orderID,
})
});
const json = await res.json();
if (!json.success) {
throw new Error(json.data.message);
}
} catch (e) {
console.error(e);
actions.reject();
}
};
}
useEffect(() => {
if (activePaymentMethod !== config.id) {
return;
}
const unsubscribeProcessing = onPaymentSetup(() => {
if (config.scriptData.continuation) {
return {
type: responseTypes.SUCCESS,
meta: {
paymentMethodData: {
'paypal_order_id': config.scriptData.continuation.order_id,
'funding_source': window.ppcpFundingSource ?? 'paypal',
}
},
};
}
const addresses = paypalOrderToWcAddresses(paypalOrder);
return {
type: responseTypes.SUCCESS,
meta: {
paymentMethodData: {
'paypal_order_id': paypalOrder.id,
'funding_source': window.ppcpFundingSource ?? 'paypal',
},
...addresses,
},
};
});
return () => {
unsubscribeProcessing();
};
}, [onPaymentSetup, paypalOrder, activePaymentMethod]);
useEffect(() => {
const unsubscribe = onCheckoutAfterProcessingWithError(({ processingResponse }) => {
if (onClose) {
onClose();
}
if (processingResponse?.paymentDetails?.errorMessage) {
return {
type: emitResponse.responseTypes.ERROR,
message: processingResponse.paymentDetails.errorMessage,
messageContext: config.scriptData.continuation ? emitResponse.noticeContexts.PAYMENTS : emitResponse.noticeContexts.EXPRESS_PAYMENTS,
};
}
return true;
});
return unsubscribe;
}, [onCheckoutAfterProcessingWithError, onClose]);
if (config.scriptData.continuation) {
return (
<div dangerouslySetInnerHTML={{__html: config.scriptData.continuation.cancel.html}}>
</div>
)
}
if (!loaded) {
return null;
}
const PayPalButton = window.paypal.Buttons.driver("react", { React, ReactDOM });
return (
<PayPalButton
style={config.scriptData.button.style}
onClick={handleClick}
onCancel={onClose}
onError={onClose}
createOrder={createOrder}
onApprove={handleApprove}
onShippingChange={handleShippingChange}
/>
);
}
const features = ['products'];
let registerMethod = registerExpressPaymentMethod;
if (config.scriptData.continuation) {
features.push('ppcp_continuation');
registerMethod = registerPaymentMethod;
}
registerMethod({
name: config.id,
label: <div dangerouslySetInnerHTML={{__html: config.title}}/>,
content: <PayPalComponent/>,
edit: <b>TODO: editing</b>,
ariaLabel: config.title,
canMakePayment: () => config.enabled,
supports: {
features: features,
},
});

View file

@ -0,0 +1,57 @@
<?php
/**
* The blocks module services.
*
* @package WooCommerce\PayPalCommerce\Blocks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Blocks;
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'blocks.url' => static function ( ContainerInterface $container ): string {
/**
* The path cannot be false.
*
* @psalm-suppress PossiblyFalseArgument
*/
return plugins_url(
'/modules/ppcp-blocks/',
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
);
},
'blocks.method' => static function ( ContainerInterface $container ): PayPalPaymentMethod {
return new PayPalPaymentMethod(
$container->get( 'blocks.url' ),
$container->get( 'ppcp.asset-version' ),
$container->get( 'button.smart-button' ),
$container->get( 'wcgateway.settings' ),
$container->get( 'wcgateway.settings.status' ),
$container->get( 'wcgateway.paypal-gateway' ),
$container->get( 'blocks.settings.final_review_enabled' ),
$container->get( 'session.cancellation.view' ),
$container->get( 'session.handler' )
);
},
'blocks.settings.final_review_enabled' => static function ( ContainerInterface $container ): bool {
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof ContainerInterface );
return $settings->has( 'blocks_final_review_enabled' ) ?
(bool) $settings->get( 'blocks_final_review_enabled' ) :
true;
},
'blocks.endpoint.update-shipping' => static function ( ContainerInterface $container ): UpdateShippingEndpoint {
return new UpdateShippingEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
);

View file

@ -0,0 +1,100 @@
<?php
/**
* The blocks module.
*
* @package WooCommerce\PayPalCommerce\Blocks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* Class BlocksModule
*/
class BlocksModule implements ModuleInterface {
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
if (
! class_exists( 'Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType' )
|| ! function_exists( 'woocommerce_store_api_register_payment_requirements' )
) {
add_action(
'admin_notices',
function () {
printf(
'<div class="notice notice-error"><p>%1$s</p></div>',
wp_kses_post(
__(
'PayPal checkout block initialization failed, possibly old WooCommerce version or disabled WooCommerce Blocks plugin.',
'woocommerce-paypal-payments'
)
)
);
}
);
return;
}
add_action(
'woocommerce_blocks_payment_method_type_registration',
function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void {
$payment_method_registry->register( $c->get( 'blocks.method' ) );
}
);
woocommerce_store_api_register_payment_requirements(
array(
'data_callback' => function() use ( $c ): array {
$smart_button = $c->get( 'button.smart-button' );
assert( $smart_button instanceof SmartButtonInterface );
if ( isset( $smart_button->script_data()['continuation'] ) ) {
return array( 'ppcp_continuation' );
}
return array();
},
)
);
add_action(
'wc_ajax_' . UpdateShippingEndpoint::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'blocks.endpoint.update-shipping' );
assert( $endpoint instanceof UpdateShippingEndpoint );
$endpoint->handle_request();
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
}
}

View file

@ -0,0 +1,133 @@
<?php
/**
* Updates PayPal order with the current shipping methods.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Blocks\Endpoint;
use Exception;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Patch;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PatchCollection;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
/**
* Class UpdateShippingEndpoint
*/
class UpdateShippingEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-update-shipping';
/**
* The Request Data Helper.
*
* @var RequestData
*/
private $request_data;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The purchase unit factory.
*
* @var PurchaseUnitFactory
*/
private $purchase_unit_factory;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* UpdateShippingEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
OrderEndpoint $order_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->order_endpoint = $order_endpoint;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->logger = $logger;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
try {
$data = $this->request_data->read_request( $this->nonce() );
$order_id = $data['order_id'];
$pu = $this->purchase_unit_factory->from_wc_cart( null, true );
$pu_data = $pu->to_array();
if ( ! isset( $pu_data['shipping']['options'] ) ) {
wp_send_json_error(
array(
'message' => 'No shipping methods.',
)
);
return false;
}
// TODO: maybe should patch only if methods changed.
// But it seems a bit difficult to detect,
// e.g. ->order($id) may not have Shipping because we drop it when address or name are missing.
// Also may consider patching only amount and options instead of the whole PU, though not sure if it makes any difference.
$patches = new PatchCollection(
new Patch(
'replace',
"/purchase_units/@reference_id=='{$pu->reference_id()}'",
$pu_data
)
);
$this->order_endpoint->patch( $order_id, $patches );
wp_send_json_success();
return true;
} catch ( Exception $error ) {
wp_send_json_error(
array(
'message' => $error->getMessage(),
)
);
return false;
}
}
}

View file

@ -0,0 +1,188 @@
<?php
/**
* The blocks module.
*
* @package WooCommerce\PayPalCommerce\Blocks
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
use WC_AJAX;
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Session\Cancellation\CancelController;
use WooCommerce\PayPalCommerce\Session\Cancellation\CancelView;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class PayPalPaymentMethod
*/
class PayPalPaymentMethod extends AbstractPaymentMethodType {
/**
* The URL of this module.
*
* @var string
*/
private $module_url;
/**
* The assets version.
*
* @var string
*/
private $version;
/**
* The smart button script loading handler.
*
* @var SmartButtonInterface
*/
private $smart_button;
/**
* The settings.
*
* @var Settings
*/
private $plugin_settings;
/**
* The Settings status helper.
*
* @var SettingsStatus
*/
protected $settings_status;
/**
* The WC gateway.
*
* @var PayPalGateway
*/
private $gateway;
/**
* Whether the final review is enabled.
*
* @var bool
*/
private $final_review_enabled;
/**
* The cancellation view.
*
* @var CancelView
*/
private $cancellation_view;
/**
* The Session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* Assets constructor.
*
* @param string $module_url The url of this module.
* @param string $version The assets version.
* @param SmartButtonInterface $smart_button The smart button script loading handler.
* @param Settings $plugin_settings The settings.
* @param SettingsStatus $settings_status The Settings status helper.
* @param PayPalGateway $gateway The WC gateway.
* @param bool $final_review_enabled Whether the final review is enabled.
* @param CancelView $cancellation_view The cancellation view.
* @param SessionHandler $session_handler The Session handler.
*/
public function __construct(
string $module_url,
string $version,
SmartButtonInterface $smart_button,
Settings $plugin_settings,
SettingsStatus $settings_status,
PayPalGateway $gateway,
bool $final_review_enabled,
CancelView $cancellation_view,
SessionHandler $session_handler
) {
$this->name = PayPalGateway::ID;
$this->module_url = $module_url;
$this->version = $version;
$this->smart_button = $smart_button;
$this->plugin_settings = $plugin_settings;
$this->settings_status = $settings_status;
$this->gateway = $gateway;
$this->final_review_enabled = $final_review_enabled;
$this->cancellation_view = $cancellation_view;
$this->session_handler = $session_handler;
}
/**
* {@inheritDoc}
*/
public function initialize() { }
/**
* {@inheritDoc}
*/
public function is_active() {
// Do not load when definitely not needed,
// but we still need to check the locations later and handle in JS
// because has_block cannot be called here (too early).
return $this->plugin_settings->has( 'enabled' ) && $this->plugin_settings->get( 'enabled' )
&& ( $this->settings_status->is_smart_button_enabled_for_location( 'checkout-block-express' ) ||
$this->settings_status->is_smart_button_enabled_for_location( 'cart-block' ) );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_script_handles() {
wp_register_script(
'ppcp-checkout-block',
trailingslashit( $this->module_url ) . 'assets/js/checkout-block.js',
array(),
$this->version,
true
);
return array( 'ppcp-checkout-block' );
}
/**
* {@inheritDoc}
*/
public function get_payment_method_data() {
$script_data = $this->smart_button->script_data();
if ( isset( $script_data['continuation'] ) ) {
$url = add_query_arg( array( CancelController::NONCE => wp_create_nonce( CancelController::NONCE ) ), wc_get_checkout_url() );
$script_data['continuation']['cancel'] = array(
'html' => $this->cancellation_view->render_session_cancellation( $url, $this->session_handler->funding_source() ),
);
}
return array(
'id' => $this->gateway->id,
'title' => $this->gateway->title,
'description' => $this->gateway->description,
'enabled' => $this->settings_status->is_smart_button_enabled_for_location( $script_data['context'] ),
'fundingSource' => $this->session_handler->funding_source(),
'finalReviewEnabled' => $this->final_review_enabled,
'ajax' => array(
'update_shipping' => array(
'endpoint' => WC_AJAX::get_endpoint( UpdateShippingEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( UpdateShippingEndpoint::nonce() ),
),
),
'scriptData' => $script_data,
);
}
}

View file

@ -0,0 +1,38 @@
const path = require('path');
const isProduction = process.env.NODE_ENV === 'production';
const DependencyExtractionWebpackPlugin = require( '@woocommerce/dependency-extraction-webpack-plugin' );
module.exports = {
devtool: isProduction ? 'source-map' : 'eval-source-map',
mode: isProduction ? 'production' : 'development',
target: 'web',
plugins: [ new DependencyExtractionWebpackPlugin() ],
entry: {
'checkout-block': path.resolve('./resources/js/checkout-block.js')
},
output: {
path: path.resolve(__dirname, 'assets/'),
filename: 'js/[name].js',
},
module: {
rules: [{
test: /\.js?$/,
exclude: /node_modules/,
loader: 'babel-loader',
},
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
{
loader: 'file-loader',
options: {
name: 'css/[name].css',
}
},
{loader:'sass-loader'}
]
}]
}
};

File diff suppressed because it is too large Load diff

View file

@ -9,6 +9,39 @@ class CartActionHandler {
this.errorHandler = errorHandler; this.errorHandler = errorHandler;
} }
subscriptionsConfiguration() {
return {
createSubscription: (data, actions) => {
return actions.subscription.create({
'plan_id': this.config.subscription_plan_id
});
},
onApprove: (data, actions) => {
fetch(this.config.ajax.approve_subscription.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.config.ajax.approve_subscription.nonce,
order_id: data.orderID,
subscription_id: data.subscriptionID
})
}).then((res)=>{
return res.json();
}).then((data) => {
if (!data.success) {
console.log(data)
throw Error(data.data.message);
}
location.href = this.config.redirect;
});
},
onError: (err) => {
console.error(err);
}
}
}
configuration() { configuration() {
const createOrder = (data, actions) => { const createOrder = (data, actions) => {
const payer = payerData(); const payer = payerData();

View file

@ -11,6 +11,34 @@ class CheckoutActionHandler {
this.spinner = spinner; this.spinner = spinner;
} }
subscriptionsConfiguration() {
return {
createSubscription: (data, actions) => {
return actions.subscription.create({
'plan_id': this.config.subscription_plan_id
});
},
onApprove: (data, actions) => {
fetch(this.config.ajax.approve_subscription.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.config.ajax.approve_subscription.nonce,
order_id: data.orderID,
subscription_id: data.subscriptionID
})
}).then((res)=>{
return res.json();
}).then((data) => {
document.querySelector('#place_order').click();
});
},
onError: (err) => {
console.error(err);
}
}
}
configuration() { configuration() {
const spinner = this.spinner; const spinner = this.spinner;
const createOrder = (data, actions) => { const createOrder = (data, actions) => {
@ -81,7 +109,7 @@ class CheckoutActionHandler {
const input = document.createElement('input'); const input = document.createElement('input');
input.setAttribute('type', 'hidden'); input.setAttribute('type', 'hidden');
input.setAttribute('name', 'ppcp-resume-order'); input.setAttribute('name', 'ppcp-resume-order');
input.setAttribute('value', data.data.purchase_units[0].custom_id); input.setAttribute('value', data.data.custom_id);
document.querySelector(formSelector).appendChild(input); document.querySelector(formSelector).appendChild(input);
return data.data.id; return data.data.id;
}); });

View file

@ -18,6 +18,56 @@ class SingleProductActionHandler {
this.errorHandler = errorHandler; this.errorHandler = errorHandler;
} }
subscriptionsConfiguration() {
return {
createSubscription: (data, actions) => {
return actions.subscription.create({
'plan_id': this.config.subscription_plan_id
});
},
onApprove: (data, actions) => {
fetch(this.config.ajax.approve_subscription.endpoint, {
method: 'POST',
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.config.ajax.approve_subscription.nonce,
order_id: data.orderID,
subscription_id: data.subscriptionID
})
}).then((res)=>{
return res.json();
}).then(() => {
const id = document.querySelector('[name="add-to-cart"]').value;
const products = [new Product(id, 1, null)];
fetch(this.config.ajax.change_cart.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin',
body: JSON.stringify({
nonce: this.config.ajax.change_cart.nonce,
products,
})
}).then((result) => {
return result.json();
}).then((result) => {
if (!result.success) {
console.log(result)
throw Error(result.data.message);
}
location.href = this.config.redirect;
})
});
},
onError: (err) => {
console.error(err);
}
}
}
configuration() configuration()
{ {
return { return {

View file

@ -50,6 +50,14 @@ class CartBootstrap {
this.errorHandler, this.errorHandler,
); );
if(
PayPalCommerceGateway.data_client_id.has_subscriptions
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled
) {
this.renderer.render(actionHandler.subscriptionsConfiguration());
return;
}
this.renderer.render( this.renderer.render(
actionHandler.configuration() actionHandler.configuration()
); );

View file

@ -64,9 +64,15 @@ class CheckoutBootstap {
this.spinner this.spinner
); );
this.renderer.render( if(
actionHandler.configuration() PayPalCommerceGateway.data_client_id.has_subscriptions
); && PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled
) {
this.renderer.render(actionHandler.subscriptionsConfiguration(), {}, actionHandler.configuration());
return;
}
this.renderer.render(actionHandler.configuration(), {}, actionHandler.configuration());
} }
updateUi() { updateUi() {

View file

@ -110,6 +110,14 @@ class SingleProductBootstap {
this.errorHandler, this.errorHandler,
); );
if(
PayPalCommerceGateway.data_client_id.has_subscriptions
&& PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled
) {
this.renderer.render(actionHandler.subscriptionsConfiguration());
return;
}
this.renderer.render( this.renderer.render(
actionHandler.configuration() actionHandler.configuration()
); );

View file

@ -10,7 +10,7 @@ class Renderer {
this.renderedSources = new Set(); this.renderedSources = new Set();
} }
render(contextConfig, settingsOverride = {}) { render(contextConfig, settingsOverride = {}, contextConfigOverride = () => {}) {
const settings = merge(this.defaultSettings, settingsOverride); const settings = merge(this.defaultSettings, settingsOverride);
const enabledSeparateGateways = Object.fromEntries(Object.entries( const enabledSeparateGateways = Object.fromEntries(Object.entries(
@ -50,7 +50,7 @@ class Renderer {
} }
if (this.creditCardRenderer) { if (this.creditCardRenderer) {
this.creditCardRenderer.render(settings.hosted_fields.wrapper, contextConfig); this.creditCardRenderer.render(settings.hosted_fields.wrapper, contextConfigOverride);
} }
for (const [fundingSource, data] of Object.entries(enabledSeparateGateways)) { for (const [fundingSource, data] of Object.entries(enabledSeparateGateways)) {

View file

@ -9,11 +9,13 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button; namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver; use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator; use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton; use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButton; use WooCommerce\PayPalCommerce\Button\Assets\SmartButton;
@ -65,25 +67,37 @@ return array(
return $dummy_ids[ $shop_country ] ?? $container->get( 'button.client_id' ); return $dummy_ids[ $shop_country ] ?? $container->get( 'button.client_id' );
}, },
'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface { 'button.is_paypal_continuation' => static function( ContainerInterface $container ): bool {
$session_handler = $container->get( 'session.handler' );
$order = $session_handler->order();
if ( ! $order ) {
return false;
}
$source = $order->payment_source();
if ( $source && $source->card() ) {
return false; // Ignore for DCC.
}
if ( 'card' === $session_handler->funding_source() ) {
return false; // Ignore for card buttons.
}
return true;
},
'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface {
$state = $container->get( 'onboarding.state' ); $state = $container->get( 'onboarding.state' );
/**
* The state.
*
* @var State $state
*/
if ( $state->current_state() !== State::STATE_ONBOARDED ) { if ( $state->current_state() !== State::STATE_ONBOARDED ) {
return new DisabledSmartButton(); return new DisabledSmartButton();
} }
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
$paypal_disabled = ! $settings->has( 'enabled' ) || ! $settings->get( 'enabled' ); $paypal_disabled = ! $settings->has( 'enabled' ) || ! $settings->get( 'enabled' );
if ( $paypal_disabled ) { if ( $paypal_disabled ) {
return new DisabledSmartButton(); return new DisabledSmartButton();
} }
$payer_factory = $container->get( 'api.factory.payer' ); $payer_factory = $container->get( 'api.factory.payer' );
$request_data = $container->get( 'button.request-data' ); $request_data = $container->get( 'button.request-data' );
$client_id = $container->get( 'button.client_id' ); $client_id = $container->get( 'button.client_id' );
$dcc_applies = $container->get( 'api.helpers.dccapplies' ); $dcc_applies = $container->get( 'api.helpers.dccapplies' );
$subscription_helper = $container->get( 'subscription.helper' ); $subscription_helper = $container->get( 'subscription.helper' );
@ -110,6 +124,7 @@ return array(
$container->get( 'wcgateway.all-funding-sources' ), $container->get( 'wcgateway.all-funding-sources' ),
$container->get( 'button.basic-checkout-validation-enabled' ), $container->get( 'button.basic-checkout-validation-enabled' ),
$container->get( 'button.early-wc-checkout-validation-enabled' ), $container->get( 'button.early-wc-checkout-validation-enabled' ),
$container->get( 'button.pay-now-contexts' ),
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
@ -119,6 +134,9 @@ return array(
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php' dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
); );
}, },
'button.pay-now-contexts' => static function ( ContainerInterface $container ): array {
return array( 'checkout', 'pay-now' );
},
'button.request-data' => static function ( ContainerInterface $container ): RequestData { 'button.request-data' => static function ( ContainerInterface $container ): RequestData {
return new RequestData(); return new RequestData();
}, },
@ -156,6 +174,8 @@ return array(
$registration_needed, $registration_needed,
$container->get( 'wcgateway.settings.card_billing_data_mode' ), $container->get( 'wcgateway.settings.card_billing_data_mode' ),
$container->get( 'button.early-wc-checkout-validation-enabled' ), $container->get( 'button.early-wc-checkout-validation-enabled' ),
$container->get( 'button.pay-now-contexts' ),
$container->get( 'button.handle-shipping-in-paypal' ),
$logger $logger
); );
}, },
@ -187,6 +207,13 @@ return array(
$logger $logger
); );
}, },
'button.endpoint.approve-subscription' => static function( ContainerInterface $container ): ApproveSubscriptionEndpoint {
return new ApproveSubscriptionEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'session.handler' )
);
},
'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver { 'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver {
return new CheckoutFormSaver(); return new CheckoutFormSaver();
}, },
@ -266,4 +293,12 @@ return array(
'button.validation.wc-checkout-validator' => static function ( ContainerInterface $container ): CheckoutFormValidator { 'button.validation.wc-checkout-validator' => static function ( ContainerInterface $container ): CheckoutFormValidator {
return new CheckoutFormValidator(); return new CheckoutFormValidator();
}, },
/**
* If true, the shipping methods are sent to PayPal allowing the customer to select it inside the popup.
* May result in slower popup performance, additional loading.
*/
'button.handle-shipping-in-paypal' => static function ( ContainerInterface $container ): bool {
return false;
},
); );

View file

@ -26,7 +26,7 @@ class DisabledSmartButton implements SmartButtonInterface {
/** /**
* Whether the scripts should be loaded. * Whether the scripts should be loaded.
*/ */
public function should_load(): bool { public function should_load_ppcp_script(): bool {
return false; return false;
} }

View file

@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
@ -68,13 +69,6 @@ class SmartButton implements SmartButtonInterface {
*/ */
private $version; private $version;
/**
* The Session Handler.
*
* @var SessionHandler
*/
private $session_handler;
/** /**
* The settings. * The settings.
* *
@ -166,13 +160,6 @@ class SmartButton implements SmartButtonInterface {
*/ */
protected $early_validation_enabled; protected $early_validation_enabled;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/** /**
* Cached payment tokens. * Cached payment tokens.
* *
@ -180,12 +167,33 @@ class SmartButton implements SmartButtonInterface {
*/ */
private $payment_tokens = null; private $payment_tokens = null;
/**
* The contexts that should have the Pay Now button.
*
* @var string[]
*/
private $pay_now_contexts;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* Session handler.
*
* @var SessionHandler
*/
private $session_handler;
/** /**
* SmartButton constructor. * SmartButton constructor.
* *
* @param string $module_url The URL to the module. * @param string $module_url The URL to the module.
* @param string $version The assets version. * @param string $version The assets version.
* @param SessionHandler $session_handler The Session Handler. * @param SessionHandler $session_handler The Session handler.
* @param Settings $settings The Settings. * @param Settings $settings The Settings.
* @param PayerFactory $payer_factory The Payer factory. * @param PayerFactory $payer_factory The Payer factory.
* @param string $client_id The client ID. * @param string $client_id The client ID.
@ -200,6 +208,7 @@ class SmartButton implements SmartButtonInterface {
* @param array $all_funding_sources All existing funding sources. * @param array $all_funding_sources All existing funding sources.
* @param bool $basic_checkout_validation_enabled Whether the basic JS validation of the form iss enabled. * @param bool $basic_checkout_validation_enabled Whether the basic JS validation of the form iss enabled.
* @param bool $early_validation_enabled Whether to execute WC validation of the checkout form. * @param bool $early_validation_enabled Whether to execute WC validation of the checkout form.
* @param array $pay_now_contexts The contexts that should have the Pay Now button.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
@ -220,6 +229,7 @@ class SmartButton implements SmartButtonInterface {
array $all_funding_sources, array $all_funding_sources,
bool $basic_checkout_validation_enabled, bool $basic_checkout_validation_enabled,
bool $early_validation_enabled, bool $early_validation_enabled,
array $pay_now_contexts,
LoggerInterface $logger LoggerInterface $logger
) { ) {
@ -240,6 +250,7 @@ class SmartButton implements SmartButtonInterface {
$this->all_funding_sources = $all_funding_sources; $this->all_funding_sources = $all_funding_sources;
$this->basic_checkout_validation_enabled = $basic_checkout_validation_enabled; $this->basic_checkout_validation_enabled = $basic_checkout_validation_enabled;
$this->early_validation_enabled = $early_validation_enabled; $this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts;
$this->logger = $logger; $this->logger = $logger;
} }
@ -254,10 +265,6 @@ class SmartButton implements SmartButtonInterface {
$this->render_message_wrapper_registrar(); $this->render_message_wrapper_registrar();
} }
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return false;
}
if ( if (
$this->settings->has( 'dcc_enabled' ) $this->settings->has( 'dcc_enabled' )
&& $this->settings->get( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' )
@ -284,7 +291,7 @@ class SmartButton implements SmartButtonInterface {
add_filter( add_filter(
'woocommerce_credit_card_form_fields', 'woocommerce_credit_card_form_fields',
function ( array $default_fields, $id ) use ( $subscription_helper ) : array { function ( array $default_fields, $id ) use ( $subscription_helper ) : array {
if ( is_user_logged_in() && $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ) && CreditCardGateway::ID === $id ) { if ( is_user_logged_in() && $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ) && CreditCardGateway::ID === $id ) {
$default_fields['card-vault'] = sprintf( $default_fields['card-vault'] = sprintf(
'<p class="form-row form-row-wide"><label for="ppcp-credit-card-vault"><input class="ppcp-credit-card-vault" type="checkbox" id="ppcp-credit-card-vault" name="vault">%s</label></p>', '<p class="form-row form-row-wide"><label for="ppcp-credit-card-vault"><input class="ppcp-credit-card-vault" type="checkbox" id="ppcp-credit-card-vault" name="vault">%s</label></p>',
@ -455,10 +462,6 @@ class SmartButton implements SmartButtonInterface {
add_action( add_action(
$this->mini_cart_button_renderer_hook(), $this->mini_cart_button_renderer_hook(),
function () { function () {
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return;
}
if ( $this->is_cart_price_total_zero() || $this->is_free_trial_cart() ) { if ( $this->is_cart_price_total_zero() || $this->is_free_trial_cart() ) {
return; return;
} }
@ -509,31 +512,70 @@ class SmartButton implements SmartButtonInterface {
} }
/** /**
* Whether the scripts should be loaded. * Whether any of our scripts (for DCC or product, mini-cart, non-block cart/checkout) should be loaded.
*/ */
public function should_load(): bool { public function should_load_ppcp_script(): bool {
$buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); $buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' );
if ( ! is_checkout() && ! $buttons_enabled ) { if ( ! $buttons_enabled ) {
return false; return false;
} }
return true; if ( in_array( $this->context(), array( 'checkout-block', 'cart-block' ), true ) ) {
return false;
}
return $this->should_load_buttons() || $this->can_render_dcc();
} }
/** /**
* Enqueues the scripts. * Determines whether the button component should be loaded.
*/ */
public function enqueue(): void { public function should_load_buttons() : bool {
if ( ! $this->should_load() ) { $buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' );
return; if ( ! $buttons_enabled ) {
return false;
} }
$load_script = false; $smart_button_enabled_for_current_location = $this->settings_status->is_smart_button_enabled_for_location( $this->context() );
if ( is_checkout() && $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) { $smart_button_enabled_for_mini_cart = $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' );
$load_script = true; $messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $this->context() );
switch ( $this->context() ) {
case 'checkout':
case 'cart':
case 'pay-now':
return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location;
case 'checkout-block':
case 'cart-block':
return $smart_button_enabled_for_current_location;
case 'product':
return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location || $smart_button_enabled_for_mini_cart;
default:
return $smart_button_enabled_for_mini_cart;
} }
if ( $this->load_button_component() ) { }
$load_script = true;
/**
* Whether DCC fields can be rendered.
*/
public function can_render_dcc() : bool {
return $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' )
&& $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' )
&& $this->dcc_applies->for_country_currency()
&& in_array( $this->context(), array( 'checkout', 'pay-now' ), true );
}
/**
* Enqueues our scripts/styles (for DCC and product, mini-cart and non-block cart/checkout)
*/
public function enqueue(): void {
if ( $this->can_render_dcc() ) {
wp_enqueue_style(
'ppcp-hosted-fields',
untrailingslashit( $this->module_url ) . '/assets/css/hosted-fields.css',
array(),
$this->version
);
} }
if ( in_array( $this->context(), array( 'pay-now', 'checkout' ), true ) ) { if ( in_array( $this->context(), array( 'pay-now', 'checkout' ), true ) ) {
@ -543,31 +585,21 @@ class SmartButton implements SmartButtonInterface {
array(), array(),
$this->version $this->version
); );
if ( $this->can_render_dcc() ) {
wp_enqueue_style(
'ppcp-hosted-fields',
untrailingslashit( $this->module_url ) . '/assets/css/hosted-fields.css',
array(),
$this->version
);
}
} }
if ( $load_script ) {
wp_enqueue_script(
'ppcp-smart-button',
untrailingslashit( $this->module_url ) . '/assets/js/button.js',
array( 'jquery' ),
$this->version,
true
);
wp_localize_script( wp_enqueue_script(
'ppcp-smart-button', 'ppcp-smart-button',
'PayPalCommerceGateway', untrailingslashit( $this->module_url ) . '/assets/js/button.js',
$this->script_data() array( 'jquery' ),
); $this->version,
} true
);
wp_localize_script(
'ppcp-smart-button',
'PayPalCommerceGateway',
$this->script_data()
);
} }
/** /**
@ -577,10 +609,6 @@ class SmartButton implements SmartButtonInterface {
*/ */
public function button_renderer( string $gateway_id ) { public function button_renderer( string $gateway_id ) {
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return;
}
$available_gateways = WC()->payment_gateways->get_available_payment_gateways(); $available_gateways = WC()->payment_gateways->get_available_payment_gateways();
if ( ! isset( $available_gateways[ $gateway_id ] ) ) { if ( ! isset( $available_gateways[ $gateway_id ] ) ) {
@ -596,9 +624,6 @@ class SmartButton implements SmartButtonInterface {
* Renders the HTML for the credit messaging. * Renders the HTML for the credit messaging.
*/ */
public function message_renderer() { public function message_renderer() {
if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) {
return false;
}
$product = wc_get_product(); $product = wc_get_product();
@ -666,18 +691,6 @@ class SmartButton implements SmartButtonInterface {
} }
/**
* Whether DCC fields can be rendered.
*
* @return bool
* @throws NotFoundException When a setting was not found.
*/
private function can_render_dcc() : bool {
$can_render = $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) && $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) && $this->dcc_applies->for_country_currency();
return $can_render;
}
/** /**
* Renders the HTML for the DCC fields. * Renders the HTML for the DCC fields.
*/ */
@ -710,7 +723,6 @@ class SmartButton implements SmartButtonInterface {
* Whether we can store vault tokens or not. * Whether we can store vault tokens or not.
* *
* @return bool * @return bool
* @throws NotFoundException If a setting hasn't been found.
*/ */
public function can_save_vault_token(): bool { public function can_save_vault_token(): bool {
@ -744,6 +756,25 @@ class SmartButton implements SmartButtonInterface {
return $this->subscription_helper->cart_contains_subscription(); return $this->subscription_helper->cart_contains_subscription();
} }
/**
* Whether PayPal subscriptions is enabled or not.
*
* @return bool
*/
private function paypal_subscriptions_enabled(): bool {
if ( defined( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) && ! PPCP_FLAG_SUBSCRIPTIONS_API ) {
return false;
}
try {
$subscriptions_mode = $this->settings->get( 'subscriptions_mode' );
} catch ( NotFoundException $exception ) {
return false;
}
return $subscriptions_mode === 'subscriptions_api';
}
/** /**
* Retrieves the 3D Secure contingency settings. * Retrieves the 3D Secure contingency settings.
* *
@ -764,7 +795,6 @@ class SmartButton implements SmartButtonInterface {
* The configuration for the smart buttons. * The configuration for the smart buttons.
* *
* @return array * @return array
* @throws NotFoundException If a setting hasn't been found.
*/ */
public function script_data(): array { public function script_data(): array {
$is_free_trial_cart = $this->is_free_trial_cart(); $is_free_trial_cart = $this->is_free_trial_cart();
@ -777,43 +807,49 @@ class SmartButton implements SmartButtonInterface {
'url_params' => $url_params, 'url_params' => $url_params,
'script_attributes' => $this->attributes(), 'script_attributes' => $this->attributes(),
'data_client_id' => array( 'data_client_id' => array(
'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(), 'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(),
'endpoint' => \WC_AJAX::get_endpoint( DataClientIdEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( DataClientIdEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( DataClientIdEndpoint::nonce() ), 'nonce' => wp_create_nonce( DataClientIdEndpoint::nonce() ),
'user' => get_current_user_id(), 'user' => get_current_user_id(),
'has_subscriptions' => $this->has_subscriptions(), 'has_subscriptions' => $this->has_subscriptions(),
'paypal_subscriptions_enabled' => $this->paypal_subscriptions_enabled(),
), ),
'redirect' => wc_get_checkout_url(), 'redirect' => wc_get_checkout_url(),
'context' => $this->context(), 'context' => $this->context(),
'ajax' => array( 'ajax' => array(
'change_cart' => array( 'change_cart' => array(
'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ),
), ),
'create_order' => array( 'create_order' => array(
'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ), 'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ),
), ),
'approve_order' => array( 'approve_order' => array(
'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ), 'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ),
), ),
'vault_paypal' => array( 'approve_subscription' => array(
'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ),
),
'vault_paypal' => array(
'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ), 'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ),
), ),
'save_checkout_form' => array( 'save_checkout_form' => array(
'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ), 'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ),
), ),
'validate_checkout' => array( 'validate_checkout' => array(
'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ),
'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ), 'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ),
), ),
'cart_script_params' => array( 'cart_script_params' => array(
'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ), 'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ),
), ),
), ),
'subscription_plan_id' => $this->paypal_subscription_id(),
'enforce_vault' => $this->has_subscriptions(), 'enforce_vault' => $this->has_subscriptions(),
'can_save_vault_token' => $this->can_save_vault_token(), 'can_save_vault_token' => $this->can_save_vault_token(),
'is_free_trial_cart' => $is_free_trial_cart, 'is_free_trial_cart' => $is_free_trial_cart,
@ -911,6 +947,15 @@ class SmartButton implements SmartButtonInterface {
$localize['button']['style']['tagline'] = false; $localize['button']['style']['tagline'] = false;
} }
if ( $this->is_paypal_continuation() ) {
$order = $this->session_handler->order();
assert( $order !== null );
$localize['continuation'] = array(
'order_id' => $order->id(),
);
}
$this->request_data->dequeue_nonce_fix(); $this->request_data->dequeue_nonce_fix();
return $localize; return $localize;
} }
@ -933,21 +978,24 @@ class SmartButton implements SmartButtonInterface {
* The JavaScript SDK url parameters. * The JavaScript SDK url parameters.
* *
* @return array * @return array
* @throws NotFoundException If a setting was not found.
*/ */
private function url_params(): array { private function url_params(): array {
$intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture'; $context = $this->context();
$product_intent = $this->subscription_helper->current_product_is_subscription() ? 'authorize' : $intent; try {
$other_context_intent = $this->subscription_helper->cart_contains_subscription() ? 'authorize' : $intent; $intent = $this->intent();
} catch ( NotFoundException $exception ) {
$intent = 'capture';
}
$params = array( $subscription_mode = $this->settings->has( 'subscriptions_mode' ) ? $this->settings->get( 'subscriptions_mode' ) : '';
$params = array(
'client-id' => $this->client_id, 'client-id' => $this->client_id,
'currency' => $this->currency, 'currency' => $this->currency,
'integration-date' => PAYPAL_INTEGRATION_DATE, 'integration-date' => PAYPAL_INTEGRATION_DATE,
'components' => implode( ',', $this->components() ), 'components' => implode( ',', $this->components() ),
'vault' => $this->can_save_vault_token() ? 'true' : 'false', 'vault' => ( $this->can_save_vault_token() || $this->subscription_helper->need_subscription_intent( $subscription_mode ) ) ? 'true' : 'false',
'commit' => is_checkout() ? 'true' : 'false', 'commit' => in_array( $context, $this->pay_now_contexts, true ) ? 'true' : 'false',
'intent' => $this->context() === 'product' ? $product_intent : $other_context_intent, 'intent' => $intent,
); );
if ( if (
$this->environment->current_environment_is( Environment::SANDBOX ) $this->environment->current_environment_is( Environment::SANDBOX )
@ -991,6 +1039,13 @@ class SmartButton implements SmartButtonInterface {
} }
} }
if ( in_array( $context, array( 'checkout-block', 'cart-block' ), true ) ) {
$disable_funding = array_diff(
array_keys( $this->all_funding_sources ),
array( 'venmo', 'paylater' )
);
}
if ( $this->is_free_trial_cart() ) { if ( $this->is_free_trial_cart() ) {
$all_sources = array_keys( $this->all_funding_sources ); $all_sources = array_keys( $this->all_funding_sources );
if ( $is_dcc_enabled || $is_separate_card_enabled ) { if ( $is_dcc_enabled || $is_separate_card_enabled ) {
@ -1001,8 +1056,8 @@ class SmartButton implements SmartButtonInterface {
$enable_funding = array( 'venmo' ); $enable_funding = array( 'venmo' );
if ( $this->settings_status->is_pay_later_button_enabled_for_location( $this->context() ) || if ( $this->settings_status->is_pay_later_button_enabled_for_location( $context ) ||
$this->settings_status->is_pay_later_messaging_enabled_for_location( $this->context() ) $this->settings_status->is_pay_later_messaging_enabled_for_location( $context )
) { ) {
$enable_funding[] = 'paylater'; $enable_funding[] = 'paylater';
} else { } else {
@ -1071,7 +1126,7 @@ class SmartButton implements SmartButtonInterface {
private function components(): array { private function components(): array {
$components = array(); $components = array();
if ( $this->load_button_component() ) { if ( $this->should_load_buttons() ) {
$components[] = 'buttons'; $components[] = 'buttons';
$components[] = 'funding-eligibility'; $components[] = 'funding-eligibility';
} }
@ -1087,34 +1142,10 @@ class SmartButton implements SmartButtonInterface {
return $components; return $components;
} }
/**
* Determines whether the button component should be loaded.
*
* @return bool
* @throws NotFoundException If a setting has not been found.
*/
private function load_button_component() : bool {
$smart_button_enabled_for_current_location = $this->settings_status->is_smart_button_enabled_for_location( $this->context() );
$smart_button_enabled_for_mini_cart = $this->settings_status->is_smart_button_enabled_for_location( 'mini-cart' );
$messaging_enabled_for_current_location = $this->settings_status->is_pay_later_messaging_enabled_for_location( $this->context() );
switch ( $this->context() ) {
case 'checkout':
case 'cart':
case 'pay-now':
return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location;
case 'product':
return $smart_button_enabled_for_current_location || $messaging_enabled_for_current_location || $smart_button_enabled_for_mini_cart;
default:
return $smart_button_enabled_for_mini_cart;
}
}
/** /**
* Whether DCC is enabled or not. * Whether DCC is enabled or not.
* *
* @return bool * @return bool
* @throws NotFoundException If a setting has not been found.
*/ */
private function dcc_is_enabled(): bool { private function dcc_is_enabled(): bool {
if ( ! is_checkout() ) { if ( ! is_checkout() ) {
@ -1143,9 +1174,11 @@ class SmartButton implements SmartButtonInterface {
* @param string $context The context. * @param string $context The context.
* *
* @return string * @return string
* @throws NotFoundException When a setting hasn't been found.
*/ */
private function style_for_context( string $style, string $context ): string { private function style_for_context( string $style, string $context ): string {
// Use the cart/checkout styles for blocks.
$context = str_replace( '-block', '', $context );
$defaults = array( $defaults = array(
'layout' => 'vertical', 'layout' => 'vertical',
'size' => 'responsive', 'size' => 'responsive',
@ -1362,6 +1395,57 @@ class SmartButton implements SmartButtonInterface {
return false; return false;
} }
/**
* Returns PayPal subscription plan id from WC subscription product.
*
* @return string
*/
private function paypal_subscription_id(): string {
if ( $this->subscription_helper->current_product_is_subscription() ) {
$product = wc_get_product();
assert( $product instanceof WC_Product );
if ( $product->get_type() === 'subscription' && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
return $product->get_meta( 'ppcp_subscription_plan' )['id'];
}
}
$cart = WC()->cart ?? null;
if ( ! $cart || $cart->is_empty() ) {
return '';
}
$items = $cart->get_cart_contents();
foreach ( $items as $item ) {
$product = wc_get_product( $item['product_id'] );
assert( $product instanceof WC_Product );
if ( $product->get_type() === 'subscription' && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
return $product->get_meta( 'ppcp_subscription_plan' )['id'];
}
}
return '';
}
/**
* Returns the intent.
*
* @return string
* @throws NotFoundException If intent is not found.
*/
private function intent(): string {
$intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture';
$product_intent = $this->subscription_helper->current_product_is_subscription() ? 'authorize' : $intent;
$other_context_intent = $this->subscription_helper->cart_contains_subscription() ? 'authorize' : $intent;
$subscription_mode = $this->settings->has( 'subscriptions_mode' ) ? $this->settings->get( 'subscriptions_mode' ) : '';
if ( $this->subscription_helper->need_subscription_intent( $subscription_mode ) ) {
return 'subscription';
}
return $this->context() === 'product' ? $product_intent : $other_context_intent;
}
/** /**
* Returns the ID of WC order on the order-pay page, or 0. * Returns the ID of WC order on the order-pay page, or 0.
* *

View file

@ -22,22 +22,15 @@ interface SmartButtonInterface {
public function render_wrapper(): bool; public function render_wrapper(): bool;
/** /**
* Whether the scripts should be loaded. * Whether any of our scripts (for DCC or product, mini-cart, non-block cart/checkout) should be loaded.
*/ */
public function should_load(): bool; public function should_load_ppcp_script(): bool;
/** /**
* Enqueues the necessary scripts. * Enqueues our scripts/styles (for DCC and product, mini-cart and non-block cart/checkout)
*/ */
public function enqueue(): void; public function enqueue(): void;
/**
* Whether the running installation could save vault tokens or not.
*
* @return bool
*/
public function can_save_vault_token(): bool;
/** /**
* The configuration for the smart buttons. * The configuration for the smart buttons.
* *

View file

@ -9,6 +9,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button; namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
@ -63,14 +64,12 @@ class ButtonModule implements ModuleInterface {
add_action( add_action(
'wp_enqueue_scripts', 'wp_enqueue_scripts',
static function () use ( $c ) { static function () use ( $c ) {
$smart_button = $c->get( 'button.smart-button' ); $smart_button = $c->get( 'button.smart-button' );
/** assert( $smart_button instanceof SmartButtonInterface );
* The Smart Button.
* if ( $smart_button->should_load_ppcp_script() ) {
* @var SmartButtonInterface $smart_button $smart_button->enqueue();
*/ }
$smart_button->enqueue();
} }
); );
@ -147,6 +146,16 @@ class ButtonModule implements ModuleInterface {
} }
); );
add_action(
'wc_ajax_' . ApproveSubscriptionEndpoint::ENDPOINT,
static function () use ( $container ) {
$endpoint = $container->get( 'button.endpoint.approve-subscription' );
assert( $endpoint instanceof ApproveSubscriptionEndpoint );
$endpoint->handle_request();
}
);
add_action( add_action(
'wc_ajax_' . CreateOrderEndpoint::ENDPOINT, 'wc_ajax_' . CreateOrderEndpoint::ENDPOINT,
static function () use ( $container ) { static function () use ( $container ) {

View file

@ -180,7 +180,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
); );
} }
$this->session_handler->replace_order( $order ); $this->session_handler->replace_order( $order );
wp_send_json_success( $order ); wp_send_json_success();
} }
if ( $this->order_helper->contains_physical_goods( $order ) && ! $order->status()->is( OrderStatus::APPROVED ) && ! $order->status()->is( OrderStatus::CREATED ) ) { if ( $this->order_helper->contains_physical_goods( $order ) && ! $order->status()->is( OrderStatus::APPROVED ) && ! $order->status()->is( OrderStatus::CREATED ) ) {
@ -198,7 +198,7 @@ class ApproveOrderEndpoint implements EndpointInterface {
$this->session_handler->replace_funding_source( $funding_source ); $this->session_handler->replace_funding_source( $funding_source );
$this->session_handler->replace_order( $order ); $this->session_handler->replace_order( $order );
wp_send_json_success( $order ); wp_send_json_success();
return true; return true;
} catch ( Exception $error ) { } catch ( Exception $error ) {
$this->logger->error( 'Order approve failed: ' . $error->getMessage() ); $this->logger->error( 'Order approve failed: ' . $error->getMessage() );

View file

@ -0,0 +1,94 @@
<?php
/**
* Endpoint to handle PayPal Subscription created.
*
* @package WooCommerce\PayPalCommerce\Button\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button\Endpoint;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
/**
* Class ApproveSubscriptionEndpoint
*/
class ApproveSubscriptionEndpoint implements EndpointInterface {
const ENDPOINT = 'ppc-approve-subscription';
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/**
* The session handler.
*
* @var SessionHandler
*/
private $session_handler;
/**
* ApproveSubscriptionEndpoint constructor.
*
* @param RequestData $request_data The request data helper.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param SessionHandler $session_handler The session handler.
*/
public function __construct(
RequestData $request_data,
OrderEndpoint $order_endpoint,
SessionHandler $session_handler
) {
$this->request_data = $request_data;
$this->order_endpoint = $order_endpoint;
$this->session_handler = $session_handler;
}
/**
* The nonce.
*
* @return string
*/
public static function nonce(): string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return bool
* @throws RuntimeException When order not found or handling failed.
*/
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' )
);
}
$order = $this->order_endpoint->order( $data['order_id'] );
$this->session_handler->replace_order( $order );
if ( isset( $data['subscription_id'] ) ) {
WC()->session->set( 'ppcp_subscription_id', $data['subscription_id'] );
}
wp_send_json_success();
return true;
}
}

View file

@ -138,6 +138,20 @@ class CreateOrderEndpoint implements EndpointInterface {
*/ */
protected $early_validation_enabled; protected $early_validation_enabled;
/**
* The contexts that should have the Pay Now button.
*
* @var string[]
*/
private $pay_now_contexts;
/**
* If true, the shipping methods are sent to PayPal allowing the customer to select it inside the popup.
*
* @var bool
*/
private $handle_shipping_in_paypal;
/** /**
* The logger. * The logger.
* *
@ -159,6 +173,8 @@ class CreateOrderEndpoint implements EndpointInterface {
* @param bool $registration_needed Whether a new user must be registered during checkout. * @param bool $registration_needed Whether a new user must be registered during checkout.
* @param string $card_billing_data_mode The value of card_billing_data_mode from the settings. * @param string $card_billing_data_mode The value of card_billing_data_mode from the settings.
* @param bool $early_validation_enabled Whether to execute WC validation of the checkout form. * @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 LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
*/ */
public function __construct( public function __construct(
@ -173,6 +189,8 @@ class CreateOrderEndpoint implements EndpointInterface {
bool $registration_needed, bool $registration_needed,
string $card_billing_data_mode, string $card_billing_data_mode,
bool $early_validation_enabled, bool $early_validation_enabled,
array $pay_now_contexts,
bool $handle_shipping_in_paypal,
LoggerInterface $logger LoggerInterface $logger
) { ) {
@ -187,6 +205,8 @@ class CreateOrderEndpoint implements EndpointInterface {
$this->registration_needed = $registration_needed; $this->registration_needed = $registration_needed;
$this->card_billing_data_mode = $card_billing_data_mode; $this->card_billing_data_mode = $card_billing_data_mode;
$this->early_validation_enabled = $early_validation_enabled; $this->early_validation_enabled = $early_validation_enabled;
$this->pay_now_contexts = $pay_now_contexts;
$this->handle_shipping_in_paypal = $handle_shipping_in_paypal;
$this->logger = $logger; $this->logger = $logger;
} }
@ -226,7 +246,7 @@ class CreateOrderEndpoint implements EndpointInterface {
} }
$this->purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); $this->purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
} else { } else {
$this->purchase_unit = $this->purchase_unit_factory->from_wc_cart(); $this->purchase_unit = $this->purchase_unit_factory->from_wc_cart( null, $this->handle_shipping_in_paypal );
// The cart does not have any info about payment method, so we must handle free trial here. // The cart does not have any info about payment method, so we must handle free trial here.
if ( ( if ( (
@ -272,7 +292,7 @@ class CreateOrderEndpoint implements EndpointInterface {
! $this->early_order_handler->should_create_early_order() ! $this->early_order_handler->should_create_early_order()
|| $this->registration_needed || $this->registration_needed
|| isset( $data['createaccount'] ) && '1' === $data['createaccount'] ) { || isset( $data['createaccount'] ) && '1' === $data['createaccount'] ) {
wp_send_json_success( $order->to_array() ); wp_send_json_success( $this->make_response( $order ) );
} }
$this->early_order_handler->register_for_order( $order ); $this->early_order_handler->register_for_order( $order );
@ -284,7 +304,7 @@ class CreateOrderEndpoint implements EndpointInterface {
$wc_order->save_meta_data(); $wc_order->save_meta_data();
} }
wp_send_json_success( $order->to_array() ); wp_send_json_success( $this->make_response( $order ) );
return true; return true;
} catch ( ValidationException $error ) { } catch ( ValidationException $error ) {
@ -342,7 +362,7 @@ class CreateOrderEndpoint implements EndpointInterface {
* during the "onApprove"-JS callback or the webhook listener. * during the "onApprove"-JS callback or the webhook listener.
*/ */
if ( ! $this->early_order_handler->should_create_early_order() ) { if ( ! $this->early_order_handler->should_create_early_order() ) {
wp_send_json_success( $order->to_array() ); wp_send_json_success( $this->make_response( $order ) );
} }
$this->early_order_handler->register_for_order( $order ); $this->early_order_handler->register_for_order( $order );
return $data; return $data;
@ -385,6 +405,9 @@ class CreateOrderEndpoint implements EndpointInterface {
$funding_source $funding_source
); );
$action = in_array( $this->parsed_request_data['context'], $this->pay_now_contexts, true ) ?
ApplicationContext::USER_ACTION_PAY_NOW : ApplicationContext::USER_ACTION_CONTINUE;
if ( 'card' === $funding_source ) { if ( 'card' === $funding_source ) {
if ( CardBillingMode::MINIMAL_INPUT === $this->card_billing_data_mode ) { if ( CardBillingMode::MINIMAL_INPUT === $this->card_billing_data_mode ) {
if ( ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS === $shipping_preference ) { if ( ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS === $shipping_preference ) {
@ -410,7 +433,9 @@ class CreateOrderEndpoint implements EndpointInterface {
$shipping_preference, $shipping_preference,
$payer, $payer,
null, null,
$this->payment_method() $this->payment_method(),
'',
$action
); );
} catch ( PayPalApiException $exception ) { } catch ( PayPalApiException $exception ) {
// Looks like currently there is no proper way to validate the shipping address for PayPal, // Looks like currently there is no proper way to validate the shipping address for PayPal,
@ -544,4 +569,17 @@ class CreateOrderEndpoint implements EndpointInterface {
); );
} }
} }
/**
* Returns the response data for success response.
*
* @param Order $order The PayPal order.
* @return array
*/
private function make_response( Order $order ): array {
return array(
'id' => $order->id(),
'custom_id' => $order->purchase_units()[0]->custom_id(),
);
}
} }

View file

@ -17,24 +17,32 @@ trait ContextTrait {
* @return string * @return string
*/ */
protected function context(): string { protected function context(): string {
$context = 'mini-cart';
if ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) { if ( is_product() || wc_post_content_has_shortcode( 'product_page' ) ) {
$context = 'product'; return 'product';
}
// has_block may not work if called too early, such as during the block registration.
if ( has_block( 'woocommerce/cart' ) ) {
return 'cart-block';
} }
if ( is_cart() ) { if ( is_cart() ) {
$context = 'cart'; return 'cart';
}
if ( is_checkout() && ! $this->is_paypal_continuation() ) {
$context = 'checkout';
} }
if ( is_checkout_pay_page() ) { if ( is_checkout_pay_page() ) {
$context = 'pay-now'; return 'pay-now';
} }
return $context; if ( has_block( 'woocommerce/checkout' ) ) {
return 'checkout-block';
}
if ( ( is_checkout() ) && ! $this->is_paypal_continuation() ) {
return 'checkout';
}
return 'mini-cart';
} }
/** /**

View file

@ -49,6 +49,9 @@ return array(
'ppcp-gateway-settings', 'ppcp-gateway-settings',
'ppcp-webhooks-status-page', 'ppcp-webhooks-status-page',
'ppcp-tracking', 'ppcp-tracking',
'ppcp-fraudnet',
'ppcp-gzd-compat',
'ppcp-clear-db',
); );
}, },

View file

@ -67,8 +67,12 @@ class PPECHelper {
* @return bool * @return bool
*/ */
public static function site_has_ppec_subscriptions() { public static function site_has_ppec_subscriptions() {
global $wpdb; $has_ppec_subscriptions = get_transient( 'ppcp_has_ppec_subscriptions' );
if ( $has_ppec_subscriptions !== false ) {
return $has_ppec_subscriptions === 'true';
}
global $wpdb;
$result = $wpdb->get_var( $result = $wpdb->get_var(
$wpdb->prepare( $wpdb->prepare(
"SELECT 1 FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID "SELECT 1 FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID
@ -80,6 +84,12 @@ class PPECHelper {
) )
); );
set_transient(
'ppcp_has_ppec_subscriptions',
! empty( $result ) ? 'true' : 'false',
3 * MONTH_IN_SECONDS
);
return ! empty( $result ); return ! empty( $result );
} }
@ -92,7 +102,9 @@ class PPECHelper {
/** /**
* The filter returning whether the compatibility layer for PPEC Subscriptions should be initialized. * The filter returning whether the compatibility layer for PPEC Subscriptions should be initialized.
*/ */
return ( ! self::is_gateway_available() ) && self::site_has_ppec_subscriptions() && apply_filters( 'woocommerce_paypal_payments_process_legacy_subscriptions', true ); return ( ! self::is_gateway_available() )
&& self::site_has_ppec_subscriptions()
&& apply_filters( 'woocommerce_paypal_payments_process_legacy_subscriptions', true );
} }
} }

View file

@ -69,7 +69,7 @@ class SubscriptionsHandler {
/** /**
* Adds a mock gateway to disguise as PPEC when needed. Hooked onto `woocommerce_payment_gateways`. * Adds a mock gateway to disguise as PPEC when needed. Hooked onto `woocommerce_payment_gateways`.
* The mock gateway fixes display issues where subscriptions paid via PPEC appear as "via Manual Renewal" and also * The mock gateway fixes display issues where subscriptions paid via PPEC appear as "via Manual Renewal" and also
* prevents Subscriptions from automatically changing the payment method to "manual" when a subscription is edited. * prevents subscriptions from automatically changing the payment method to "manual" when a subscription is edited.
* *
* @param array $gateways List of gateways. * @param array $gateways List of gateways.
* @return array * @return array

View file

@ -123,6 +123,11 @@ class LoginSellerEndpoint implements EndpointInterface {
public function handle_request(): bool { public function handle_request(): bool {
try { try {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Not admin.', 403 );
return false;
}
$data = $this->request_data->read_request( $this->nonce() ); $data = $this->request_data->read_request( $this->nonce() );
$is_sandbox = isset( $data['env'] ) && 'sandbox' === $data['env']; $is_sandbox = isset( $data['env'] ) && 'sandbox' === $data['env'];
$this->settings->set( 'sandbox_on', $is_sandbox ); $this->settings->set( 'sandbox_on', $is_sandbox );

View file

@ -107,6 +107,11 @@ class PayUponInvoiceEndpoint implements EndpointInterface {
* @throws NotFoundException When order not found or handling failed. * @throws NotFoundException When order not found or handling failed.
*/ */
public function handle_request(): bool { public function handle_request(): bool {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Not admin.', 403 );
return false;
}
$signup_links = array(); $signup_links = array();
try { try {

View file

@ -85,6 +85,11 @@ class OrderTrackingEndpoint {
* Handles the request. * Handles the request.
*/ */
public function handle_request(): void { public function handle_request(): void {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Not admin.', 403 );
return;
}
try { try {
$data = $this->request_data->read_request( $this->nonce() ); $data = $this->request_data->read_request( $this->nonce() );
$action = $data['action']; $action = $data['action'];

View file

@ -15,17 +15,7 @@ use WooCommerce\PayPalCommerce\Session\Cancellation\CancelView;
return array( return array(
'session.handler' => function ( ContainerInterface $container ) : SessionHandler { 'session.handler' => function ( ContainerInterface $container ) : SessionHandler {
return new SessionHandler();
if ( is_null( WC()->session ) ) {
return new SessionHandler();
}
$result = WC()->session->get( SessionHandler::ID );
if ( is_a( $result, SessionHandler::class ) ) {
return $result;
}
$session_handler = new SessionHandler();
WC()->session->set( SessionHandler::ID, $session_handler );
return $session_handler;
}, },
'session.cancellation.view' => function ( ContainerInterface $container ) : CancelView { 'session.cancellation.view' => function ( ContainerInterface $container ) : CancelView {
return new CancelView( return new CancelView(

View file

@ -16,6 +16,8 @@ use WooCommerce\PayPalCommerce\Session\SessionHandler;
*/ */
class CancelController { class CancelController {
public const NONCE = 'ppcp-cancel';
/** /**
* The Session handler. * The Session handler.
* *
@ -49,12 +51,11 @@ class CancelController {
* Runs the controller. * Runs the controller.
*/ */
public function run() { public function run() {
$param_name = 'ppcp-cancel'; $param_name = self::NONCE;
$nonce = 'ppcp-cancel-' . get_current_user_id();
if ( isset( $_GET[ $param_name ] ) && // Input var ok. if ( isset( $_GET[ $param_name ] ) && // Input var ok.
wp_verify_nonce( wp_verify_nonce(
sanitize_text_field( wp_unslash( $_GET[ $param_name ] ) ), // Input var ok. sanitize_text_field( wp_unslash( $_GET[ $param_name ] ) ), // Input var ok.
$nonce self::NONCE
) )
) { // Input var ok. ) { // Input var ok.
$this->session_handler->destroy_session_data(); $this->session_handler->destroy_session_data();
@ -74,11 +75,12 @@ class CancelController {
return; // Ignore for card buttons. return; // Ignore for card buttons.
} }
$url = add_query_arg( array( $param_name => wp_create_nonce( $nonce ) ), wc_get_checkout_url() ); $url = add_query_arg( array( $param_name => wp_create_nonce( self::NONCE ) ), wc_get_checkout_url() );
add_action( add_action(
'woocommerce_review_order_after_submit', 'woocommerce_review_order_after_submit',
function () use ( $url ) { function () use ( $url ) {
$this->view->render_session_cancellation( $url, $this->session_handler->funding_source() ); // phpcs:ignore WordPress.Security.EscapeOutput
echo $this->view->render_session_cancellation( $url, $this->session_handler->funding_source() );
} }
); );
} }

View file

@ -50,7 +50,8 @@ class CancelView {
* @param string $url The URL. * @param string $url The URL.
* @param string|null $funding_source The ID of the funding source, such as 'venmo'. * @param string|null $funding_source The ID of the funding source, such as 'venmo'.
*/ */
public function render_session_cancellation( string $url, ?string $funding_source ) { public function render_session_cancellation( string $url, ?string $funding_source ): string {
ob_start();
?> ?>
<p id="ppcp-cancel" <p id="ppcp-cancel"
class="has-text-align-center ppcp-cancel" class="has-text-align-center ppcp-cancel"
@ -73,5 +74,6 @@ class CancelView {
?> ?>
</p> </p>
<?php <?php
return (string) ob_get_clean();
} }
} }

View file

@ -16,7 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
*/ */
class SessionHandler { class SessionHandler {
const ID = 'ppcp'; private const SESSION_KEY = 'ppcp';
/** /**
* The Order. * The Order.
@ -33,7 +33,7 @@ class SessionHandler {
private $bn_code = ''; private $bn_code = '';
/** /**
* If PayPal respondes with INSTRUMENT_DECLINED, we only * If PayPal responds with INSTRUMENT_DECLINED, we only
* want to go max. three times through the process of trying again. * want to go max. three times through the process of trying again.
* *
* @var int * @var int
@ -53,6 +53,8 @@ class SessionHandler {
* @return Order|null * @return Order|null
*/ */
public function order() { public function order() {
$this->load_session();
return $this->order; return $this->order;
} }
@ -60,13 +62,13 @@ class SessionHandler {
* Replaces the current order. * Replaces the current order.
* *
* @param Order $order The new order. * @param Order $order The new order.
*
* @return SessionHandler
*/ */
public function replace_order( Order $order ) : SessionHandler { public function replace_order( Order $order ): void {
$this->load_session();
$this->order = $order; $this->order = $order;
$this->store_session(); $this->store_session();
return $this;
} }
/** /**
@ -75,6 +77,8 @@ class SessionHandler {
* @return string * @return string
*/ */
public function bn_code() : string { public function bn_code() : string {
$this->load_session();
return $this->bn_code; return $this->bn_code;
} }
@ -82,13 +86,13 @@ class SessionHandler {
* Replaces the BN Code. * Replaces the BN Code.
* *
* @param string $bn_code The new BN Code. * @param string $bn_code The new BN Code.
*
* @return SessionHandler
*/ */
public function replace_bn_code( string $bn_code ) : SessionHandler { public function replace_bn_code( string $bn_code ) : void {
$this->load_session();
$this->bn_code = $bn_code; $this->bn_code = $bn_code;
$this->store_session(); $this->store_session();
return $this;
} }
/** /**
@ -97,6 +101,8 @@ class SessionHandler {
* @return string|null * @return string|null
*/ */
public function funding_source(): ?string { public function funding_source(): ?string {
$this->load_session();
return $this->funding_source; return $this->funding_source;
} }
@ -104,13 +110,13 @@ class SessionHandler {
* Replaces the funding source of the current checkout. * Replaces the funding source of the current checkout.
* *
* @param string|null $funding_source The funding source. * @param string|null $funding_source The funding source.
*
* @return SessionHandler
*/ */
public function replace_funding_source( ?string $funding_source ): SessionHandler { public function replace_funding_source( ?string $funding_source ): void {
$this->load_session();
$this->funding_source = $funding_source; $this->funding_source = $funding_source;
$this->store_session(); $this->store_session();
return $this;
} }
/** /**
@ -119,18 +125,20 @@ class SessionHandler {
* @return int * @return int
*/ */
public function insufficient_funding_tries() : int { public function insufficient_funding_tries() : int {
$this->load_session();
return $this->insufficient_funding_tries; return $this->insufficient_funding_tries;
} }
/** /**
* Increments the number of tries, the customer has done in this session. * Increments the number of tries, the customer has done in this session.
*
* @return SessionHandler
*/ */
public function increment_insufficient_funding_tries() : SessionHandler { public function increment_insufficient_funding_tries(): void {
$this->load_session();
$this->insufficient_funding_tries++; $this->insufficient_funding_tries++;
$this->store_session(); $this->store_session();
return $this;
} }
/** /**
@ -148,9 +156,52 @@ class SessionHandler {
} }
/** /**
* Stores the session. * Stores the data into the WC session.
*/ */
private function store_session() { private function store_session(): void {
WC()->session->set( self::ID, $this ); WC()->session->set( self::SESSION_KEY, self::make_array( $this ) );
}
/**
* Loads the data from the session.
*/
private function load_session(): void {
if ( isset( WC()->session ) ) {
$data = WC()->session->get( self::SESSION_KEY );
} else {
$data = array();
}
if ( $data instanceof SessionHandler ) {
$data = self::make_array( $data );
} elseif ( ! is_array( $data ) ) {
$data = array();
}
$this->order = $data['order'] ?? null;
if ( ! $this->order instanceof Order ) {
$this->order = null;
}
$this->bn_code = (string) ( $data['bn_code'] ?? '' );
$this->insufficient_funding_tries = (int) ( $data['insufficient_funding_tries'] ?? '' );
$this->funding_source = $data['funding_source'] ?? null;
if ( ! is_string( $this->funding_source ) ) {
$this->funding_source = null;
}
}
/**
* Converts given SessionHandler object into an array.
*
* @param SessionHandler $obj The object to convert.
* @return array
*/
private static function make_array( SessionHandler $obj ): array {
return array(
'order' => $obj->order,
'bn_code' => $obj->bn_code,
'insufficient_funding_tries' => $obj->insufficient_funding_tries,
'funding_source' => $obj->funding_source,
);
} }
} }

View file

@ -43,4 +43,15 @@ return array(
$endpoint = $container->get( 'api.endpoint.payment-token' ); $endpoint = $container->get( 'api.endpoint.payment-token' );
return new PaymentTokenRepository( $factory, $endpoint ); return new PaymentTokenRepository( $factory, $endpoint );
}, },
'subscription.api-handler' => static function( ContainerInterface $container ): SubscriptionsApiHandler {
return new SubscriptionsApiHandler(
$container->get( 'api.endpoint.catalog-products' ),
$container->get( 'api.factory.product' ),
$container->get( 'api.endpoint.billing-plans' ),
$container->get( 'api.factory.billing-cycle' ),
$container->get( 'api.factory.payment-preferences' ),
$container->get( 'api.shop.currency' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
); );

View file

@ -127,4 +127,27 @@ class SubscriptionHelper {
return true; return true;
} }
/**
* Checks whether subscription needs subscription intent.
*
* @param string $subscription_mode The subscriptiopn mode.
* @return bool
*/
public function need_subscription_intent( string $subscription_mode ): bool {
if ( defined( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) && ! PPCP_FLAG_SUBSCRIPTIONS_API ) {
return false;
}
if ( $subscription_mode === 'subscriptions_api' ) {
if (
$this->current_product_is_subscription()
|| ( ( is_cart() || is_checkout() ) && $this->cart_contains_subscription() )
) {
return true;
}
}
return false;
}
} }

View file

@ -162,13 +162,6 @@ class RenewalHandler {
return; return;
} }
$this->logger->info(
sprintf(
'Renewal for order %d is completed.',
$wc_order->get_id()
)
);
} }
/** /**
@ -186,6 +179,7 @@ class RenewalHandler {
if ( ! $token ) { if ( ! $token ) {
return; return;
} }
$purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order );
$payer = $this->payer_factory->from_customer( $customer ); $payer = $this->payer_factory->from_customer( $customer );
$shipping_preference = $this->shipping_preference_factory->from_state( $shipping_preference = $this->shipping_preference_factory->from_state(
@ -217,6 +211,13 @@ class RenewalHandler {
if ( $this->capture_authorized_downloads( $order ) ) { if ( $this->capture_authorized_downloads( $order ) ) {
$this->authorized_payments_processor->capture_authorized_payment( $wc_order ); $this->authorized_payments_processor->capture_authorized_payment( $wc_order );
} }
$this->logger->info(
sprintf(
'Renewal for order %d is completed.',
$wc_order->get_id()
)
);
} }
/** /**

View file

@ -9,6 +9,11 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription; namespace WooCommerce\PayPalCommerce\Subscription;
use Exception;
use WC_Product;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@ -102,8 +107,19 @@ class SubscriptionModule implements ModuleInterface {
function( array $data ) use ( $c ) { function( array $data ) use ( $c ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing // phpcs:ignore WordPress.Security.NonceVerification.Missing
$wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) ); $wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) );
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$subscription_id = wc_clean( wp_unslash( $_POST['post_ID'] ?? '' ) );
if ( ! $subscription_id ) {
return $data;
}
$subscription = wc_get_order( $subscription_id );
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return $data;
}
if ( if (
$wc_order_action === 'wcs_process_renewal' $wc_order_action === 'wcs_process_renewal' && $subscription->get_payment_method() === CreditCardGateway::ID
&& isset( $data['payment_source']['token'] ) && $data['payment_source']['token']['type'] === 'PAYMENT_METHOD_TOKEN' && isset( $data['payment_source']['token'] ) && $data['payment_source']['token']['type'] === 'PAYMENT_METHOD_TOKEN'
&& isset( $data['payment_source']['token']['source']->card ) && isset( $data['payment_source']['token']['source']->card )
) { ) {
@ -135,6 +151,10 @@ class SubscriptionModule implements ModuleInterface {
return $data; return $data;
} }
); );
if ( defined( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) && PPCP_FLAG_SUBSCRIPTIONS_API ) {
$this->subscriptions_api_integration( $c );
}
} }
/** /**
@ -265,8 +285,8 @@ class SubscriptionModule implements ModuleInterface {
SubscriptionHelper $subscription_helper SubscriptionHelper $subscription_helper
) { ) {
if ( $settings->has( 'vault_enabled' ) if ( $settings->has( 'vault_enabled_dcc' )
&& $settings->get( 'vault_enabled' ) && $settings->get( 'vault_enabled_dcc' )
&& $subscription_helper->is_subscription_change_payment() && $subscription_helper->is_subscription_change_payment()
&& CreditCardGateway::ID === $id && CreditCardGateway::ID === $id
) { ) {
@ -303,4 +323,406 @@ class SubscriptionModule implements ModuleInterface {
return $default_fields; return $default_fields;
} }
/**
* Adds PayPal subscriptions API integration.
*
* @param ContainerInterface $c The container.
* @return void
* @throws Exception When something went wrong.
*/
protected function subscriptions_api_integration( ContainerInterface $c ): void {
add_action(
'save_post',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $product_id ) use ( $c ) {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
try {
$subscriptions_mode = $settings->get( 'subscriptions_mode' );
} catch ( NotFoundException $exception ) {
return;
}
$nonce = wc_clean( wp_unslash( $_POST['_wcsnonce'] ?? '' ) );
if (
$subscriptions_mode !== 'subscriptions_api'
|| ! is_string( $nonce )
|| ! wp_verify_nonce( $nonce, 'wcs_subscription_meta' ) ) {
return;
}
$product = wc_get_product( $product_id );
if ( ! is_a( $product, WC_Product::class ) ) {
return;
}
$enable_subscription_product = wc_clean( wp_unslash( $_POST['_ppcp_enable_subscription_product'] ?? '' ) );
$product->update_meta_data( '_ppcp_enable_subscription_product', $enable_subscription_product );
$product->save();
if ( $product->get_type() === 'subscription' && $enable_subscription_product === 'yes' ) {
$subscriptions_api_handler = $c->get( 'subscription.api-handler' );
assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler );
if ( $product->meta_exists( 'ppcp_subscription_product' ) && $product->meta_exists( 'ppcp_subscription_plan' ) ) {
$subscriptions_api_handler->update_product( $product );
$subscriptions_api_handler->update_plan( $product );
return;
}
if ( ! $product->meta_exists( 'ppcp_subscription_product' ) ) {
$subscriptions_api_handler->create_product( $product );
}
if ( $product->meta_exists( 'ppcp_subscription_product' ) && ! $product->meta_exists( 'ppcp_subscription_plan' ) ) {
$subscription_plan_name = wc_clean( wp_unslash( $_POST['_ppcp_subscription_plan_name'] ?? '' ) );
if ( ! is_string( $subscription_plan_name ) ) {
return;
}
$product->update_meta_data( '_ppcp_subscription_plan_name', $subscription_plan_name );
$product->save();
$subscriptions_api_handler->create_plan( $subscription_plan_name, $product );
}
}
},
12
);
add_action(
'woocommerce_process_shop_subscription_meta',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $id, $subscription ) use ( $c ) {
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id ) {
$subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' );
assert( $subscriptions_endpoint instanceof BillingSubscriptions );
if ( $subscription->get_status() === 'cancelled' ) {
try {
$subscriptions_endpoint->cancel( $subscription_id );
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not cancel subscription product on PayPal. ' . $error );
}
}
if ( $subscription->get_status() === 'pending-cancel' ) {
try {
$subscriptions_endpoint->suspend( $subscription_id );
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not suspend subscription product on PayPal. ' . $error );
}
}
if ( $subscription->get_status() === 'active' ) {
try {
$current_subscription = $subscriptions_endpoint->subscription( $subscription_id );
if ( $current_subscription->status === 'SUSPENDED' ) {
$subscriptions_endpoint->activate( $subscription_id );
}
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not reactivate subscription product on PayPal. ' . $error );
}
}
}
},
20,
2
);
add_filter(
'woocommerce_order_actions',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $actions, $subscription ): array {
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return $actions;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id && isset( $actions['wcs_process_renewal'] ) ) {
unset( $actions['wcs_process_renewal'] );
}
return $actions;
},
20,
2
);
add_filter(
'wcs_view_subscription_actions',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $actions, $subscription ): array {
if ( ! is_a( $subscription, WC_Subscription::class ) ) {
return $actions;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id && $subscription->get_status() === 'active' ) {
$url = wp_nonce_url(
add_query_arg(
array(
'change_subscription_to' => 'cancelled',
'ppcp_cancel_subscription' => $subscription->get_id(),
)
),
'ppcp_cancel_subscription_nonce'
);
array_unshift(
$actions,
array(
'url' => esc_url( $url ),
'name' => esc_html__( 'Cancel', 'woocommerce-paypal-payments' ),
)
);
$actions['cancel']['name'] = esc_html__( 'Suspend', 'woocommerce-paypal-payments' );
unset( $actions['subscription_renewal_early'] );
}
return $actions;
},
11,
2
);
add_action(
'wp_loaded',
function() use ( $c ) {
if ( ! function_exists( 'wcs_get_subscription' ) ) {
return;
}
$cancel_subscription_id = wc_clean( wp_unslash( $_GET['ppcp_cancel_subscription'] ?? '' ) );
$subscription = wcs_get_subscription( absint( $cancel_subscription_id ) );
if ( ! wcs_is_subscription( $subscription ) || $subscription === false ) {
return;
}
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
$nonce = wc_clean( wp_unslash( $_GET['_wpnonce'] ?? '' ) );
if ( ! is_string( $nonce ) ) {
return;
}
if (
$subscription_id
&& $cancel_subscription_id
&& $nonce
) {
if (
! wp_verify_nonce( $nonce, 'ppcp_cancel_subscription_nonce' )
|| ! user_can( get_current_user_id(), 'edit_shop_subscription_status', $subscription->get_id() )
) {
return;
}
$subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' );
$subscription_id = $subscription->get_meta( 'ppcp_subscription' );
try {
$subscriptions_endpoint->cancel( $subscription_id );
$subscription->update_status( 'cancelled' );
$subscription->add_order_note( __( 'Subscription cancelled by the subscriber from their account page.', 'woocommerce-paypal-payments' ) );
wc_add_notice( __( 'Your subscription has been cancelled.', 'woocommerce-paypal-payments' ) );
wp_safe_redirect( $subscription->get_view_order_url() );
exit;
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not cancel subscription product on PayPal. ' . $error );
}
}
},
100
);
add_action(
'woocommerce_subscription_before_actions',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $subscription ) use ( $c ) {
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id ) {
$environment = $c->get( 'onboarding.environment' );
$host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com';
?>
<tr>
<td><?php esc_html_e( 'PayPal Subscription', 'woocommerce-paypal-payments' ); ?></td>
<td>
<a href="<?php echo esc_url( $host . "/myaccount/autopay/connect/{$subscription_id}" ); ?>" id="ppcp-subscription-id" target="_blank"><?php echo esc_html( $subscription_id ); ?></a>
</td>
</tr>
<?php
}
}
);
add_action(
'woocommerce_product_options_general_product_data',
function() use ( $c ) {
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
try {
$subscriptions_mode = $settings->get( 'subscriptions_mode' );
if ( $subscriptions_mode === 'subscriptions_api' ) {
/**
* Needed for getting global post object.
*
* @psalm-suppress InvalidGlobal
*/
global $post;
$product = wc_get_product( $post->ID );
if ( ! is_a( $product, WC_Product::class ) ) {
return;
}
$enable_subscription_product = $product->get_meta( '_ppcp_enable_subscription_product' );
$subscription_plan_name = $product->get_meta( '_ppcp_subscription_plan_name' );
echo '<div class="options_group subscription_pricing show_if_subscription hidden">';
echo '<p class="form-field"><label for="_ppcp_enable_subscription_product">Connect to PayPal</label><input type="checkbox" id="ppcp_enable_subscription_product" name="_ppcp_enable_subscription_product" value="yes" ' . checked( $enable_subscription_product, 'yes', false ) . '/><span class="description">Connect Product to PayPal Subscriptions Plan</span></p>';
$subscription_product = $product->get_meta( 'ppcp_subscription_product' );
$subscription_plan = $product->get_meta( 'ppcp_subscription_plan' );
if ( $subscription_product && $subscription_plan ) {
$environment = $c->get( 'onboarding.environment' );
$host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com';
echo '<p class="form-field"><label>Product</label><a href="' . esc_url( $host . '/billing/plans/products/' . $subscription_product['id'] ) . '" target="_blank">' . esc_attr( $subscription_product['id'] ) . '</a></p>';
echo '<p class="form-field"><label>Plan</label><a href="' . esc_url( $host . '/billing/plans/' . $subscription_plan['id'] ) . '" target="_blank">' . esc_attr( $subscription_plan['id'] ) . '</a></p>';
} else {
echo '<p class="form-field"><label for="_ppcp_subscription_plan_name">Plan Name</label><input type="text" class="short" id="ppcp_subscription_plan_name" name="_ppcp_subscription_plan_name" value="' . esc_attr( $subscription_plan_name ) . '"></p>';
}
echo '</div>';
}
} catch ( NotFoundException $exception ) {
return;
}
}
);
add_filter(
'woocommerce_order_data_store_cpt_get_orders_query',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $query, $query_vars ): array {
if ( ! empty( $query_vars['ppcp_subscription'] ) ) {
$query['meta_query'][] = array(
'key' => 'ppcp_subscription',
'value' => esc_attr( $query_vars['ppcp_subscription'] ),
);
}
return $query;
},
10,
2
);
add_action(
'woocommerce_customer_changed_subscription_to_cancelled',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $subscription ) use ( $c ) {
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id ) {
$subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' );
assert( $subscriptions_endpoint instanceof BillingSubscriptions );
try {
$subscriptions_endpoint->suspend( $subscription_id );
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not suspend subscription product on PayPal. ' . $error );
}
}
}
);
add_action(
'woocommerce_customer_changed_subscription_to_active',
/**
* Param types removed to avoid third-party issues.
*
* @psalm-suppress MissingClosureParamType
*/
function( $subscription ) use ( $c ) {
$subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? '';
if ( $subscription_id ) {
$subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' );
assert( $subscriptions_endpoint instanceof BillingSubscriptions );
try {
$subscriptions_endpoint->activate( $subscription_id );
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$logger = $c->get( 'woocommerce.logger.woocommerce' );
$logger->error( 'Could not active subscription product on PayPal. ' . $error );
}
}
}
);
}
} }

View file

@ -0,0 +1,277 @@
<?php
/**
* The subscription module.
*
* @package WooCommerce\PayPalCommerce\Subscription
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Subscription;
use Psr\Log\LoggerInterface;
use WC_Product;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts;
use WooCommerce\PayPalCommerce\ApiClient\Entity\BillingCycle;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory;
/**
* Class SubscriptionsApiHandler
*/
class SubscriptionsApiHandler {
/**
* Catalog products.
*
* @var CatalogProducts
*/
private $products_endpoint;
/**
* Product factory.
*
* @var ProductFactory
*/
private $product_factory;
/**
* Billing plans.
*
* @var BillingPlans
*/
private $billing_plans_endpoint;
/**
* Billing cycle factory.
*
* @var BillingCycleFactory
*/
private $billing_cycle_factory;
/**
* Payment preferences factory.
*
* @var PaymentPreferencesFactory
*/
private $payment_preferences_factory;
/**
* The currency.
*
* @var string
*/
private $currency;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* SubscriptionsApiHandler constructor.
*
* @param CatalogProducts $products_endpoint Products endpoint.
* @param ProductFactory $product_factory Product factory.
* @param BillingPlans $billing_plans_endpoint Billing plans endpoint.
* @param BillingCycleFactory $billing_cycle_factory Billing cycle factory.
* @param PaymentPreferencesFactory $payment_preferences_factory Payment preferences factory.
* @param string $currency The currency.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
CatalogProducts $products_endpoint,
ProductFactory $product_factory,
BillingPlans $billing_plans_endpoint,
BillingCycleFactory $billing_cycle_factory,
PaymentPreferencesFactory $payment_preferences_factory,
string $currency,
LoggerInterface $logger
) {
$this->products_endpoint = $products_endpoint;
$this->product_factory = $product_factory;
$this->billing_plans_endpoint = $billing_plans_endpoint;
$this->billing_cycle_factory = $billing_cycle_factory;
$this->payment_preferences_factory = $payment_preferences_factory;
$this->currency = $currency;
$this->logger = $logger;
}
/**
* Creates a Catalog Product and adds it as WC product meta.
*
* @param WC_Product $product The WC product.
* @return void
*/
public function create_product( WC_Product $product ) {
try {
$subscription_product = $this->products_endpoint->create( $product->get_title(), $product->get_description() );
$product->update_meta_data( 'ppcp_subscription_product', $subscription_product->to_array() );
$product->save();
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$this->logger->error( 'Could not create catalog product on PayPal. ' . $error );
}
}
/**
* Creates a subscription plan.
*
* @param string $plan_name The plan name.
* @param WC_Product $product The WC product.
* @return void
*/
public function create_plan( string $plan_name, WC_Product $product ): void {
try {
$subscription_plan = $this->billing_plans_endpoint->create(
$plan_name,
$product->get_meta( 'ppcp_subscription_product' )['id'] ?? '',
$this->billing_cycles( $product ),
$this->payment_preferences_factory->from_wc_product( $product )->to_array()
);
$product->update_meta_data( 'ppcp_subscription_plan', $subscription_plan->to_array() );
$product->save();
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$this->logger->error( 'Could not create subscription plan on PayPal. ' . $error );
}
}
/**
* Updates a product.
*
* @param WC_Product $product The WC product.
* @return void
*/
public function update_product( WC_Product $product ): void {
try {
$catalog_product_id = $product->get_meta( 'ppcp_subscription_product' )['id'] ?? '';
if ( $catalog_product_id ) {
$catalog_product = $this->products_endpoint->product( $catalog_product_id );
$catalog_product_name = $catalog_product->name() ?: '';
$catalog_product_description = $catalog_product->description() ?: '';
if ( $catalog_product_name !== $product->get_title() || $catalog_product_description !== $product->get_description() ) {
$data = array();
if ( $catalog_product_name !== $product->get_title() ) {
$data[] = (object) array(
'op' => 'replace',
'path' => '/name',
'value' => $product->get_title(),
);
}
if ( $catalog_product_description !== $product->get_description() ) {
$data[] = (object) array(
'op' => 'replace',
'path' => '/description',
'value' => $product->get_description(),
);
}
$this->products_endpoint->update( $catalog_product_id, $data );
}
}
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$this->logger->error( 'Could not update catalog product on PayPal. ' . $error );
}
}
/**
* Updates a plan.
*
* @param WC_Product $product The WC product.
* @return void
*/
public function update_plan( WC_Product $product ): void {
try {
$subscription_plan_id = $product->get_meta( 'ppcp_subscription_plan' )['id'] ?? '';
if ( $subscription_plan_id ) {
$subscription_plan = $this->billing_plans_endpoint->plan( $subscription_plan_id );
$price = $subscription_plan->billing_cycles()[0]->pricing_scheme()['fixed_price']['value'] ?? '';
if ( $price && round( $price, 2 ) !== round( (float) $product->get_price(), 2 ) ) {
$this->billing_plans_endpoint->update_pricing(
$subscription_plan_id,
$this->billing_cycle_factory->from_wc_product( $product )
);
}
}
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) ) {
$error = $exception->get_details( $error );
}
$this->logger->error( 'Could not update subscription plan on PayPal. ' . $error );
}
}
/**
* Returns billing cycles based on WC Subscription product.
*
* @param WC_Product $product The WC Subscription product.
* @return array
*/
private function billing_cycles( WC_Product $product ): array {
$billing_cycles = array();
$sequence = 1;
$trial_length = $product->get_meta( '_subscription_trial_length' ) ?? '';
if ( $trial_length ) {
$billing_cycles[] = ( new BillingCycle(
array(
'interval_unit' => $product->get_meta( '_subscription_trial_period' ),
'interval_count' => $product->get_meta( '_subscription_trial_length' ),
),
$sequence,
'TRIAL',
array(
'fixed_price' => array(
'value' => '0',
'currency_code' => $this->currency,
),
),
1
) )->to_array();
$sequence++;
}
$billing_cycles[] = ( new BillingCycle(
array(
'interval_unit' => $product->get_meta( '_subscription_period' ),
'interval_count' => $product->get_meta( '_subscription_period_interval' ),
),
$sequence,
'REGULAR',
array(
'fixed_price' => array(
'value' => $product->get_meta( '_subscription_price' ),
'currency_code' => $this->currency,
),
),
(int) $product->get_meta( '_subscription_length' )
) )->to_array();
return $billing_cycles;
}
}

View file

@ -81,8 +81,14 @@ class UninstallModule implements ModuleInterface {
"wc_ajax_{$nonce}", "wc_ajax_{$nonce}",
static function () use ( $request_data, $clear_db, $nonce, $option_names, $scheduled_action_names ) { static function () use ( $request_data, $clear_db, $nonce, $option_names, $scheduled_action_names ) {
try { try {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Not admin.', 403 );
return false;
}
// Validate nonce. // Validate nonce.
$request_data->read_request( $nonce ); $request_data->read_request( $nonce );
$clear_db->delete_options( $option_names ); $clear_db->delete_options( $option_names );
$clear_db->clear_scheduled_actions( $scheduled_action_names ); $clear_db->clear_scheduled_actions( $scheduled_action_names );

View file

@ -1,72 +0,0 @@
<?php
/**
* WooCommerce Payment token for PayPal ACDC (Advanced Credit and Debit Card).
*
* @package WooCommerce\PayPalCommerce\Vaulting
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vaulting;
use WC_Payment_Token;
/**
* Class PaymentTokenACDC
*/
class PaymentTokenACDC extends WC_Payment_Token {
/**
* Token Type String.
*
* @var string
*/
protected $type = 'ACDC';
/**
* Stores Credit Card payment token data.
*
* @var array
*/
protected $extra_data = array(
'last4' => '',
'card_type' => '',
);
/**
* Returns the last four digits.
*
* @param string $context The context.
* @return mixed|null
*/
public function get_last4( $context = 'view' ) {
return $this->get_prop( 'last4', $context );
}
/**
* Set the last four digits.
*
* @param string $last4 Last four digits.
*/
public function set_last4( $last4 ) {
$this->set_prop( 'last4', $last4 );
}
/**
* Returns the card type (mastercard, visa, ...).
*
* @param string $context The context.
* @return string Card type
*/
public function get_card_type( $context = 'view' ) {
return $this->get_prop( 'card_type', $context );
}
/**
* Set the card type (mastercard, visa, ...).
*
* @param string $type Credit card type (mastercard, visa, ...).
*/
public function set_card_type( $type ) {
$this->set_prop( 'card_type', $type );
}
}

View file

@ -19,14 +19,12 @@ class PaymentTokenFactory {
* *
* @param string $type The type of WC payment token. * @param string $type The type of WC payment token.
* *
* @return void|PaymentTokenACDC|PaymentTokenPayPal * @return void|PaymentTokenPayPal
*/ */
public function create( string $type ) { public function create( string $type ) {
switch ( $type ) { switch ( $type ) {
case 'paypal': case 'paypal':
return new PaymentTokenPayPal(); return new PaymentTokenPayPal();
case 'acdc':
return new PaymentTokenACDC();
} }
} }
} }

View file

@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Vaulting;
use Exception; use Exception;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WC_Payment_Token_CC;
use WC_Payment_Tokens; use WC_Payment_Tokens;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
@ -64,23 +65,26 @@ class PaymentTokensMigration {
* @param int $id WooCommerce customer id. * @param int $id WooCommerce customer id.
*/ */
public function migrate_payment_tokens_for_user( int $id ):void { public function migrate_payment_tokens_for_user( int $id ):void {
$tokens = $this->payment_token_repository->all_for_user_id( $id ); $tokens = $this->payment_token_repository->all_for_user_id( $id );
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $id ); $total_tokens = count( $tokens );
$this->logger->info( 'Migrating ' . (string) $total_tokens . ' tokens for user ' . (string) $id );
foreach ( $tokens as $token ) { foreach ( $tokens as $token ) {
if ( $this->token_exist( $wc_tokens, $token ) ) {
continue;
}
if ( isset( $token->source()->card ) ) { if ( isset( $token->source()->card ) ) {
$payment_token_acdc = $this->payment_token_factory->create( 'acdc' ); $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $id, CreditCardGateway::ID );
assert( $payment_token_acdc instanceof PaymentTokenACDC ); if ( $this->token_exist( $wc_tokens, $token ) ) {
$this->logger->info( 'Token already exist for user ' . (string) $id );
continue;
}
$payment_token_acdc = new WC_Payment_Token_CC();
$payment_token_acdc->set_token( $token->id() ); $payment_token_acdc->set_token( $token->id() );
$payment_token_acdc->set_user_id( $id ); $payment_token_acdc->set_user_id( $id );
$payment_token_acdc->set_gateway_id( CreditCardGateway::ID ); $payment_token_acdc->set_gateway_id( CreditCardGateway::ID );
$payment_token_acdc->set_last4( $token->source()->card->last_digits ); $payment_token_acdc->set_last4( $token->source()->card->last_digits );
$payment_token_acdc->set_card_type( $token->source()->card->brand ); $payment_token_acdc->set_card_type( $token->source()->card->brand );
$payment_token_acdc->set_expiry_year( '0000' );
$payment_token_acdc->set_expiry_month( '00' );
try { try {
$payment_token_acdc->save(); $payment_token_acdc->save();
@ -92,6 +96,12 @@ class PaymentTokensMigration {
continue; continue;
} }
} elseif ( $token->source()->paypal ) { } elseif ( $token->source()->paypal ) {
$wc_tokens = WC_Payment_Tokens::get_customer_tokens( $id, PayPalGateway::ID );
if ( $this->token_exist( $wc_tokens, $token ) ) {
$this->logger->info( 'Token already exist for user ' . (string) $id );
continue;
}
$payment_token_paypal = $this->payment_token_factory->create( 'paypal' ); $payment_token_paypal = $this->payment_token_factory->create( 'paypal' );
assert( $payment_token_paypal instanceof PaymentTokenPayPal ); assert( $payment_token_paypal instanceof PaymentTokenPayPal );

View file

@ -22,6 +22,7 @@ use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WP_User_Query; use WP_User_Query;
/** /**
@ -47,11 +48,6 @@ class VaultingModule implements ModuleInterface {
* @throws NotFoundException When service could not be found. * @throws NotFoundException When service could not be found.
*/ */
public function run( ContainerInterface $container ): void { public function run( ContainerInterface $container ): void {
$settings = $container->get( 'wcgateway.settings' );
if ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) {
return;
}
$listener = $container->get( 'vaulting.customer-approval-listener' ); $listener = $container->get( 'vaulting.customer-approval-listener' );
assert( $listener instanceof CustomerApprovalListener ); assert( $listener instanceof CustomerApprovalListener );
@ -92,10 +88,6 @@ class VaultingModule implements ModuleInterface {
* @psalm-suppress MissingClosureParamType * @psalm-suppress MissingClosureParamType
*/ */
function ( $type ) { function ( $type ) {
if ( $type === 'WC_Payment_Token_ACDC' ) {
return PaymentTokenACDC::class;
}
if ( $type === 'WC_Payment_Token_PayPal' ) { if ( $type === 'WC_Payment_Token_PayPal' ) {
return PaymentTokenPayPal::class; return PaymentTokenPayPal::class;
} }
@ -116,13 +108,6 @@ class VaultingModule implements ModuleInterface {
return $item; return $item;
} }
if ( strtolower( $payment_token->get_type() ) === 'acdc' ) {
assert( $payment_token instanceof PaymentTokenACDC );
$item['method']['brand'] = $payment_token->get_card_type() . ' ...' . $payment_token->get_last4();
return $item;
}
if ( strtolower( $payment_token->get_type() ) === 'paypal' ) { if ( strtolower( $payment_token->get_type() ) === 'paypal' ) {
assert( $payment_token instanceof PaymentTokenPayPal ); assert( $payment_token instanceof PaymentTokenPayPal );
$item['method']['brand'] = $payment_token->get_email(); $item['method']['brand'] = $payment_token->get_email();
@ -179,23 +164,37 @@ class VaultingModule implements ModuleInterface {
add_action( add_action(
'woocommerce_paypal_payments_gateway_migrate_on_update', 'woocommerce_paypal_payments_gateway_migrate_on_update',
function () use ( $container ) { function () use ( $container ) {
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key $settings = $container->get( 'wcgateway.settings' );
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query assert( $settings instanceof Settings );
$customers = new WP_User_Query( if ( $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ) && $settings->has( 'vault_enabled_dcc' ) ) {
array( $settings->set( 'vault_enabled_dcc', true );
'fields' => 'ID', $settings->persist();
'limit' => -1,
'meta_key' => 'ppcp-vault-token',
)
);
// phpcs:enable
$migrate = $container->get( 'vaulting.payment-tokens-migration' );
assert( $migrate instanceof PaymentTokensMigration );
foreach ( $customers->get_results() as $id ) {
$migrate->migrate_payment_tokens_for_user( (int) $id );
} }
$logger = $container->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$this->migrate_payment_tokens( $logger );
}
);
add_action(
'pcp_migrate_payment_tokens',
function() use ( $container ) {
$logger = $container->get( 'woocommerce.logger.woocommerce' );
assert( $logger instanceof LoggerInterface );
$this->migrate_payment_tokens( $logger );
}
);
add_action(
'woocommerce_paypal_payments_payment_tokens_migration',
function( int $customer_id ) use ( $container ) {
$migration = $container->get( 'vaulting.payment-tokens-migration' );
assert( $migration instanceof PaymentTokensMigration );
$migration->migrate_payment_tokens_for_user( $customer_id );
} }
); );
@ -212,6 +211,51 @@ class VaultingModule implements ModuleInterface {
); );
} }
/**
* Runs the payment tokens migration for users with saved payments.
*
* @param LoggerInterface $logger The logger.
* @return void
*/
public function migrate_payment_tokens( LoggerInterface $logger ): void {
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key
// phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query
$customers = new WP_User_Query(
array(
'fields' => 'ID',
'limit' => -1,
'meta_key' => 'ppcp-vault-token',
)
);
// phpcs:enable
$customers = $customers->get_results();
if ( count( $customers ) === 0 ) {
$logger->info( 'No customers for payment tokens migration.' );
return;
}
$logger->info( 'Starting payment tokens migration for ' . (string) count( $customers ) . ' users' );
$interval_in_seconds = 5;
$timestamp = time();
foreach ( $customers as $id ) {
/**
* Function already exist in WooCommerce
*
* @psalm-suppress UndefinedFunction
*/
as_schedule_single_action(
$timestamp,
'woocommerce_paypal_payments_payment_tokens_migration',
array( 'customer_id' => $id )
);
$timestamp += $interval_in_seconds;
}
}
/** /**
* Filters the emails when vaulting is failed for subscription orders. * Filters the emails when vaulting is failed for subscription orders.
* *

View file

@ -11,6 +11,7 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway; namespace WooCommerce\PayPalCommerce\WcGateway;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Session\SessionHandler;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
@ -32,13 +33,13 @@ use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderAuthorizeAction;
use WooCommerce\PayPalCommerce\WcGateway\Assets\FraudNetAssets; use WooCommerce\PayPalCommerce\WcGateway\Assets\FraudNetAssets;
use WooCommerce\PayPalCommerce\WcGateway\Checkout\CheckoutPayPalAddressPreset; use WooCommerce\PayPalCommerce\WcGateway\Checkout\CheckoutPayPalAddressPreset;
use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways; use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways;
use WooCommerce\PayPalCommerce\WcGateway\Cli\SettingsCommand;
use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewayRepository; use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewayRepository;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO; use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOEndpoint;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway;
use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet; use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet;
@ -95,7 +96,8 @@ return array(
$environment, $environment,
$payment_token_repository, $payment_token_repository,
$logger, $logger,
$api_shop_country $api_shop_country,
$container->get( 'api.endpoint.order' )
); );
}, },
'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway {
@ -386,8 +388,6 @@ return array(
$state = $container->get( 'onboarding.state' ); $state = $container->get( 'onboarding.state' );
assert( $state instanceof State ); assert( $state instanceof State );
$messages_disclaimers = $container->get( 'button.helper.messages-disclaimers' );
$dcc_applies = $container->get( 'api.helpers.dccapplies' ); $dcc_applies = $container->get( 'api.helpers.dccapplies' );
assert( $dcc_applies instanceof DccApplies ); assert( $dcc_applies instanceof DccApplies );
@ -610,40 +610,6 @@ return array(
'requirements' => array(), 'requirements' => array(),
'gateway' => 'paypal', 'gateway' => 'paypal',
), ),
'vault_enabled' => array(
'title' => __( 'Vaulting', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'desc_tip' => true,
'label' => $container->get( 'button.helper.vaulting-label' ),
'description' => __( 'Allow registered buyers to save PayPal and Credit Card accounts. Allow Subscription renewals.', 'woocommerce-paypal-payments' ),
'default' => false,
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => array( 'paypal', 'dcc' ),
'input_class' => $container->get( 'wcgateway.helper.vaulting-scope' ) ? array() : array( 'ppcp-disabled-checkbox' ),
),
'subscription_behavior_when_vault_fails' => array(
'title' => __( 'Subscription capture behavior if Vault fails', 'woocommerce-paypal-payments' ),
'type' => 'select',
'classes' => $subscription_helper->plugin_is_active() ? array() : array( 'hide' ),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'void_auth',
'desc_tip' => true,
'description' => __( 'By default, subscription payments are captured only when saving the payment method was successful. Without a saved payment method, automatic renewal payments are not possible.', 'woocommerce-paypal-payments' ),
'description_with_tip' => __( 'Determines whether authorized payments for subscription orders are captured or voided if there is no saved payment method. This only applies when the intent Capture is used for the subscription order.', 'woocommerce-paypal-payments' ),
'options' => array(
'void_auth' => __( 'Void authorization & fail the order/subscription', 'woocommerce-paypal-payments' ),
'capture_auth' => __( 'Capture authorized payment & set subscription to Manual Renewal', 'woocommerce-paypal-payments' ),
'capture_auth_ignore' => __( 'Capture authorized payment & disregard missing payment method', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => array( 'paypal', 'dcc' ),
),
'card_billing_data_mode' => array( 'card_billing_data_mode' => array(
'title' => __( 'Card billing data handling', 'woocommerce-paypal-payments' ), 'title' => __( 'Card billing data handling', 'woocommerce-paypal-payments' ),
'type' => 'select', 'type' => 'select',
@ -737,6 +703,25 @@ return array(
), ),
'gateway' => 'dcc', 'gateway' => 'dcc',
), ),
'vault_enabled_dcc' => array(
'title' => __( 'Vaulting', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'desc_tip' => true,
'label' => sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Securely store your customers credit cards for a seamless checkout experience and subscription features. Payment methods are saved in the secure %1$sPayPal Vault%2$s.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#vaulting-saving-a-payment-method" target="_blank">',
'</a>'
),
'description' => __( 'Allow registered buyers to save Credit Card payments.', 'woocommerce-paypal-payments' ),
'default' => false,
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'dcc',
'input_class' => $container->get( 'wcgateway.helper.vaulting-scope' ) ? array() : array( 'ppcp-disabled-checkbox' ),
),
'3d_secure_heading' => array( '3d_secure_heading' => array(
'heading' => __( '3D Secure', 'woocommerce-paypal-payments' ), 'heading' => __( '3D Secure', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading', 'type' => 'ppcp-heading',
@ -793,9 +778,85 @@ return array(
), ),
'gateway' => 'dcc', 'gateway' => 'dcc',
), ),
'paypal_saved_payments' => array(
'heading' => __( 'Saved payments', 'woocommerce-paypal-payments' ),
'description' => __( 'PayPal can save your customers payment methods.', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading',
'screens' => array(
State::STATE_START,
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'subscriptions_mode' => array(
'title' => __( 'Subscriptions Mode', 'woocommerce-paypal-payments' ),
'type' => 'select',
'class' => array(),
'input_class' => array( 'wc-enhanced-select' ),
'desc_tip' => true,
'description' => __( 'Utilize PayPal Vaulting for flexible subscription processing with saved payment methods, create “PayPal Subscriptions” to bill customers at regular intervals, or disable PayPal for subscription-type products.', 'woocommerce-paypal-payments' ),
'default' => 'vaulting_api',
'options' => array(
'vaulting_api' => __( 'PayPal Vaulting', 'woocommerce-paypal-payments' ),
'subscriptions_api' => __( 'PayPal Subscriptions', 'woocommerce-paypal-payments' ),
'disable_paypal_subscriptions' => __( 'Disable PayPal for subscriptions', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
),
'vault_enabled' => array(
'title' => __( 'Vaulting', 'woocommerce-paypal-payments' ),
'type' => 'checkbox',
'desc_tip' => true,
'label' => sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Securely store your customers PayPal accounts for a seamless checkout experience. Payment methods are saved in the secure %1$sPayPal Vault%2$s.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#vaulting-saving-a-payment-method" target="_blank">',
'</a>'
) . $container->get( 'button.helper.vaulting-label' ),
'description' => __( 'Allow registered buyers to save PayPal payments.', 'woocommerce-paypal-payments' ),
'default' => false,
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => 'paypal',
'input_class' => $container->get( 'wcgateway.helper.vaulting-scope' ) ? array() : array( 'ppcp-disabled-checkbox' ),
),
'subscription_behavior_when_vault_fails' => array(
'title' => __( 'Subscription capture behavior if Vault fails', 'woocommerce-paypal-payments' ),
'type' => 'select',
'classes' => $subscription_helper->plugin_is_active() ? array() : array( 'hide' ),
'input_class' => array( 'wc-enhanced-select' ),
'default' => 'void_auth',
'desc_tip' => true,
'description' => __( 'By default, subscription payments are captured only when saving the payment method was successful. Without a saved payment method, automatic renewal payments are not possible.', 'woocommerce-paypal-payments' ),
'description_with_tip' => __( 'Determines whether authorized payments for subscription orders are captured or voided if there is no saved payment method. This only applies when the intent Capture is used for the subscription order.', 'woocommerce-paypal-payments' ),
'options' => array(
'void_auth' => __( 'Void authorization & fail the order/subscription', 'woocommerce-paypal-payments' ),
'capture_auth' => __( 'Capture authorized payment & set subscription to Manual Renewal', 'woocommerce-paypal-payments' ),
'capture_auth_ignore' => __( 'Capture authorized payment & disregard missing payment method', 'woocommerce-paypal-payments' ),
),
'screens' => array(
State::STATE_ONBOARDED,
),
'requirements' => array(),
'gateway' => array( 'paypal' ),
),
); );
if ( ! defined( 'PPCP_FLAG_SUBSCRIPTION' ) || ! PPCP_FLAG_SUBSCRIPTION ) {
if ( defined( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) && ! PPCP_FLAG_SUBSCRIPTIONS_API || ! $subscription_helper->plugin_is_active() ) {
unset( $fields['subscriptions_mode'] );
}
$billing_agreements_endpoint = $container->get( 'api.endpoint.billing-agreements' );
if ( ! $billing_agreements_endpoint->reference_transaction_enabled() ) {
unset( $fields['vault_enabled'] ); unset( $fields['vault_enabled'] );
unset( $fields['vault_enabled_dcc'] );
} }
/** /**
@ -1014,15 +1075,6 @@ return array(
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
'wcgateway.endpoint.oxxo' => static function ( ContainerInterface $container ): OXXOEndpoint {
return new OXXOEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'api.endpoint.order' ),
$container->get( 'api.factory.purchase-unit' ),
$container->get( 'api.factory.shipping-preference' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
'wcgateway.logging.is-enabled' => function ( ContainerInterface $container ) : bool { 'wcgateway.logging.is-enabled' => function ( ContainerInterface $container ) : bool {
$settings = $container->get( 'wcgateway.settings' ); $settings = $container->get( 'wcgateway.settings' );
@ -1045,13 +1097,7 @@ return array(
}, },
'button.helper.vaulting-label' => static function ( ContainerInterface $container ): string { 'button.helper.vaulting-label' => static function ( ContainerInterface $container ): string {
$vaulting_label = sprintf( $vaulting_label = '';
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
__( 'Enable saved cards, PayPal accounts, and subscription features on your store. Payment methods are saved in the secure %1$sPayPal Vault%2$s.', 'woocommerce-paypal-payments' ),
'<a href="https://woocommerce.com/document/woocommerce-paypal-payments/#vaulting-saving-a-payment-method" target="_blank">',
'</a>'
);
if ( ! $container->get( 'wcgateway.helper.vaulting-scope' ) ) { if ( ! $container->get( 'wcgateway.helper.vaulting-scope' ) ) {
$vaulting_label .= sprintf( $vaulting_label .= sprintf(
// translators: %1$s and %2$s are the opening and closing of HTML <a> tag. // translators: %1$s and %2$s are the opening and closing of HTML <a> tag.
@ -1219,6 +1265,11 @@ return array(
return 'https://www.paypal.com/bizsignup/entry?country.x=DE&product=payment_methods&capabilities=PAY_UPON_INVOICE'; return 'https://www.paypal.com/bizsignup/entry?country.x=DE&product=payment_methods&capabilities=PAY_UPON_INVOICE';
}, },
'wcgateway.settings.connection.dcc-status-text' => static function ( ContainerInterface $container ): string { 'wcgateway.settings.connection.dcc-status-text' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
if ( $state->current_state() < State::STATE_ONBOARDED ) {
return '';
}
$dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' ); $dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' );
assert( $dcc_product_status instanceof DCCProductStatus ); assert( $dcc_product_status instanceof DCCProductStatus );
@ -1252,6 +1303,11 @@ return array(
); );
}, },
'wcgateway.settings.connection.pui-status-text' => static function ( ContainerInterface $container ): string { 'wcgateway.settings.connection.pui-status-text' => static function ( ContainerInterface $container ): string {
$state = $container->get( 'onboarding.state' );
if ( $state->current_state() < State::STATE_ONBOARDED ) {
return '';
}
$pui_product_status = $container->get( 'wcgateway.pay-upon-invoice-product-status' ); $pui_product_status = $container->get( 'wcgateway.pay-upon-invoice-product-status' );
assert( $pui_product_status instanceof PayUponInvoiceProductStatus ); assert( $pui_product_status instanceof PayUponInvoiceProductStatus );
@ -1348,4 +1404,9 @@ return array(
$container->get( 'wcgateway.is-fraudnet-enabled' ) $container->get( 'wcgateway.is-fraudnet-enabled' )
); );
}, },
'wcgateway.cli.settings.command' => function( ContainerInterface $container ) : SettingsCommand {
return new SettingsCommand(
$container->get( 'wcgateway.settings' )
);
},
); );

View file

@ -137,7 +137,7 @@ class CheckoutPayPalAddressPreset {
} }
$shipping = null; $shipping = null;
foreach ( $this->session_handler->order()->purchase_units() as $unit ) { foreach ( $order->purchase_units() as $unit ) {
$shipping = $unit->shipping(); $shipping = $unit->shipping();
if ( $shipping ) { if ( $shipping ) {
break; break;

View file

@ -0,0 +1,70 @@
<?php
/**
* WP-CLI commands for managing plugin settings.
*
* @package WooCommerce\PayPalCommerce\WcGateway\Cli
*/
declare( strict_types=1 );
namespace WooCommerce\PayPalCommerce\WcGateway\Cli;
use WP_CLI;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class SettingsCommand.
*/
class SettingsCommand {
/**
* The settings.
*
* @var Settings
*/
private $settings;
/**
* SettingsCommand constructor.
*
* @param Settings $settings The settings.
*/
public function __construct( Settings $settings ) {
$this->settings = $settings;
}
/**
* Updates the specified settings.
*
* ## OPTIONS
*
* <id>
* : The setting key.
*
* <value>
* : The setting value.
*
* ## EXAMPLES
*
* wp pcp settings update description "Pay via PayPal."
* wp pcp settings update vault_enabled true
* wp pcp settings update vault_enabled false
*
* @param array $args Positional args.
* @param array $assoc_args Option args.
*/
public function update( array $args, array $assoc_args ): void {
$key = (string) $args[0];
$value = $args[1];
if ( 'true' === strtolower( $value ) ) {
$value = true;
} elseif ( 'false' === strtolower( $value ) ) {
$value = false;
}
$this->settings->set( $key, $value );
$this->settings->persist();
WP_CLI::success( "Updated '{$key}' to '{$value}'." );
}
}

View file

@ -174,12 +174,7 @@ class CardButtonGateway extends \WC_Payment_Gateway {
if ( $this->onboarded ) { if ( $this->onboarded ) {
$this->supports = array( 'refunds' ); $this->supports = array( 'refunds' );
} }
if ( if ( $this->gateways_enabled() ) {
defined( 'PPCP_FLAG_SUBSCRIPTION' )
&& PPCP_FLAG_SUBSCRIPTION
&& $this->gateways_enabled()
&& $this->vault_setting_enabled()
) {
$this->supports = array( $this->supports = array(
'refunds', 'refunds',
'products', 'products',

View file

@ -174,26 +174,30 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC {
if ( $state->current_state() === State::STATE_ONBOARDED ) { if ( $state->current_state() === State::STATE_ONBOARDED ) {
$this->supports = array( 'refunds' ); $this->supports = array( 'refunds' );
} }
if ( if ( $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' ) ) {
defined( 'PPCP_FLAG_SUBSCRIPTION' )
&& PPCP_FLAG_SUBSCRIPTION
&& $this->gateways_enabled()
&& $this->vault_setting_enabled()
) {
$this->supports = array( $this->supports = array(
'refunds', 'refunds',
'products', 'products',
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'multiple_subscriptions',
); );
if (
( $this->config->has( 'vault_enabled_dcc' ) && $this->config->get( 'vault_enabled_dcc' ) )
|| ( $this->config->has( 'subscriptions_mode' ) && $this->config->get( 'subscriptions_mode' ) === 'subscriptions_api' )
) {
array_push(
$this->supports,
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'multiple_subscriptions'
);
}
} }
$this->method_title = __( $this->method_title = __(

View file

@ -228,24 +228,5 @@ class OXXO {
true true
); );
} }
wp_localize_script(
'ppcp-oxxo',
'OXXOConfig',
array(
'oxxo_endpoint' => \WC_AJAX::get_endpoint( 'ppc-oxxo' ),
'oxxo_nonce' => wp_create_nonce( 'ppc-oxxo' ),
'error' => array(
'generic' => __(
'Something went wrong. Please try again or choose another payment source.',
'woocommerce-paypal-payments'
),
'js_validation' => __(
'Required form fields are not filled or invalid.',
'woocommerce-paypal-payments'
),
),
)
);
} }
} }

View file

@ -1,156 +0,0 @@
<?php
/**
* Handles OXXO payer action.
*
* @package WooCommerce\PayPalCommerce\Onboarding\Endpoint
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO;
use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
/**
* OXXOEndpoint constructor.
*/
class OXXOEndpoint implements EndpointInterface {
/**
* The request data
*
* @var RequestData
*/
protected $request_data;
/**
* The purchase unit factory.
*
* @var PurchaseUnitFactory
*/
protected $purchase_unit_factory;
/**
* The shipping preference factory.
*
* @var ShippingPreferenceFactory
*/
protected $shipping_preference_factory;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
protected $order_endpoint;
/**
* The logger.
*
* @var LoggerInterface
*/
protected $logger;
/**
* OXXOEndpoint constructor
*
* @param RequestData $request_data The request data.
* @param OrderEndpoint $order_endpoint The order endpoint.
* @param PurchaseUnitFactory $purchase_unit_factory The purchase unit factory.
* @param ShippingPreferenceFactory $shipping_preference_factory The shipping preference factory.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
RequestData $request_data,
OrderEndpoint $order_endpoint,
PurchaseUnitFactory $purchase_unit_factory,
ShippingPreferenceFactory $shipping_preference_factory,
LoggerInterface $logger
) {
$this->request_data = $request_data;
$this->purchase_unit_factory = $purchase_unit_factory;
$this->shipping_preference_factory = $shipping_preference_factory;
$this->order_endpoint = $order_endpoint;
$this->logger = $logger;
}
/**
* The nonce
*
* @return string
*/
public static function nonce(): string {
return 'ppc-oxxo';
}
/**
* Handles the request.
*
* @return bool
*/
public function handle_request(): bool {
$purchase_unit = $this->purchase_unit_factory->from_wc_cart();
$payer_action = '';
try {
$shipping_preference = $this->shipping_preference_factory->from_state(
$purchase_unit,
'checkout'
);
$order = $this->order_endpoint->create( array( $purchase_unit ), $shipping_preference );
$payment_source = array(
'oxxo' => array(
'name' => 'John Doe',
'email' => 'foo@bar.com',
'country_code' => 'MX',
),
);
$payment_method = $this->order_endpoint->confirm_payment_source( $order->id(), $payment_source );
foreach ( $payment_method->links as $link ) {
if ( $link->rel === 'payer-action' ) {
$payer_action = $link->href;
}
}
} catch ( RuntimeException $exception ) {
$error = $exception->getMessage();
if ( is_a( $exception, PayPalApiException::class ) && is_array( $exception->details() ) ) {
$details = '';
foreach ( $exception->details() as $detail ) {
$issue = $detail->issue ?? '';
$field = $detail->field ?? '';
$description = $detail->description ?? '';
$details .= $issue . ' ' . $field . ' ' . $description . '<br>';
}
$error = $details;
}
$this->logger->error( $error );
wc_add_notice( $error, 'error' );
wp_send_json_error( 'Could not get OXXO payer action.' );
return false;
}
WC()->session->set( 'ppcp_payer_action', $payer_action );
wp_send_json_success(
array( 'payer_action' => $payer_action )
);
return true;
}
}

View file

@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway;
use Exception; use Exception;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use WC_Order; use WC_Order;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken;
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
@ -24,8 +25,11 @@ use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository;
use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException;
use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor;
use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
@ -35,7 +39,7 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
*/ */
class PayPalGateway extends \WC_Payment_Gateway { class PayPalGateway extends \WC_Payment_Gateway {
use ProcessPaymentTrait, FreeTrialHandlerTrait, GatewaySettingsRendererTrait; use ProcessPaymentTrait, FreeTrialHandlerTrait, GatewaySettingsRendererTrait, OrderMetaTrait, TransactionIdHandlingTrait, PaymentsStatusHandlingTrait;
const ID = 'ppcp-gateway'; const ID = 'ppcp-gateway';
const INTENT_META_KEY = '_ppcp_paypal_intent'; const INTENT_META_KEY = '_ppcp_paypal_intent';
@ -150,6 +154,13 @@ class PayPalGateway extends \WC_Payment_Gateway {
*/ */
protected $api_shop_country; protected $api_shop_country;
/**
* The order endpoint.
*
* @var OrderEndpoint
*/
private $order_endpoint;
/** /**
* PayPalGateway constructor. * PayPalGateway constructor.
* *
@ -165,8 +176,9 @@ class PayPalGateway extends \WC_Payment_Gateway {
* @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page.
* @param Environment $environment The environment. * @param Environment $environment The environment.
* @param PaymentTokenRepository $payment_token_repository The payment token repository. * @param PaymentTokenRepository $payment_token_repository The payment token repository.
* @param LoggerInterface $logger The logger. * @param LoggerInterface $logger The logger.
* @param string $api_shop_country The api shop country. * @param string $api_shop_country The api shop country.
* @param OrderEndpoint $order_endpoint The order endpoint.
*/ */
public function __construct( public function __construct(
SettingsRenderer $settings_renderer, SettingsRenderer $settings_renderer,
@ -182,7 +194,8 @@ class PayPalGateway extends \WC_Payment_Gateway {
Environment $environment, Environment $environment,
PaymentTokenRepository $payment_token_repository, PaymentTokenRepository $payment_token_repository,
LoggerInterface $logger, LoggerInterface $logger,
string $api_shop_country string $api_shop_country,
OrderEndpoint $order_endpoint
) { ) {
$this->id = self::ID; $this->id = self::ID;
$this->settings_renderer = $settings_renderer; $this->settings_renderer = $settings_renderer;
@ -204,27 +217,31 @@ class PayPalGateway extends \WC_Payment_Gateway {
if ( $this->onboarded ) { if ( $this->onboarded ) {
$this->supports = array( 'refunds', 'tokenization' ); $this->supports = array( 'refunds', 'tokenization' );
} }
if ( if ( $this->config->has( 'enabled' ) && $this->config->get( 'enabled' ) ) {
defined( 'PPCP_FLAG_SUBSCRIPTION' )
&& PPCP_FLAG_SUBSCRIPTION
&& $this->gateways_enabled()
&& $this->vault_setting_enabled()
) {
$this->supports = array( $this->supports = array(
'refunds', 'refunds',
'products', 'products',
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'multiple_subscriptions',
'tokenization',
); );
if (
( $this->config->has( 'vault_enabled' ) && $this->config->get( 'vault_enabled' ) )
|| ( $this->config->has( 'subscriptions_mode' ) && $this->config->get( 'subscriptions_mode' ) === 'subscriptions_api' )
) {
array_push(
$this->supports,
'tokenization',
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'subscription_payment_method_change',
'subscription_payment_method_change_customer',
'subscription_payment_method_change_admin',
'multiple_subscriptions'
);
}
} }
$this->method_title = $this->define_method_title(); $this->method_title = $this->define_method_title();
@ -250,6 +267,8 @@ class PayPalGateway extends \WC_Payment_Gateway {
'process_admin_options', 'process_admin_options',
) )
); );
$this->order_endpoint = $order_endpoint;
} }
/** /**
@ -425,7 +444,13 @@ class PayPalGateway extends \WC_Payment_Gateway {
} }
// phpcs:ignore WordPress.Security.NonceVerification.Missing // phpcs:ignore WordPress.Security.NonceVerification.Missing
$funding_source = wc_clean( wp_unslash( $_POST['ppcp-funding-source'] ?? '' ) ); $funding_source = wc_clean( wp_unslash( $_POST['ppcp-funding-source'] ?? ( $_POST['funding_source'] ?? '' ) ) );
if ( $funding_source ) {
$wc_order->set_payment_method_title( $this->funding_source_renderer->render_name( $funding_source ) );
$wc_order->save();
}
if ( 'card' !== $funding_source && $this->is_free_trial_order( $wc_order ) ) { if ( 'card' !== $funding_source && $this->is_free_trial_order( $wc_order ) ) {
$user_id = (int) $wc_order->get_customer_id(); $user_id = (int) $wc_order->get_customer_id();
$tokens = $this->payment_token_repository->all_for_user_id( $user_id ); $tokens = $this->payment_token_repository->all_for_user_id( $user_id );
@ -467,6 +492,28 @@ class PayPalGateway extends \WC_Payment_Gateway {
//phpcs:enable WordPress.Security.NonceVerification.Recommended //phpcs:enable WordPress.Security.NonceVerification.Recommended
try { try {
$paypal_subscription_id = WC()->session->get( 'ppcp_subscription_id' ) ?? '';
if ( $paypal_subscription_id ) {
$order = $this->session_handler->order();
$this->add_paypal_meta( $wc_order, $order, $this->environment );
$subscriptions = wcs_get_subscriptions_for_order( $order_id );
foreach ( $subscriptions as $subscription ) {
$subscription->update_meta_data( 'ppcp_subscription', $paypal_subscription_id );
$subscription->save();
$subscription->add_order_note( "PayPal subscription {$paypal_subscription_id} added." );
}
$transaction_id = $this->get_paypal_order_transaction_id( $order );
if ( $transaction_id ) {
$this->update_transaction_id( $transaction_id, $wc_order );
}
$wc_order->payment_complete();
return $this->handle_payment_success( $wc_order );
}
if ( ! $this->order_processor->process( $wc_order ) ) { if ( ! $this->order_processor->process( $wc_order ) ) {
return $this->handle_payment_failure( return $this->handle_payment_failure(
$wc_order, $wc_order,

View file

@ -440,6 +440,7 @@ class PayUponInvoice {
} }
if ( if (
// phpcs:ignore WordPress.Security.NonceVerification
isset( $_GET['pay_for_order'] ) && $_GET['pay_for_order'] === 'true' isset( $_GET['pay_for_order'] ) && $_GET['pay_for_order'] === 'true'
&& ! $this->pui_helper->is_pay_for_order_ready_for_pui() && ! $this->pui_helper->is_pay_for_order_ready_for_pui()
) { ) {

View file

@ -33,19 +33,6 @@ trait ProcessPaymentTrait {
return false; return false;
} }
/**
* Checks if vault setting is enabled.
*
* @return bool Whether vault settings are enabled or not.
* @throws \WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException When a setting hasn't been found.
*/
protected function vault_setting_enabled(): bool {
if ( $this->config->has( 'vault_enabled' ) && $this->config->get( 'vault_enabled' ) ) {
return true;
}
return false;
}
/** /**
* Scheduled the vaulted payment check. * Scheduled the vaulted payment check.
* *
@ -94,8 +81,9 @@ trait ProcessPaymentTrait {
wc_add_notice( $error->getMessage(), 'error' ); wc_add_notice( $error->getMessage(), 'error' );
return array( return array(
'result' => 'failure', 'result' => 'failure',
'redirect' => wc_get_checkout_url(), 'redirect' => wc_get_checkout_url(),
'errorMessage' => $error->getMessage(),
); );
} }

View file

@ -98,6 +98,9 @@ class SettingsStatus {
if ( 'pay-now' === $location ) { if ( 'pay-now' === $location ) {
$location = 'checkout'; $location = 'checkout';
} }
if ( 'checkout-block' === $location ) {
$location = 'checkout-block-express';
}
return $location; return $location;
} }

View file

@ -162,8 +162,13 @@ class OrderProcessor {
* *
* @return bool * @return bool
*/ */
public function process( WC_Order $wc_order ): bool { public function process( \WC_Order $wc_order ): bool {
$order = $this->session_handler->order(); // phpcs:ignore WordPress.Security.NonceVerification
$order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY ) ?: wc_clean( wp_unslash( $_POST['paypal_order_id'] ?? '' ) );
$order = $this->session_handler->order();
if ( ! $order && is_string( $order_id ) ) {
$order = $this->order_endpoint->order( $order_id );
}
if ( ! $order ) { if ( ! $order ) {
$order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY ); $order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY );
if ( ! $order_id ) { if ( ! $order_id ) {

View file

@ -408,7 +408,6 @@ return function ( ContainerInterface $container, array $fields ): array {
'gateway' => Settings::CONNECTION_TAB_ID, 'gateway' => Settings::CONNECTION_TAB_ID,
'input_class' => $container->get( 'wcgateway.settings.should-disable-fraudnet-checkbox' ) ? array( 'ppcp-disabled-checkbox' ) : array(), 'input_class' => $container->get( 'wcgateway.settings.should-disable-fraudnet-checkbox' ) ? array( 'ppcp-disabled-checkbox' ) : array(),
), ),
'credentials_integration_configuration_heading' => array( 'credentials_integration_configuration_heading' => array(
'heading' => __( 'General integration configuration', 'woocommerce-paypal-payments' ), 'heading' => __( 'General integration configuration', 'woocommerce-paypal-payments' ),
'type' => 'ppcp-heading', 'type' => 'ppcp-heading',

View file

@ -225,11 +225,13 @@ class SettingsListener {
$token = $this->bearer->bearer(); $token = $this->bearer->bearer();
if ( ! $token->vaulting_available() ) { if ( ! $token->vaulting_available() ) {
$this->settings->set( 'vault_enabled', false ); $this->settings->set( 'vault_enabled', false );
$this->settings->set( 'vault_enabled_dcc', false );
$this->settings->persist(); $this->settings->persist();
return; return;
} }
} catch ( RuntimeException $exception ) { } catch ( RuntimeException $exception ) {
$this->settings->set( 'vault_enabled', false ); $this->settings->set( 'vault_enabled', false );
$this->settings->set( 'vault_enabled_dcc', false );
$this->settings->persist(); $this->settings->persist();
throw $exception; throw $exception;
@ -336,6 +338,11 @@ class SettingsListener {
$this->dcc_status_cache->delete( DCCProductStatus::DCC_STATUS_CACHE_KEY ); $this->dcc_status_cache->delete( DCCProductStatus::DCC_STATUS_CACHE_KEY );
} }
$ppcp_reference_transaction_enabled = get_transient( 'ppcp_reference_transaction_enabled' ) ?? '';
if ( $ppcp_reference_transaction_enabled ) {
delete_transient( 'ppcp_reference_transaction_enabled' );
}
$redirect_url = false; $redirect_url = false;
if ( $credentials_change_status && self::CREDENTIALS_UNCHANGED !== $credentials_change_status ) { if ( $credentials_change_status && self::CREDENTIALS_UNCHANGED !== $credentials_change_status ) {
$redirect_url = $this->get_onboarding_redirect_url(); $redirect_url = $this->get_onboarding_redirect_url();

View file

@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface;
use Throwable; use Throwable;
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WC_Order; use WC_Order;
@ -322,14 +323,6 @@ class WCGatewayModule implements ModuleInterface {
2 2
); );
add_action(
'wc_ajax_ppc-oxxo',
static function () use ( $c ) {
$endpoint = $c->get( 'wcgateway.endpoint.oxxo' );
$endpoint->handle_request();
}
);
add_action( add_action(
'woocommerce_order_status_changed', 'woocommerce_order_status_changed',
static function ( int $order_id, string $from, string $to ) use ( $c ) { static function ( int $order_id, string $from, string $to ) use ( $c ) {
@ -380,6 +373,13 @@ class WCGatewayModule implements ModuleInterface {
10, 10,
3 3
); );
if ( defined( 'WP_CLI' ) && WP_CLI ) {
\WP_CLI::add_command(
'pcp settings',
$c->get( 'wcgateway.cli.settings.command' )
);
}
} }
/** /**

View file

@ -14,19 +14,24 @@ use Psr\Log\LoggerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook; use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook;
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint; use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulateEndpoint; use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulateEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulationStateEndpoint; use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulationStateEndpoint;
use WooCommerce\PayPalCommerce\Webhooks\Handler\BillingPlanPricingChangeActivated;
use WooCommerce\PayPalCommerce\Webhooks\Handler\BillingPlanUpdated;
use WooCommerce\PayPalCommerce\Webhooks\Handler\BillingSubscriptionCancelled;
use WooCommerce\PayPalCommerce\Webhooks\Handler\CatalogProductUpdated;
use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutOrderApproved; use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutOrderApproved;
use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutOrderCompleted; use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutOrderCompleted;
use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutPaymentApprovalReversed; use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutPaymentApprovalReversed;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureCompleted; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureCompleted;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureDenied;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCapturePending; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCapturePending;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureRefunded; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureRefunded;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureReversed; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureReversed;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Webhooks\Handler\VaultCreditCardCreated; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentSaleCompleted;
use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentSaleRefunded;
use WooCommerce\PayPalCommerce\Webhooks\Handler\VaultPaymentTokenCreated; use WooCommerce\PayPalCommerce\Webhooks\Handler\VaultPaymentTokenCreated;
use WooCommerce\PayPalCommerce\Webhooks\Handler\VaultPaymentTokenDeleted; use WooCommerce\PayPalCommerce\Webhooks\Handler\VaultPaymentTokenDeleted;
use WooCommerce\PayPalCommerce\Webhooks\Status\Assets\WebhooksStatusPageAssets; use WooCommerce\PayPalCommerce\Webhooks\Status\Assets\WebhooksStatusPageAssets;
@ -86,6 +91,12 @@ return array(
new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory ), new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory ),
new VaultPaymentTokenDeleted( $logger ), new VaultPaymentTokenDeleted( $logger ),
new PaymentCapturePending( $logger ), new PaymentCapturePending( $logger ),
new PaymentSaleCompleted( $logger ),
new PaymentSaleRefunded( $logger ),
new BillingSubscriptionCancelled( $logger ),
new BillingPlanPricingChangeActivated( $logger ),
new CatalogProductUpdated( $logger ),
new BillingPlanUpdated( $logger ),
); );
}, },
@ -116,7 +127,12 @@ return array(
$endpoint = $container->get( 'api.endpoint.webhook' ); $endpoint = $container->get( 'api.endpoint.webhook' );
assert( $endpoint instanceof WebhookEndpoint ); assert( $endpoint instanceof WebhookEndpoint );
return $endpoint->list(); $state = $container->get( 'onboarding.state' );
if ( $state->current_state() >= State::STATE_ONBOARDED ) {
return $endpoint->list();
}
return array();
}, },
'webhook.status.registered-webhooks-data' => function( ContainerInterface $container ) : array { 'webhook.status.registered-webhooks-data' => function( ContainerInterface $container ) : array {

View file

@ -58,6 +58,11 @@ class ResubscribeEndpoint {
* Handles the incoming request. * Handles the incoming request.
*/ */
public function handle_request() { public function handle_request() {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Not admin.', 403 );
return false;
}
try { try {
// Validate nonce. // Validate nonce.
$this->request_data->read_request( $this->nonce() ); $this->request_data->read_request( $this->nonce() );

View file

@ -61,6 +61,11 @@ class SimulateEndpoint {
* Handles the incoming request. * Handles the incoming request.
*/ */
public function handle_request() { public function handle_request() {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Not admin.', 403 );
return false;
}
try { try {
// Validate nonce. // Validate nonce.
$this->request_data->read_request( $this->nonce() ); $this->request_data->read_request( $this->nonce() );

View file

@ -51,6 +51,11 @@ class SimulationStateEndpoint {
* Handles the incoming request. * Handles the incoming request.
*/ */
public function handle_request() { public function handle_request() {
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_send_json_error( 'Not admin.', 403 );
return false;
}
try { try {
$state = $this->simulation->get_state(); $state = $this->simulation->get_state();

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