Fix merge conflicts

This commit is contained in:
Emili Castells Guasch 2023-05-16 12:44:14 +02:00
commit 3c0e807758
55 changed files with 5170 additions and 202 deletions

View file

@ -27,6 +27,7 @@ web_environment:
- ADMIN_PASS=admin - ADMIN_PASS=admin
- ADMIN_EMAIL=admin@example.com - ADMIN_EMAIL=admin@example.com
- WC_VERSION=6.1.0 - WC_VERSION=6.1.0
- PCP_BLOCKS_ENABLED=1
# Key features of ddev's config.yaml: # Key features of ddev's config.yaml:

View file

@ -2,7 +2,16 @@ 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" 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
WP_MERCHANT_USER="admin" WP_MERCHANT_USER="admin"
WP_MERCHANT_PASSWORD="admin" WP_MERCHANT_PASSWORD="admin"

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 ) {
}

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

@ -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

@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory; use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory;
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory; 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;
@ -320,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(

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

@ -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

@ -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

@ -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

@ -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.74",
"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,252 @@
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} = 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 = json.data;
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]);
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\SmartButton;
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 SmartButton );
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

@ -177,6 +177,7 @@ return array(
$container->get( 'button.context' ), $container->get( 'button.context' ),
$container->get( 'button.can_save_vault_token' ), $container->get( 'button.can_save_vault_token' ),
$container->get( 'button.vault' ), $container->get( 'button.vault' ),
$container->get( 'button.pay-now-contexts' ),
$container->get( 'woocommerce.logger.woocommerce' ) $container->get( 'woocommerce.logger.woocommerce' )
); );
}, },
@ -186,6 +187,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();
}, },
@ -223,6 +227,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
); );
}, },
@ -340,4 +346,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

@ -194,6 +194,13 @@ class SmartButton implements SmartButtonInterface {
*/ */
private $vault; private $vault;
/**
* The contexts that should have the Pay Now button.
*
* @var string[]
*/
private $pay_now_contexts;
/** /**
* The logger. * The logger.
* *
@ -232,6 +239,7 @@ class SmartButton implements SmartButtonInterface {
* @param string $context The current context. * @param string $context The current context.
* @param bool $can_save_vault_token Whether vault tokens could be saved. * @param bool $can_save_vault_token Whether vault tokens could be saved.
* @param string $vault Whether vault could be enabled or not. * @param string $vault Whether vault could be enabled or not.
* @param string[] $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(
@ -252,6 +260,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,
string $intent, string $intent,
string $context, string $context,
bool $can_save_vault_token, bool $can_save_vault_token,
@ -276,6 +285,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->intent = $intent; $this->intent = $intent;
$this->logger = $logger; $this->logger = $logger;
$this->context = $context; $this->context = $context;
@ -541,31 +551,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 ) ) {
@ -575,31 +624,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()
);
} }
/** /**
@ -691,18 +730,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.
*/ */
@ -731,6 +758,24 @@ class SmartButton implements SmartButtonInterface {
); );
} }
/**
* Whether we can store vault tokens or not.
*
* @return bool
*/
public function can_save_vault_token(): bool {
if ( ! $this->settings->has( 'client_id' ) || ! $this->settings->get( 'client_id' ) ) {
return false;
}
if ( ! $this->settings->has( 'vault_enabled' ) || ! $this->settings->get( 'vault_enabled' ) ) {
return false;
}
return true;
}
/** /**
* Whether we need to initialize the script to enable tokenization for subscriptions or not. * Whether we need to initialize the script to enable tokenization for subscriptions or not.
* *
@ -789,7 +834,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 {
global $wp; global $wp;
@ -944,6 +988,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;
} }
@ -966,9 +1019,9 @@ 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 {
$context = $this->context();
$intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture'; $intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture';
$product_intent = $this->subscription_helper->current_product_is_subscription() ? 'authorize' : $intent; $product_intent = $this->subscription_helper->current_product_is_subscription() ? 'authorize' : $intent;
$other_context_intent = $this->subscription_helper->cart_contains_subscription() ? 'authorize' : $intent; $other_context_intent = $this->subscription_helper->cart_contains_subscription() ? 'authorize' : $intent;
@ -978,9 +1031,9 @@ class SmartButton implements SmartButtonInterface {
'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->vault, 'vault' => $this->can_save_vault_token() ? 'true' : 'false',
'commit' => is_checkout() ? 'true' : 'false', 'commit' => in_array( $context, $this->pay_now_contexts, true ) ? 'true' : 'false',
'intent' => $this->intent, 'intent' => $context === 'product' ? $product_intent : $other_context_intent,
); );
if ( if (
$this->environment->current_environment_is( Environment::SANDBOX ) $this->environment->current_environment_is( Environment::SANDBOX )
@ -1011,6 +1064,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 ) {
@ -1021,8 +1081,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 {
@ -1091,7 +1151,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';
} }
@ -1107,34 +1167,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() ) {
@ -1163,9 +1199,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',

View file

@ -22,12 +22,12 @@ 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;

View file

@ -64,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();
} }
); );

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( $order->to_array() );
} }
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( $order->to_array() );
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

@ -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 ( (
@ -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,

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

@ -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

@ -33,6 +33,7 @@ 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;
@ -1413,4 +1414,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

@ -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

@ -444,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 );

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

@ -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

@ -381,6 +381,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

@ -7,6 +7,7 @@
"author": "WooCommerce", "author": "WooCommerce",
"scripts": { "scripts": {
"postinstall": "run-s install:modules:* && run-s build:modules", "postinstall": "run-s install:modules:* && run-s build:modules",
"install:modules:ppcp-blocks": "cd modules/ppcp-blocks && yarn install",
"install:modules:ppcp-button": "cd modules/ppcp-button && yarn install", "install:modules:ppcp-button": "cd modules/ppcp-button && yarn install",
"install:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn install", "install:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn install",
"install:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn install", "install:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn install",
@ -14,6 +15,7 @@
"install:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn install", "install:modules:ppcp-onboarding": "cd modules/ppcp-onboarding && yarn install",
"install:modules:ppcp-compat": "cd modules/ppcp-compat && yarn install", "install:modules:ppcp-compat": "cd modules/ppcp-compat && yarn install",
"install:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn install", "install:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn install",
"build:modules:ppcp-blocks": "cd modules/ppcp-blocks && yarn run build",
"build:modules:ppcp-button": "cd modules/ppcp-button && yarn run build", "build:modules:ppcp-button": "cd modules/ppcp-button && yarn run build",
"build:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn run build", "build:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn run build",
"build:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run build", "build:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run build",
@ -22,6 +24,7 @@
"build:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run build", "build:modules:ppcp-compat": "cd modules/ppcp-compat && yarn run build",
"build:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run build", "build:modules:ppcp-uninstall": "cd modules/ppcp-uninstall && yarn run build",
"build:modules": "run-p build:modules:*", "build:modules": "run-p build:modules:*",
"watch:modules:ppcp-blocks": "cd modules/ppcp-blocks && yarn run watch",
"watch:modules:ppcp-button": "cd modules/ppcp-button && yarn run watch", "watch:modules:ppcp-button": "cd modules/ppcp-button && yarn run watch",
"watch:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn run watch", "watch:modules:ppcp-wc-gateway": "cd modules/ppcp-wc-gateway && yarn run watch",
"watch:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run watch", "watch:modules:ppcp-webhooks": "cd modules/ppcp-webhooks && yarn run watch",
@ -39,7 +42,7 @@
"ddev:watch-js": "ddev yarn watch:modules", "ddev:watch-js": "ddev yarn watch:modules",
"ddev:composer-update": "ddev composer update && ddev composer update --lock", "ddev:composer-update": "ddev composer update && ddev composer update --lock",
"ddev:unit-tests": "ddev exec phpunit", "ddev:unit-tests": "ddev exec phpunit",
"ddev:e2e-tests": "cp -n .env.e2e.example .env.e2e && ddev php tests/e2e/PHPUnit/setup.php && ddev exec phpunit -c tests/e2e/phpunit.xml.dist", "ddev:e2e-tests": "(cp -n .env.e2e.example .env.e2e || true) && ddev php tests/e2e/PHPUnit/setup.php && ddev exec phpunit -c tests/e2e/phpunit.xml.dist",
"ddev:pw-install": "ddev yarn playwright install --with-deps", "ddev:pw-install": "ddev yarn playwright install --with-deps",
"ddev:pw-tests": "ddev yarn playwright test", "ddev:pw-tests": "ddev yarn playwright test",
"ddev:test": "yarn run ddev:unit-tests && yarn run ddev:e2e-tests && yarn run ddev:pw-tests", "ddev:test": "yarn run ddev:unit-tests && yarn run ddev:e2e-tests && yarn run ddev:pw-tests",
@ -58,7 +61,7 @@
"prearchive": "rm -rf $npm_package_name.zip", "prearchive": "rm -rf $npm_package_name.zip",
"archive": "zip -r $npm_package_name.zip . -x **.git/\\* **node_modules/\\*", "archive": "zip -r $npm_package_name.zip . -x **.git/\\* **node_modules/\\*",
"postarchive": "yarn run archive:cleanup && rm -rf $npm_package_name && unzip $npm_package_name.zip -d $npm_package_name && rm $npm_package_name.zip && zip -r $npm_package_name.zip $npm_package_name && rm -rf $npm_package_name", "postarchive": "yarn run archive:cleanup && rm -rf $npm_package_name && unzip $npm_package_name.zip -d $npm_package_name && rm $npm_package_name.zip && zip -r $npm_package_name.zip $npm_package_name && rm -rf $npm_package_name",
"archive:cleanup": "zip -d $npm_package_name.zip .env* .ddev/\\* \\*.idea/\\* .editorconfig tests/\\* .github/\\* .psalm/\\* wordpress_org_assets/\\* \\*.DS_Store \\*README.md \\*.gitattributes \\*.gitignore \\*composer.json \\*composer.lock patchwork.json phpunit.xml.dist .phpunit.result.cache phpcs.xml* psalm*.xml* playwright.config.js \\*.babelrc \\*package.json \\*webpack.config.js \\*yarn.lock \\*.travis.yml\\" "archive:cleanup": "zip -d $npm_package_name.zip .env* .ddev/\\* \\*.idea/\\* .editorconfig tests/\\* .github/\\* .psalm/\\* wordpress_org_assets/\\* \\*.DS_Store \\*README.md \\*.gitattributes \\*.gitignore \\*composer.json \\*composer.lock patchwork.json phpunit.xml.dist .phpunit.result.cache phpcs.xml* psalm*.xml* playwright.config.js wp-cli.yml \\*.babelrc \\*package.json \\*webpack.config.js \\*yarn.lock \\*.travis.yml\\"
}, },
"config": { "config": {
"wp_org_slug": "woocommerce-paypal-payments" "wp_org_slug": "woocommerce-paypal-payments"

View file

@ -53,4 +53,5 @@
<exclude-pattern>*/vendor/*</exclude-pattern> <exclude-pattern>*/vendor/*</exclude-pattern>
<exclude-pattern>./tests/*</exclude-pattern> <exclude-pattern>./tests/*</exclude-pattern>
<exclude-pattern>*/resources/*</exclude-pattern> <exclude-pattern>*/resources/*</exclude-pattern>
<exclude-pattern>*.asset.php</exclude-pattern>
</ruleset> </ruleset>

View file

@ -32,8 +32,10 @@
<stubs> <stubs>
<file name=".psalm/stubs.php"/> <file name=".psalm/stubs.php"/>
<file name=".psalm/wcblocks.php"/>
<file name=".psalm/wcs.php"/> <file name=".psalm/wcs.php"/>
<file name=".psalm/gzd.php"/> <file name=".psalm/gzd.php"/>
<file name=".psalm/wpcli.php"/>
<file name="vendor/php-stubs/wordpress-stubs/wordpress-stubs.php"/> <file name="vendor/php-stubs/wordpress-stubs/wordpress-stubs.php"/>
<file name="vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php"/> <file name="vendor/php-stubs/woocommerce-stubs/woocommerce-stubs.php"/>
</stubs> </stubs>

View file

@ -647,7 +647,7 @@ class OrderEndpointTest extends TestCase
$intent = 'CAPTURE'; $intent = 'CAPTURE';
$logger = Mockery::mock(LoggerInterface::class); $logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log'); $logger->shouldReceive('warning');
$logger->shouldReceive('debug'); $logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$subscription_helper = Mockery::mock(SubscriptionHelper::class); $subscription_helper = Mockery::mock(SubscriptionHelper::class);
@ -742,7 +742,7 @@ class OrderEndpointTest extends TestCase
$intent = 'CAPTURE'; $intent = 'CAPTURE';
$logger = Mockery::mock(LoggerInterface::class); $logger = Mockery::mock(LoggerInterface::class);
$logger->shouldReceive('log'); $logger->shouldReceive('warning');
$logger->shouldReceive('debug'); $logger->shouldReceive('debug');
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
@ -882,7 +882,7 @@ class OrderEndpointTest extends TestCase
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$applicationContextRepository $applicationContextRepository
->expects('current_context') ->expects('current_context')
->with(Matchers::identicalTo(ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING)) ->with(ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING, ApplicationContext::USER_ACTION_CONTINUE)
->andReturn($applicationContext); ->andReturn($applicationContext);
$subscription_helper = Mockery::mock(SubscriptionHelper::class); $subscription_helper = Mockery::mock(SubscriptionHelper::class);
$subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true); $subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true);
@ -985,7 +985,7 @@ class OrderEndpointTest extends TestCase
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$applicationContextRepository $applicationContextRepository
->expects('current_context') ->expects('current_context')
->with(Matchers::identicalTo(ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE)) ->with(ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE, ApplicationContext::USER_ACTION_CONTINUE)
->andReturn($applicationContext); ->andReturn($applicationContext);
$subscription_helper = Mockery::mock(SubscriptionHelper::class); $subscription_helper = Mockery::mock(SubscriptionHelper::class);
$subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true); $subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true);
@ -1065,7 +1065,7 @@ class OrderEndpointTest extends TestCase
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$applicationContextRepository $applicationContextRepository
->expects('current_context') ->expects('current_context')
->with(Matchers::identicalTo(ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING)) ->with(ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING, ApplicationContext::USER_ACTION_CONTINUE)
->andReturn($applicationContext); ->andReturn($applicationContext);
$subscription_helper = Mockery::mock(SubscriptionHelper::class); $subscription_helper = Mockery::mock(SubscriptionHelper::class);
$subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true); $subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true);
@ -1156,7 +1156,7 @@ class OrderEndpointTest extends TestCase
$applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class);
$applicationContextRepository $applicationContextRepository
->expects('current_context') ->expects('current_context')
->with(Matchers::identicalTo(ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE)) ->with(ApplicationContext::SHIPPING_PREFERENCE_GET_FROM_FILE, ApplicationContext::USER_ACTION_CONTINUE)
->andReturn($applicationContext); ->andReturn($applicationContext);
$subscription_helper = Mockery::mock(SubscriptionHelper::class); $subscription_helper = Mockery::mock(SubscriptionHelper::class);
$subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true); $subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true);

View file

@ -294,7 +294,7 @@ class PurchaseUnitFactoryTest extends TestCase
$shippingFactory = Mockery::mock(ShippingFactory::class); $shippingFactory = Mockery::mock(ShippingFactory::class);
$shippingFactory $shippingFactory
->expects('from_wc_customer') ->expects('from_wc_customer')
->with($wcCustomer) ->with($wcCustomer, false)
->andReturn($shipping); ->andReturn($shipping);
$paymentsFacory = Mockery::mock(PaymentsFactory::class); $paymentsFacory = Mockery::mock(PaymentsFactory::class);
$testee = new PurchaseUnitFactory( $testee = new PurchaseUnitFactory(

View file

@ -167,6 +167,8 @@ class CreateOrderEndpointTest extends TestCase
false, false,
CardBillingMode::MINIMAL_INPUT, CardBillingMode::MINIMAL_INPUT,
false, false,
['checkout'],
false,
new NullLogger() new NullLogger()
); );
return array($payer_factory, $testee); return array($payer_factory, $testee);

View file

@ -1,4 +1,5 @@
const {test, expect} = require('@playwright/test'); const {test, expect} = require('@playwright/test');
const {serverExec} = require("./utils/server");
const { const {
CUSTOMER_EMAIL, CUSTOMER_EMAIL,
@ -7,6 +8,13 @@ const {
CREDIT_CARD_EXPIRATION, CREDIT_CARD_EXPIRATION,
CREDIT_CARD_CVV, CREDIT_CARD_CVV,
PRODUCT_URL, PRODUCT_URL,
PRODUCT_ID,
CHECKOUT_URL,
CHECKOUT_PAGE_ID,
CART_URL,
BLOCK_CHECKOUT_URL,
BLOCK_CHECKOUT_PAGE_ID,
BLOCK_CART_URL,
} = process.env; } = process.env;
async function fillCheckoutForm(page) { async function fillCheckoutForm(page) {
@ -30,72 +38,182 @@ async function fillCheckoutForm(page) {
} }
} }
async function openPaypalPopup(page) { async function openPaypalPopup(page, retry = true) {
await page.locator('.component-frame').scrollIntoViewIfNeeded(); try {
await page.locator('.component-frame').scrollIntoViewIfNeeded();
const [popup] = await Promise.all([ const [popup] = await Promise.all([
page.waitForEvent('popup'), page.waitForEvent('popup', {timeout: 5000}),
page.frameLocator('.component-frame').locator('[data-funding-source="paypal"]').click(), page.frameLocator('.component-frame').locator('[data-funding-source="paypal"]').click(),
]); ]);
await popup.waitForLoadState(); await popup.waitForLoadState();
return popup; return popup;
} catch (err) {
if (retry) {
return openPaypalPopup(page, false);
}
throw err;
}
} }
async function loginIntoPaypal(popup) { async function loginIntoPaypal(popup) {
await popup.click("text=Log in"); await Promise.any([
popup.locator('[name="login_email"]'),
popup.click("text=Log in"),
]);
await popup.fill('[name="login_email"]', CUSTOMER_EMAIL); await popup.fill('[name="login_email"]', CUSTOMER_EMAIL);
await popup.locator('#btnNext').click(); await popup.locator('#btnNext').click();
await popup.fill('[name="login_password"]', CUSTOMER_PASSWORD); await popup.fill('[name="login_password"]', CUSTOMER_PASSWORD);
await popup.locator('#btnLogin').click(); await popup.locator('#btnLogin').click();
} }
test('PayPal button place order from Product page', async ({page}) => { async function waitForPaypalShippingList(popup) {
await expect(popup.locator('#shippingMethodsDropdown')).toBeVisible({timeout: 15000});
await page.goto(PRODUCT_URL); }
const popup = await openPaypalPopup(page);
await loginIntoPaypal(popup);
await popup.locator('#payment-submit-btn').click();
await fillCheckoutForm(page);
async function completePaypalPayment(popup) {
await Promise.all([ await Promise.all([
page.waitForNavigation(), popup.waitForEvent('close', {timeout: 20000}),
page.locator('#place_order').click(), popup.click('#payment-submit-btn'),
]); ]);
}
async function expectOrderReceivedPage(page) {
const title = await page.locator('.entry-title'); const title = await page.locator('.entry-title');
await expect(title).toHaveText('Order received'); await expect(title).toHaveText('Order received');
}
async function completeBlockContinuation(page) {
await expect(page.locator('#radio-control-wc-payment-method-options-ppcp-gateway')).toBeChecked();
await expect(page.locator('.component-frame')).toHaveCount(0);
await page.locator('.wc-block-components-checkout-place-order-button').click();
await page.waitForNavigation();
await expectOrderReceivedPage(page);
}
test.describe('Classic checkout', () => {
test.beforeAll(async ({ browser }) => {
await serverExec('wp option update woocommerce_checkout_page_id ' + CHECKOUT_PAGE_ID);
});
test('PayPal button place order from Product page', async ({page}) => {
await page.goto(PRODUCT_URL);
const popup = await openPaypalPopup(page);
await loginIntoPaypal(popup);
await completePaypalPayment(popup);
await fillCheckoutForm(page);
await Promise.all([
page.waitForNavigation(),
page.locator('#place_order').click(),
]);
await expectOrderReceivedPage(page);
});
test('Advanced Credit and Debit Card (ACDC) place order from Checkout page', async ({page}) => {
await page.goto(PRODUCT_URL);
await page.locator('.single_add_to_cart_button').click();
await page.goto(CHECKOUT_URL);
await fillCheckoutForm(page);
await page.click("text=Credit Cards");
const creditCardNumber = page.frameLocator('#braintree-hosted-field-number').locator('#credit-card-number');
await creditCardNumber.fill(CREDIT_CARD_NUMBER);
const expirationDate = page.frameLocator('#braintree-hosted-field-expirationDate').locator('#expiration');
await expirationDate.fill(CREDIT_CARD_EXPIRATION);
const cvv = page.frameLocator('#braintree-hosted-field-cvv').locator('#cvv');
await cvv.fill(CREDIT_CARD_CVV);
await Promise.all([
page.waitForNavigation(),
page.locator('.ppcp-dcc-order-button').click(),
]);
await expectOrderReceivedPage(page);
});
}); });
test('Advanced Credit and Debit Card (ACDC) place order from Checkout page', async ({page}) => { test.describe('Block checkout', () => {
test.beforeAll(async ({browser}) => {
await serverExec('wp option update woocommerce_checkout_page_id ' + BLOCK_CHECKOUT_PAGE_ID);
await serverExec('wp pcp settings update blocks_final_review_enabled true');
});
await page.goto(PRODUCT_URL); test('PayPal express block checkout', async ({page}) => {
await page.locator('.single_add_to_cart_button').click(); await page.goto('?add-to-cart=' + PRODUCT_ID);
await page.goto('/checkout/'); await page.goto(BLOCK_CHECKOUT_URL)
await fillCheckoutForm(page);
await page.click("text=Credit Cards"); const popup = await openPaypalPopup(page);
const creditCardNumber = page.frameLocator('#braintree-hosted-field-number').locator('#credit-card-number'); await loginIntoPaypal(popup);
await creditCardNumber.fill(CREDIT_CARD_NUMBER);
const expirationDate = page.frameLocator('#braintree-hosted-field-expirationDate').locator('#expiration'); await completePaypalPayment(popup);
await expirationDate.fill(CREDIT_CARD_EXPIRATION);
const cvv = page.frameLocator('#braintree-hosted-field-cvv').locator('#cvv'); await completeBlockContinuation(page);
await cvv.fill(CREDIT_CARD_CVV); });
await Promise.all([ test('PayPal express block cart', async ({page}) => {
page.waitForNavigation(), await page.goto(BLOCK_CART_URL + '?add-to-cart=' + PRODUCT_ID)
page.locator('.ppcp-dcc-order-button').click(),
]);
const title = await page.locator('.entry-title'); const popup = await openPaypalPopup(page);
await expect(title).toHaveText('Order received');
await loginIntoPaypal(popup);
await completePaypalPayment(popup);
await completeBlockContinuation(page);
});
test.describe('Without review', () => {
test.beforeAll(async ({browser}) => {
await serverExec('wp pcp settings update blocks_final_review_enabled false');
});
test('PayPal express block checkout', async ({page}) => {
await page.goto('?add-to-cart=' + PRODUCT_ID);
await page.goto(BLOCK_CHECKOUT_URL)
const popup = await openPaypalPopup(page);
await loginIntoPaypal(popup);
await waitForPaypalShippingList(popup);
await completePaypalPayment(popup);
await expectOrderReceivedPage(page);
});
test('PayPal express block cart', async ({page}) => {
await page.goto(BLOCK_CART_URL + '?add-to-cart=' + PRODUCT_ID)
const popup = await openPaypalPopup(page);
await loginIntoPaypal(popup);
await waitForPaypalShippingList(popup);
await completePaypalPayment(popup);
await expectOrderReceivedPage(page);
});
});
}); });

View file

@ -0,0 +1,27 @@
const { exec } = require('node:child_process');
/**
* Executes the command on the server (inside DDEV). Can be called inside and outside DDEV.
*/
export const serverExec = async (cmd) => {
const isDdev = process.env.IS_DDEV_PROJECT === 'true';
if (!isDdev) {
cmd = 'ddev exec ' + cmd;
}
console.log(cmd);
return new Promise((resolve) => exec(cmd, (error, stdout, stderr) => {
if (stderr) {
console.error(stderr);
}
if (stdout) {
console.log(stdout);
}
if (error) {
throw error;
} else {
resolve(stdout);
}
}))
}

1
wp-cli.yml Normal file
View file

@ -0,0 +1 @@
path: .ddev/wordpress/