diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 251638bc4..b56e10cf2 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -18,7 +18,7 @@ hooks: pre-start: - exec-host: "mkdir -p .ddev/wordpress/wp-content/plugins/${DDEV_PROJECT}" web_environment: - - WP_VERSION=5.9.3 + - WP_VERSION=6.2.2 - WP_LOCALE=en_US - WP_TITLE=WooCommerce PayPal Payments - WP_MULTISITE=true @@ -26,7 +26,8 @@ web_environment: - ADMIN_USER=admin - ADMIN_PASS=admin - ADMIN_EMAIL=admin@example.com - - WC_VERSION=6.1.0 + - WC_VERSION=7.7.2 + - PCP_BLOCKS_ENABLED=1 # Key features of ddev's config.yaml: diff --git a/.env.e2e.example b/.env.e2e.example index c8023a9cb..f080f8faf 100644 --- a/.env.e2e.example +++ b/.env.e2e.example @@ -1,11 +1,35 @@ PPCP_E2E_WP_DIR=${ROOT_DIR}/.ddev/wordpress BASEURL="https://woocommerce-paypal-payments.ddev.site" +AUTHORIZATION="Bearer ABC123" + +CHECKOUT_URL="/checkout" +CHECKOUT_PAGE_ID=7 +CART_URL="/cart" +BLOCK_CHECKOUT_URL="/checkout-block" +BLOCK_CHECKOUT_PAGE_ID=22 +BLOCK_CART_URL="/cart-block" PRODUCT_URL="/product/prod" +PRODUCT_ID=123 + +SUBSCRIPTION_URL="/product/sub" + +WP_MERCHANT_USER="admin" +WP_MERCHANT_PASSWORD="admin" + +WP_CUSTOMER_USER="customer" +WP_CUSTOMER_PASSWORD="password" CUSTOMER_EMAIL="customer@example.com" CUSTOMER_PASSWORD="password" +CUSTOMER_FIRST_NAME="John" +CUSTOMER_LAST_NAME="Doe" +CUSTOMER_COUNTRY="DE" +CUSTOMER_ADDRESS="street 1" +CUSTOMER_POSTCODE="12345" +CUSTOMER_CITY="city" +CUSTOMER_PHONE="1234567890" CREDIT_CARD_NUMBER="1234567890" CREDIT_CARD_EXPIRATION="01/2042" diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 791fe3a18..c6496f58a 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -7,11 +7,8 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php-versions: ['7.2', '7.4', '8.1'] - wc-versions: ['5.9.5', '7.1.0'] - exclude: - - php-versions: 7.2 - wc-versions: 7.1.0 + php-versions: ['7.3', '7.4', '8.1'] + wc-versions: ['5.9.5', '7.7.2'] name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }} steps: @@ -24,8 +21,10 @@ jobs: - uses: actions/checkout@v1 - - name: Configure DDEV - run: ddev config --php-version ${{ matrix.php-versions }} --web-environment-add="WC_VERSION=${{ matrix.wc-versions }}" + - name: Configure DDEV PHP + run: ddev config --php-version ${{ matrix.php-versions }} + - name: Configure DDEV WC + run: ddev config --web-environment-add="WC_VERSION=${{ matrix.wc-versions }}" - name: Start DDEV run: ddev start diff --git a/.psalm/wcblocks.php b/.psalm/wcblocks.php new file mode 100644 index 000000000..4c8274398 --- /dev/null +++ b/.psalm/wcblocks.php @@ -0,0 +1,381 @@ + $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 ) { +} diff --git a/.psalm/wcs.php b/.psalm/wcs.php index 2106378e2..873fd4d50 100644 --- a/.psalm/wcs.php +++ b/.psalm/wcs.php @@ -1631,7 +1631,7 @@ function wcs_get_order_items_product_id($item_id) * * When acting on cart items or order items, Subscriptions often needs to use an item's canonical product ID. For * items representing a variation, that means the 'variation_id' value, if the item is not a variation, that means - * the 'product_id value. This function helps save keystrokes on the idiom to check if an item is to a variation or not. + * the product_id value. This function helps save keystrokes on the idiom to check if an item is to a variation or not. * * @param array or object $item Either a cart item, order/subscription line item, or a product. */ diff --git a/.psalm/wpcli.php b/.psalm/wpcli.php new file mode 100644 index 000000000..4b2ea7ed8 --- /dev/null +++ b/.psalm/wpcli.php @@ -0,0 +1,651 @@ + ['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:` - Before the command is added. + * * `after_add_command:` - After the command was added. + * * `before_invoke:` (1) - Just before a command is invoked. + * * `after_invoke:` (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 + * * + * * + * * : An awesome message to display + * * + * * --append= + * * : 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=`, `--url=`, and other values of + * the [global configuration parameters](https://wp-cli.org/config/). + * + * ``` + * WP_CLI::log( 'The --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 = [] ) { + } +} diff --git a/changelog.txt b/changelog.txt index d795329a8..8ff7d9506 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,22 @@ *** Changelog *** += 2.1.0 - TBD = +* Fix - Performance issue #1182 +* Fix - Webhooks not registered when onboarding with manual credentials #1223 +* Fix - Boolean false type sent as empty value when setting cache #1313 +* Fix - Ajax vulnerabilities #1411 +* Enhancement - Save and display vaulted payment methods in WC Payment Token API #1059 +* Enhancement - Cache webhook verification results #1379 +* Enhancement - Refresh checkout totals after validation if needed #1294 +* Enhancement - Improve Divi and Elementor Pro compatibility #1254 +* Enhancement - Add MX and JP to ACDC #1415 +* Enhancement - Add fraudnet script to SGO filter #1366 +* Feature preview - Add express cart/checkout block #1346 +* Feature preview - Integrate PayPal Subscriptions API #1217 + += 2.0.5 - 2023-05-31 = +* Fix - Potential invalidation of merchant credentials #1339 + = 2.0.4 - 2023-04-03 = * Fix - Allow Pay Later in mini-cart #1221 * Fix - Duplicated auth error when credentials become wrong #1229 diff --git a/modules.php b/modules.php index 6bbca541d..2962988ce 100644 --- a/modules.php +++ b/modules.php @@ -28,5 +28,12 @@ return function ( string $root_dir ): iterable { ( 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; }; diff --git a/modules/ppcp-api-client/services.php b/modules/ppcp-api-client/services.php index 1b1eb3da0..ce363dabc 100644 --- a/modules/ppcp-api-client/services.php +++ b/modules/ppcp-api-client/services.php @@ -9,6 +9,14 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans; +use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory; +use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; @@ -209,6 +217,30 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, + 'api.endpoint.catalog-products' => static function ( ContainerInterface $container ): CatalogProducts { + return new CatalogProducts( + $container->get( 'api.host' ), + $container->get( 'api.bearer' ), + $container->get( 'api.factory.product' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'api.endpoint.billing-plans' => static function( ContainerInterface $container ): BillingPlans { + return new BillingPlans( + $container->get( 'api.host' ), + $container->get( 'api.bearer' ), + $container->get( 'api.factory.billing-cycle' ), + $container->get( 'api.factory.plan' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, + 'api.endpoint.billing-subscriptions' => static function( ContainerInterface $container ): BillingSubscriptions { + return new BillingSubscriptions( + $container->get( 'api.host' ), + $container->get( 'api.bearer' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, 'api.repository.application-context' => static function( ContainerInterface $container ) : ApplicationContextRepository { $settings = $container->get( 'wcgateway.settings' ); @@ -289,12 +321,19 @@ return array( ); }, 'api.factory.shipping' => static function ( ContainerInterface $container ): ShippingFactory { - $address_factory = $container->get( 'api.factory.address' ); - return new ShippingFactory( $address_factory ); + return new ShippingFactory( + $container->get( 'api.factory.address' ), + $container->get( 'api.factory.shipping-option' ) + ); }, 'api.factory.shipping-preference' => static function ( ContainerInterface $container ): 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 { $item_factory = $container->get( 'api.factory.item' ); return new AmountFactory( @@ -357,6 +396,21 @@ return array( 'api.factory.fraud-processor-response' => static function ( ContainerInterface $container ): FraudProcessorResponseFactory { return new FraudProcessorResponseFactory(); }, + 'api.factory.product' => static function( ContainerInterface $container ): ProductFactory { + return new ProductFactory(); + }, + 'api.factory.billing-cycle' => static function( ContainerInterface $container ): BillingCycleFactory { + return new BillingCycleFactory( $container->get( 'api.shop.currency' ) ); + }, + 'api.factory.payment-preferences' => static function( ContainerInterface $container ):PaymentPreferencesFactory { + return new PaymentPreferencesFactory( $container->get( 'api.shop.currency' ) ); + }, + 'api.factory.plan' => static function( ContainerInterface $container ): PlanFactory { + return new PlanFactory( + $container->get( 'api.factory.billing-cycle' ), + $container->get( 'api.factory.payment-preferences' ) + ); + }, 'api.helpers.dccapplies' => static function ( ContainerInterface $container ) : DccApplies { return new DccApplies( $container->get( 'api.dcc-supported-country-currency-matrix' ), @@ -631,6 +685,27 @@ return array( 'SGD', 'USD', ), + 'MX' => array( + 'MXN', + ), + 'JP' => array( + 'AUD', + 'CAD', + 'CHF', + 'CZK', + 'DKK', + 'EUR', + 'GBP', + 'HKD', + 'HUF', + 'JPY', + 'NOK', + 'NZD', + 'PLN', + 'SEK', + 'SGD', + 'USD', + ), ) ); }, @@ -687,6 +762,17 @@ return array( 'amex' => array( 'CAD' ), 'jcb' => array( 'CAD' ), ), + 'MX' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array(), + ), + 'JP' => array( + 'mastercard' => array(), + 'visa' => array(), + 'amex' => array( 'JPY' ), + 'jcb' => array( 'JPY' ), + ), ) ); }, diff --git a/modules/ppcp-api-client/src/Endpoint/BillingAgreementsEndpoint.php b/modules/ppcp-api-client/src/Endpoint/BillingAgreementsEndpoint.php index e226253a2..233c41fe2 100644 --- a/modules/ppcp-api-client/src/Endpoint/BillingAgreementsEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/BillingAgreementsEndpoint.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint; +use Exception; use stdClass; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; @@ -120,6 +121,10 @@ class BillingAgreementsEndpoint { */ public function reference_transaction_enabled(): bool { try { + if ( get_transient( 'ppcp_reference_transaction_enabled' ) === true ) { + return true; + } + $this->is_request_logging_enabled = false; try { @@ -130,10 +135,12 @@ class BillingAgreementsEndpoint { ); } finally { $this->is_request_logging_enabled = true; + set_transient( 'ppcp_reference_transaction_enabled', true, 3 * MONTH_IN_SECONDS ); } return true; - } catch ( PayPalApiException $exception ) { + } catch ( Exception $exception ) { + delete_transient( 'ppcp_reference_transaction_enabled' ); return false; } } diff --git a/modules/ppcp-api-client/src/Endpoint/BillingPlans.php b/modules/ppcp-api-client/src/Endpoint/BillingPlans.php new file mode 100644 index 000000000..57bb8c406 --- /dev/null +++ b/modules/ppcp-api-client/src/Endpoint/BillingPlans.php @@ -0,0 +1,229 @@ +host = $host; + $this->bearer = $bearer; + $this->billing_cycle_factory = $billing_cycle_factory; + $this->plan_factory = $plan_factory; + $this->logger = $logger; + } + + /** + * Creates a subscription plan. + * + * @param string $name Product name. + * @param string $product_id Product ID. + * @param array $billing_cycles Billing cycles. + * @param array $payment_preferences Payment preferences. + * + * @return Plan + * + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function create( + string $name, + string $product_id, + array $billing_cycles, + array $payment_preferences + ): Plan { + + $data = array( + 'name' => $name, + 'product_id' => $product_id, + 'billing_cycles' => $billing_cycles, + 'payment_preferences' => $payment_preferences, + ); + + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/billing/plans'; + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ), + 'body' => wp_json_encode( $data ), + ); + + $response = $this->request( $url, $args ); + + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + throw new RuntimeException( 'Not able to create plan.' ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 201 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + + return $this->plan_factory->from_paypal_response( $json ); + } + + /** + * Returns a plan, + * + * @param string $id Plan ID. + * + * @return Plan + * + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function plan( string $id ): Plan { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/billing/plans/' . $id; + $args = array( + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ), + ); + + $response = $this->request( $url, $args ); + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + throw new RuntimeException( 'Not able to get plan.' ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 200 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + + return $this->plan_factory->from_paypal_response( $json ); + } + + /** + * Updates pricing. + * + * @param string $id Plan ID. + * @param BillingCycle $billing_cycle Billing cycle. + * + * @return void + * + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function update_pricing( string $id, BillingCycle $billing_cycle ): void { + $data = array( + 'pricing_schemes' => array( + (object) array( + 'billing_cycle_sequence' => 1, + 'pricing_scheme' => $billing_cycle->pricing_scheme(), + ), + ), + ); + + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/billing/plans/' . $id . '/update-pricing-schemes'; + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( $data ), + ); + + $response = $this->request( $url, $args ); + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + throw new RuntimeException( 'Could not update pricing.' ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 204 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + } +} diff --git a/modules/ppcp-api-client/src/Endpoint/BillingSubscriptions.php b/modules/ppcp-api-client/src/Endpoint/BillingSubscriptions.php new file mode 100644 index 000000000..3145ffc7d --- /dev/null +++ b/modules/ppcp-api-client/src/Endpoint/BillingSubscriptions.php @@ -0,0 +1,217 @@ +host = $host; + $this->bearer = $bearer; + $this->logger = $logger; + } + + /** + * Suspends a subscription. + * + * @param string $id Subscription ID. + * @return void + * + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function suspend( string $id ):void { + $data = array( + 'reason' => 'Suspended by customer', + ); + + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id . '/suspend'; + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( $data ), + ); + + $response = $this->request( $url, $args ); + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + throw new RuntimeException( 'Not able to suspend subscription.' ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 204 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + } + + /** + * Activates a subscription. + * + * @param string $id Subscription ID. + * @return void + * + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function activate( string $id ): void { + $data = array( + 'reason' => 'Reactivated by customer', + ); + + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id . '/activate'; + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( $data ), + ); + + $response = $this->request( $url, $args ); + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + throw new RuntimeException( 'Not able to reactivate subscription.' ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 204 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + } + + /** + * Cancels a Subscription. + * + * @param string $id Subscription ID. + * + * @return void + * + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function cancel( string $id ): void { + $data = array( + 'reason' => 'Cancelled by customer', + ); + + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id . '/cancel'; + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + ), + 'body' => wp_json_encode( $data ), + ); + + $response = $this->request( $url, $args ); + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + throw new RuntimeException( 'Not able to cancel subscription.' ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 204 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + } + + /** + * Returns a Subscription object from the given ID. + * + * @param string $id Subscription ID. + * + * @return stdClass + * + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function subscription( string $id ): stdClass { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/billing/subscriptions/' . $id; + $args = array( + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ), + ); + + $response = $this->request( $url, $args ); + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + throw new RuntimeException( 'Not able to get subscription.' ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 200 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + + return $json; + } +} diff --git a/modules/ppcp-api-client/src/Endpoint/CatalogProducts.php b/modules/ppcp-api-client/src/Endpoint/CatalogProducts.php new file mode 100644 index 000000000..32296f4ee --- /dev/null +++ b/modules/ppcp-api-client/src/Endpoint/CatalogProducts.php @@ -0,0 +1,199 @@ +host = $host; + $this->bearer = $bearer; + $this->product_factory = $product_factory; + $this->logger = $logger; + } + + /** + * Creates a product. + * + * @param string $name Product name. + * @param string $description Product description. + * + * @return Product + * + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function create( string $name, string $description ): Product { + $data = array( + 'name' => $name, + ); + + if ( $description ) { + $data['description'] = $description; + } + + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/catalogs/products'; + $args = array( + 'method' => 'POST', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ), + 'body' => wp_json_encode( $data ), + ); + + $response = $this->request( $url, $args ); + + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + throw new RuntimeException( 'Not able to create product.' ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 201 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + + return $this->product_factory->from_paypal_response( $json ); + } + + /** + * Updates a Product. + * + * @param string $id Product ID. + * @param array $data Data to update. + * + * @return void + * + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function update( string $id, array $data ): void { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/catalogs/products/' . $id; + $args = array( + 'method' => 'PATCH', + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ), + 'body' => wp_json_encode( $data ), + ); + + $response = $this->request( $url, $args ); + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + throw new RuntimeException( 'Not able to update product.' ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 204 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + } + + /** + * Return a Product from the given ID. + * + * @param string $id Product ID. + * + * @return Product + * + * @throws RuntimeException If the request fails. + * @throws PayPalApiException If the request fails. + */ + public function product( string $id ): Product { + $bearer = $this->bearer->bearer(); + $url = trailingslashit( $this->host ) . 'v1/catalogs/products/' . $id; + $args = array( + 'headers' => array( + 'Authorization' => 'Bearer ' . $bearer->token(), + 'Content-Type' => 'application/json', + 'Prefer' => 'return=representation', + ), + ); + + $response = $this->request( $url, $args ); + if ( is_wp_error( $response ) || ! is_array( $response ) ) { + throw new RuntimeException( 'Not able to get product.' ); + } + + $json = json_decode( $response['body'] ); + $status_code = (int) wp_remote_retrieve_response_code( $response ); + if ( 200 !== $status_code ) { + throw new PayPalApiException( + $json, + $status_code + ); + } + + return $this->product_factory->from_paypal_response( $json ); + } +} diff --git a/modules/ppcp-api-client/src/Endpoint/IdentityToken.php b/modules/ppcp-api-client/src/Endpoint/IdentityToken.php index 0c27e3fe4..42265e26c 100644 --- a/modules/ppcp-api-client/src/Endpoint/IdentityToken.php +++ b/modules/ppcp-api-client/src/Endpoint/IdentityToken.php @@ -103,7 +103,8 @@ class IdentityToken { ); if ( ( $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ) ) - && defined( 'PPCP_FLAG_SUBSCRIPTION' ) && PPCP_FLAG_SUBSCRIPTION + || ( $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ) ) + || ( $this->settings->has( 'subscriptions_mode' ) && $this->settings->get( 'subscriptions_mode' ) === 'vaulting_api' ) ) { $customer_id = $this->customer_repository->customer_id_for_user( ( $user_id ) ); update_user_meta( $user_id, 'ppcp_customer_id', $customer_id ); diff --git a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php index 249f5a106..cf22e8106 100644 --- a/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php +++ b/modules/ppcp-api-client/src/Endpoint/OrderEndpoint.php @@ -16,6 +16,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\AuthorizationStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus; use WooCommerce\PayPalCommerce\ApiClient\Entity\Order; use WooCommerce\PayPalCommerce\ApiClient\Entity\OrderStatus; +use WooCommerce\PayPalCommerce\ApiClient\Entity\PatchCollection; use WooCommerce\PayPalCommerce\ApiClient\Entity\Payer; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentMethod; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; @@ -180,6 +181,8 @@ class OrderEndpoint { * @param Payer|null $payer The payer off the order. * @param PaymentToken|null $payment_token The payment token. * @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 * @throws RuntimeException If the request fails. @@ -189,19 +192,28 @@ class OrderEndpoint { string $shipping_preference, Payer $payer = 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 { $bearer = $this->bearer->bearer(); $data = array( 'intent' => ( $this->subscription_helper->cart_contains_subscription() || $this->subscription_helper->current_product_is_subscription() ) ? 'AUTHORIZE' : $this->intent, 'purchase_units' => array_map( - static function ( PurchaseUnit $item ): array { - return $item->to_array(); + static function ( PurchaseUnit $item ) use ( $shipping_preference ): 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 ), '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() ) ) { $data['payer'] = $payer->to_array(); @@ -510,13 +522,22 @@ class OrderEndpoint { 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(); - 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). @@ -524,7 +545,7 @@ class OrderEndpoint { $patches_array = apply_filters( 'ppcp_patch_order_request_body_data', $patches_array ); $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( 'method' => 'PATCH', 'headers' => array( @@ -540,11 +561,8 @@ class OrderEndpoint { $response = $this->request( $url, $args ); if ( is_wp_error( $response ) ) { - $error = new RuntimeException( - __( 'Could not retrieve order.', 'woocommerce-paypal-payments' ) - ); - $this->logger->log( - 'warning', + $error = new RuntimeException( 'Could not patch order.' ); + $this->logger->warning( $error->getMessage(), array( 'args' => $args, @@ -560,8 +578,7 @@ class OrderEndpoint { $json, $status_code ); - $this->logger->log( - 'warning', + $this->logger->warning( $error->getMessage(), array( 'args' => $args, @@ -570,9 +587,6 @@ class OrderEndpoint { ); throw $error; } - - $new_order = $this->order( $order_to_update->id() ); - return $new_order; } /** diff --git a/modules/ppcp-api-client/src/Entity/BillingCycle.php b/modules/ppcp-api-client/src/Entity/BillingCycle.php new file mode 100644 index 000000000..76f1c25fa --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/BillingCycle.php @@ -0,0 +1,134 @@ +frequency = $frequency; + $this->sequence = $sequence; + $this->tenure_type = $tenure_type; + $this->pricing_scheme = $pricing_scheme; + $this->total_cycles = $total_cycles; + } + + /** + * Returns frequency. + * + * @return array + */ + public function frequency(): array { + return $this->frequency; + } + + /** + * Returns sequence. + * + * @return int + */ + public function sequence(): int { + return $this->sequence; + } + + /** + * Returns tenure type. + * + * @return string + */ + public function tenure_type(): string { + return $this->tenure_type; + } + + /** + * Returns pricing scheme. + * + * @return array + */ + public function pricing_scheme(): array { + return $this->pricing_scheme; + } + + /** + * Return total cycles. + * + * @return int + */ + public function total_cycles(): int { + return $this->total_cycles; + } + + /** + * Returns Billing Cycle as array. + * + * @return array + */ + public function to_array(): array { + return array( + 'frequency' => $this->frequency(), + 'sequence' => $this->sequence(), + 'tenure_type' => $this->tenure_type(), + 'pricing_scheme' => $this->pricing_scheme(), + 'total_cycles' => $this->total_cycles(), + ); + } +} diff --git a/modules/ppcp-api-client/src/Entity/Patch.php b/modules/ppcp-api-client/src/Entity/Patch.php index 4350565cf..8814532ea 100644 --- a/modules/ppcp-api-client/src/Entity/Patch.php +++ b/modules/ppcp-api-client/src/Entity/Patch.php @@ -83,8 +83,8 @@ class Patch { public function to_array(): array { return array( 'op' => $this->op(), - 'value' => $this->value(), 'path' => $this->path(), + 'value' => $this->value(), ); } diff --git a/modules/ppcp-api-client/src/Entity/PaymentPreferences.php b/modules/ppcp-api-client/src/Entity/PaymentPreferences.php new file mode 100644 index 000000000..b257c2389 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/PaymentPreferences.php @@ -0,0 +1,115 @@ +setup_fee = $setup_fee; + $this->auto_bill_outstanding = $auto_bill_outstanding; + $this->setup_fee_failure_action = $setup_fee_failure_action; + $this->payment_failure_threshold = $payment_failure_threshold; + } + + /** + * Setup fee. + * + * @return array + */ + public function setup_fee(): array { + return $this->setup_fee; + } + + /** + * Auto bill outstanding. + * + * @return bool + */ + public function auto_bill_outstanding(): bool { + return $this->auto_bill_outstanding; + } + + /** + * Setup fee failure action. + * + * @return string + */ + public function setup_fee_failure_action(): string { + return $this->setup_fee_failure_action; + } + + /** + * Payment failure threshold. + * + * @return int + */ + public function payment_failure_threshold(): int { + return $this->payment_failure_threshold; + } + + /** + * Returns Payment Preferences as array. + * + * @return array + */ + public function to_array():array { + return array( + 'setup_fee' => $this->setup_fee(), + 'auto_bill_outstanding' => $this->auto_bill_outstanding(), + 'setup_fee_failure_action' => $this->setup_fee_failure_action(), + 'payment_failure_threshold' => $this->payment_failure_threshold(), + ); + } +} diff --git a/modules/ppcp-api-client/src/Entity/Plan.php b/modules/ppcp-api-client/src/Entity/Plan.php new file mode 100644 index 000000000..06747776b --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/Plan.php @@ -0,0 +1,154 @@ +id = $id; + $this->name = $name; + $this->product_id = $product_id; + $this->billing_cycles = $billing_cycles; + $this->payment_preferences = $payment_preferences; + $this->status = $status; + } + + /** + * Returns Plan ID. + * + * @return string + */ + public function id(): string { + return $this->id; + } + + /** + * Returns Plan name. + * + * @return string + */ + public function name(): string { + return $this->name; + } + + /** + * Returns Product ID. + * + * @return string + */ + public function product_id(): string { + return $this->product_id; + } + + /** + * Returns Billing cycles. + * + * @return array + */ + public function billing_cycles(): array { + return $this->billing_cycles; + } + + /** + * Returns Payment preferences. + * + * @return PaymentPreferences + */ + public function payment_preferences(): PaymentPreferences { + return $this->payment_preferences; + } + + /** + * Returns Plan status. + * + * @return string + */ + public function status(): string { + return $this->status; + } + + /** + * Returns Plan as array. + * + * @return array + */ + public function to_array():array { + return array( + 'id' => $this->id(), + 'name' => $this->name(), + 'product_id' => $this->product_id(), + 'billing_cycles' => $this->billing_cycles(), + 'payment_preferences' => $this->payment_preferences(), + 'status' => $this->status(), + ); + } +} diff --git a/modules/ppcp-api-client/src/Entity/Product.php b/modules/ppcp-api-client/src/Entity/Product.php new file mode 100644 index 000000000..63acecc80 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/Product.php @@ -0,0 +1,90 @@ +id = $id; + $this->name = $name; + $this->description = $description; + } + + /** + * Returns the product ID. + * + * @return string + */ + public function id(): string { + return $this->id; + } + + /** + * Returns the product name. + * + * @return string + */ + public function name(): string { + return $this->name; + } + + /** + * Returns the product description. + * + * @return string + */ + public function description(): string { + return $this->description; + } + + /** + * Returns the object as array. + * + * @return array + */ + public function to_array() { + return array( + 'id' => $this->id(), + 'name' => $this->name(), + 'description' => $this->description(), + ); + } +} diff --git a/modules/ppcp-api-client/src/Entity/Shipping.php b/modules/ppcp-api-client/src/Entity/Shipping.php index 244d876f3..eeb4001dc 100644 --- a/modules/ppcp-api-client/src/Entity/Shipping.php +++ b/modules/ppcp-api-client/src/Entity/Shipping.php @@ -28,15 +28,24 @@ class Shipping { */ private $address; + /** + * Shipping methods. + * + * @var ShippingOption[] + */ + private $options; + /** * Shipping constructor. * - * @param string $name The name. - * @param Address $address The address. + * @param string $name The name. + * @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->address = $address; + $this->options = $options; } /** @@ -57,17 +66,35 @@ class Shipping { return $this->address; } + /** + * Returns the shipping methods. + * + * @return ShippingOption[] + */ + public function options(): array { + return $this->options; + } + /** * Returns the object as array. * * @return array */ public function to_array(): array { - return array( + $result = array( 'name' => array( 'full_name' => $this->name(), ), 'address' => $this->address()->to_array(), ); + if ( $this->options ) { + $result['options'] = array_map( + function ( ShippingOption $opt ): array { + return $opt->to_array(); + }, + $this->options + ); + } + return $result; } } diff --git a/modules/ppcp-api-client/src/Entity/ShippingOption.php b/modules/ppcp-api-client/src/Entity/ShippingOption.php new file mode 100644 index 000000000..40a7b5ef7 --- /dev/null +++ b/modules/ppcp-api-client/src/Entity/ShippingOption.php @@ -0,0 +1,139 @@ +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, + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/BillingCycleFactory.php b/modules/ppcp-api-client/src/Factory/BillingCycleFactory.php new file mode 100644 index 000000000..6b00ff0ac --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/BillingCycleFactory.php @@ -0,0 +1,84 @@ +currency = $currency; + } + + /** + * Returns a BillingCycle object from the given WC product. + * + * @param WC_Product $product WC product. + * @return BillingCycle + */ + public function from_wc_product( WC_Product $product ): BillingCycle { + return new BillingCycle( + array( + 'interval_unit' => $product->get_meta( '_subscription_period' ), + 'interval_count' => $product->get_meta( '_subscription_period_interval' ), + ), + 1, + 'REGULAR', + array( + 'fixed_price' => array( + 'value' => $product->get_meta( '_subscription_price' ), + 'currency_code' => $this->currency, + ), + ), + (int) $product->get_meta( '_subscription_length' ) + ); + } + + /** + * Returns a BillingCycle object based off a PayPal response. + * + * @param stdClass $data the data. + * @return BillingCycle + */ + public function from_paypal_response( stdClass $data ): BillingCycle { + return new BillingCycle( + array( + 'interval_unit' => $data->frequency->interval_unit, + 'interval_count' => $data->frequency->interval_count, + ), + $data->sequence, + $data->tenure_type, + array( + 'fixed_price' => array( + 'value' => $data->pricing_scheme->fixed_price->value, + 'currency_code' => $data->pricing_scheme->fixed_price->currency_code, + ), + ), + $data->total_cycles + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/PatchCollectionFactory.php b/modules/ppcp-api-client/src/Factory/PatchCollectionFactory.php index 3be240393..d6ae8b7fc 100644 --- a/modules/ppcp-api-client/src/Factory/PatchCollectionFactory.php +++ b/modules/ppcp-api-client/src/Factory/PatchCollectionFactory.php @@ -71,7 +71,15 @@ class PatchCollectionFactory { ); $operation = $purchase_unit_from ? 'replace' : 'add'; $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, $path . "/@reference_id=='" . $purchase_unit_to->reference_id() . "'", $value diff --git a/modules/ppcp-api-client/src/Factory/PaymentPreferencesFactory.php b/modules/ppcp-api-client/src/Factory/PaymentPreferencesFactory.php new file mode 100644 index 000000000..44c0d0cef --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/PaymentPreferencesFactory.php @@ -0,0 +1,69 @@ +currency = $currency; + } + + /** + * Returns a PaymentPreferences object from the given WC product. + * + * @param WC_Product $product WC product. + * @return PaymentPreferences + */ + public function from_wc_product( WC_Product $product ):PaymentPreferences { + return new PaymentPreferences( + array( + 'value' => $product->get_meta( '_subscription_sign_up_fee' ) ?: '0', + 'currency_code' => $this->currency, + ) + ); + } + + /** + * Returns a PaymentPreferences object based off a PayPal response. + * + * @param stdClass $data The data. + * @return PaymentPreferences + */ + public function from_paypal_response( stdClass $data ) { + return new PaymentPreferences( + array( + 'value' => $data->setup_fee->value, + 'currency_code' => $data->setup_fee->currency_code, + ), + $data->auto_bill_outstanding, + $data->setup_fee_failure_action, + $data->payment_failure_threshold + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/PlanFactory.php b/modules/ppcp-api-client/src/Factory/PlanFactory.php new file mode 100644 index 000000000..bbf332d10 --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/PlanFactory.php @@ -0,0 +1,96 @@ +billing_cycle_factory = $billing_cycle_factory; + $this->payment_preferences_factory = $payment_preferences_factory; + } + + /** + * Returns a Plan from PayPal response. + * + * @param stdClass $data The data. + * + * @return Plan + * + * @throws RuntimeException If it could not create Plan. + */ + public function from_paypal_response( stdClass $data ): Plan { + if ( ! isset( $data->id ) ) { + throw new RuntimeException( + __( 'No id for given plan', 'woocommerce-paypal-payments' ) + ); + } + if ( ! isset( $data->name ) ) { + throw new RuntimeException( + __( 'No name for plan given', 'woocommerce-paypal-payments' ) + ); + } + if ( ! isset( $data->product_id ) ) { + throw new RuntimeException( + __( 'No product id for given plan', 'woocommerce-paypal-payments' ) + ); + } + if ( ! isset( $data->billing_cycles ) ) { + throw new RuntimeException( + __( 'No billing cycles for given plan', 'woocommerce-paypal-payments' ) + ); + } + + $billing_cycles = array(); + foreach ( $data->billing_cycles as $billing_cycle ) { + $billing_cycles[] = $this->billing_cycle_factory->from_paypal_response( $billing_cycle ); + } + + $payment_preferences = $this->payment_preferences_factory->from_paypal_response( $data->payment_preferences ); + + return new Plan( + $data->id, + $data->name, + $data->product_id, + $billing_cycles, + $payment_preferences, + $data->status ?? '' + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/ProductFactory.php b/modules/ppcp-api-client/src/Factory/ProductFactory.php new file mode 100644 index 000000000..a34602a21 --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/ProductFactory.php @@ -0,0 +1,47 @@ +id ) ) { + throw new RuntimeException( + __( 'No id for product given', 'woocommerce-paypal-payments' ) + ); + } + if ( ! isset( $data->name ) ) { + throw new RuntimeException( + __( 'No name for product given', 'woocommerce-paypal-payments' ) + ); + } + + return new Product( + $data->id, + $data->name, + $data->description ?? '' + ); + } +} diff --git a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php index c2667fc92..9f8f12c5e 100644 --- a/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php +++ b/modules/ppcp-api-client/src/Factory/PurchaseUnitFactory.php @@ -153,10 +153,11 @@ class PurchaseUnitFactory { * Creates a PurchaseUnit based off a WooCommerce cart. * * @param \WC_Cart|null $cart The cart. + * @param bool $with_shipping_options Include WC shipping methods. * * @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 ) { $cart = WC()->cart ?? new \WC_Cart(); } @@ -172,7 +173,7 @@ class PurchaseUnitFactory { $shipping = null; $customer = \WC()->customer; 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 ( 2 !== strlen( $shipping->address()->country_code() ) || ( ! $shipping->address()->postal_code() && ! $this->country_without_postal_code( $shipping->address()->country_code() ) ) diff --git a/modules/ppcp-api-client/src/Factory/ShippingFactory.php b/modules/ppcp-api-client/src/Factory/ShippingFactory.php index dcebb9b9f..b85259394 100644 --- a/modules/ppcp-api-client/src/Factory/ShippingFactory.php +++ b/modules/ppcp-api-client/src/Factory/ShippingFactory.php @@ -10,6 +10,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\ApiClient\Factory; use WooCommerce\PayPalCommerce\ApiClient\Entity\Shipping; +use WooCommerce\PayPalCommerce\ApiClient\Entity\ShippingOption; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; /** @@ -24,23 +25,33 @@ class ShippingFactory { */ private $address_factory; + /** + * The shipping option factory. + * + * @var ShippingOptionFactory + */ + private $shipping_option_factory; + /** * 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 ) { - $this->address_factory = $address_factory; + public function __construct( AddressFactory $address_factory, ShippingOptionFactory $shipping_option_factory ) { + $this->address_factory = $address_factory; + $this->shipping_option_factory = $shipping_option_factory; } /** * Creates a shipping object based off a WooCommerce customer. * * @param \WC_Customer $customer The WooCommerce customer. + * @param bool $with_shipping_options Include WC shipping methods. * * @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(). $full_name = sprintf( // 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 ); return new Shipping( $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 ); + $options = array_map( + array( $this->shipping_option_factory, 'from_paypal_response' ), + $data->options ?? array() + ); return new Shipping( $data->name->full_name, - $address + $address, + $options ); } } diff --git a/modules/ppcp-api-client/src/Factory/ShippingOptionFactory.php b/modules/ppcp-api-client/src/Factory/ShippingOptionFactory.php new file mode 100644 index 000000000..08af8c79e --- /dev/null +++ b/modules/ppcp-api-client/src/Factory/ShippingOptionFactory.php @@ -0,0 +1,111 @@ +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 + ); + } +} diff --git a/modules/ppcp-api-client/src/Repository/ApplicationContextRepository.php b/modules/ppcp-api-client/src/Repository/ApplicationContextRepository.php index 03c619495..acfb4a6cf 100644 --- a/modules/ppcp-api-client/src/Repository/ApplicationContextRepository.php +++ b/modules/ppcp-api-client/src/Repository/ApplicationContextRepository.php @@ -38,11 +38,13 @@ class ApplicationContextRepository { * Returns the current application context. * * @param string $shipping_preferences The shipping preferences. + * @param string $user_action The user action. * * @return ApplicationContext */ 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 { $brand_name = $this->settings->has( 'brand_name' ) ? $this->settings->get( 'brand_name' ) : ''; @@ -55,7 +57,8 @@ class ApplicationContextRepository { (string) $brand_name, $locale, (string) $landingpage, - $shipping_preferences + $shipping_preferences, + $user_action ); return $context; } diff --git a/modules/ppcp-blocks/.babelrc b/modules/ppcp-blocks/.babelrc new file mode 100644 index 000000000..822778e6c --- /dev/null +++ b/modules/ppcp-blocks/.babelrc @@ -0,0 +1,14 @@ +{ + "presets": [ + [ + "@babel/preset-env", + { + "useBuiltIns": "usage", + "corejs": "3.25.0" + } + ], + [ + "@babel/preset-react" + ] + ] +} diff --git a/modules/ppcp-blocks/.gitignore b/modules/ppcp-blocks/.gitignore new file mode 100644 index 000000000..0bd2b9f58 --- /dev/null +++ b/modules/ppcp-blocks/.gitignore @@ -0,0 +1,3 @@ +node_modules +assets/js +assets/css diff --git a/modules/ppcp-blocks/composer.json b/modules/ppcp-blocks/composer.json new file mode 100644 index 000000000..f71b084a2 --- /dev/null +++ b/modules/ppcp-blocks/composer.json @@ -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 +} diff --git a/modules/ppcp-blocks/extensions.php b/modules/ppcp-blocks/extensions.php new file mode 100644 index 000000000..31f90576d --- /dev/null +++ b/modules/ppcp-blocks/extensions.php @@ -0,0 +1,74 @@ + 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. +

If this setting is not enabled, payment confirmation on the checkout will be skipped. +Skipping the final confirmation on the checkout page may impact the buyer experience during the PayPal payment process.

', + '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' ); + }, +); diff --git a/modules/ppcp-blocks/module.php b/modules/ppcp-blocks/module.php new file mode 100644 index 000000000..cfb4f43f9 --- /dev/null +++ b/modules/ppcp-blocks/module.php @@ -0,0 +1,16 @@ + 0.5%", + "Safari >= 8", + "Chrome >= 41", + "Firefox >= 43", + "Edge >= 14" + ], + "dependencies": { + "core-js": "^3.25.0" + }, + "devDependencies": { + "@babel/core": "^7.19", + "@babel/preset-env": "^7.19", + "@babel/preset-react": "^7.18.6", + "@woocommerce/dependency-extraction-webpack-plugin": "^2.2.0", + "babel-loader": "^8.2", + "cross-env": "^7.0.3", + "file-loader": "^6.2.0", + "sass": "^1.42.1", + "sass-loader": "^12.1.0", + "webpack": "^5.76", + "webpack-cli": "^4.10" + }, + "scripts": { + "build": "cross-env BABEL_ENV=default NODE_ENV=production webpack", + "watch": "cross-env BABEL_ENV=default NODE_ENV=production webpack --watch", + "dev": "cross-env BABEL_ENV=default webpack --watch" + } +} diff --git a/modules/ppcp-blocks/resources/js/Helper/Address.js b/modules/ppcp-blocks/resources/js/Helper/Address.js new file mode 100644 index 000000000..d522d208e --- /dev/null +++ b/modules/ppcp-blocks/resources/js/Helper/Address.js @@ -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}; +} diff --git a/modules/ppcp-blocks/resources/js/checkout-block.js b/modules/ppcp-blocks/resources/js/checkout-block.js new file mode 100644 index 000000000..1766fcf26 --- /dev/null +++ b/modules/ppcp-blocks/resources/js/checkout-block.js @@ -0,0 +1,269 @@ +import {useEffect, useState} from '@wordpress/element'; +import {registerExpressPaymentMethod, registerPaymentMethod} from '@woocommerce/blocks-registry'; +import {paypalAddressToWc, paypalOrderToWcAddresses} from "./Helper/Address"; +import {loadPaypalScript} from '../../../ppcp-button/resources/js/modules/Helper/ScriptLoading' + +const config = wc.wcSettings.getSetting('ppcp-gateway_data'); + +window.ppcpFundingSource = config.fundingSource; + +const PayPalComponent = ({ + onClick, + onClose, + onSubmit, + onError, + eventRegistration, + emitResponse, + activePaymentMethod, + shippingData, +}) => { + const {onPaymentSetup, onCheckoutAfterProcessingWithError} = eventRegistration; + const {responseTypes} = emitResponse; + + const [paypalOrder, setPaypalOrder] = useState(null); + + const [loaded, setLoaded] = useState(false); + useEffect(() => { + if (!loaded) { + loadPaypalScript(config.scriptData, () => { + setLoaded(true); + }); + } + }, [loaded]); + + const createOrder = async () => { + try { + const res = await fetch(config.scriptData.ajax.create_order.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify({ + nonce: config.scriptData.ajax.create_order.nonce, + bn_code: '', + context: config.scriptData.context, + payment_method: 'ppcp-gateway', + createaccount: false + }), + }); + + const json = await res.json(); + + if (!json.success) { + if (json.data?.details?.length > 0) { + throw new Error(json.data.details.map(d => `${d.issue} ${d.description}`).join('
')); + } else if (json.data?.message) { + throw new Error(json.data.message); + } + + throw new Error(config.scriptData.labels.error.generic); + } + return json.data.id; + } catch (err) { + console.error(err); + + onError(err.message); + + onClose(); + + throw err; + } + }; + + const handleApprove = async (data, actions) => { + try { + const res = await fetch(config.scriptData.ajax.approve_order.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify({ + nonce: config.scriptData.ajax.approve_order.nonce, + order_id: data.orderID, + funding_source: window.ppcpFundingSource ?? 'paypal', + }) + }); + + const json = await res.json(); + + if (!json.success) { + if (typeof actions !== 'undefined' && typeof actions.restart !== 'undefined') { + return actions.restart(); + } + if (json.data?.message) { + throw new Error(json.data.message); + } + + throw new Error(config.scriptData.labels.error.generic) + } + + const order = await actions.order.get(); + + setPaypalOrder(order); + + if (config.finalReviewEnabled) { + const addresses = paypalOrderToWcAddresses(order); + + await wp.data.dispatch('wc/store/cart').updateCustomerData({ + billing_address: addresses.billingAddress, + shipping_address: addresses.shippingAddress, + }); + const checkoutUrl = new URL(config.scriptData.redirect); + // sometimes some browsers may load some kind of cached version of the page, + // so adding a parameter to avoid that + checkoutUrl.searchParams.append('ppcp-continuation-redirect', (new Date()).getTime().toString()); + + location.href = checkoutUrl.toString(); + } else { + onSubmit(); + } + } catch (err) { + console.error(err); + + onError(err.message); + + onClose(); + + throw err; + } + }; + + const handleClick = (data, actions) => { + window.ppcpFundingSource = data.fundingSource; + + onClick(); + }; + + let handleShippingChange = null; + if (shippingData.needsShipping && !config.finalReviewEnabled) { + handleShippingChange = async (data, actions) => { + try { + const shippingOptionId = data.selected_shipping_option?.id; + if (shippingOptionId) { + await shippingData.setSelectedRates(shippingOptionId); + } + + const address = paypalAddressToWc(data.shipping_address); + + await wp.data.dispatch('wc/store/cart').updateCustomerData({ + shipping_address: address, + }); + + await shippingData.setShippingAddress(address); + + const res = await fetch(config.ajax.update_shipping.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify({ + nonce: config.ajax.update_shipping.nonce, + order_id: data.orderID, + }) + }); + + const json = await res.json(); + + if (!json.success) { + throw new Error(json.data.message); + } + } catch (e) { + console.error(e); + + actions.reject(); + } + }; + } + + useEffect(() => { + if (activePaymentMethod !== config.id) { + return; + } + + const unsubscribeProcessing = onPaymentSetup(() => { + if (config.scriptData.continuation) { + return { + type: responseTypes.SUCCESS, + meta: { + paymentMethodData: { + 'paypal_order_id': config.scriptData.continuation.order_id, + 'funding_source': window.ppcpFundingSource ?? 'paypal', + } + }, + }; + } + + const addresses = paypalOrderToWcAddresses(paypalOrder); + + return { + type: responseTypes.SUCCESS, + meta: { + paymentMethodData: { + 'paypal_order_id': paypalOrder.id, + 'funding_source': window.ppcpFundingSource ?? 'paypal', + }, + ...addresses, + }, + }; + }); + return () => { + unsubscribeProcessing(); + }; + }, [onPaymentSetup, paypalOrder, activePaymentMethod]); + + useEffect(() => { + const unsubscribe = onCheckoutAfterProcessingWithError(({ processingResponse }) => { + if (onClose) { + onClose(); + } + if (processingResponse?.paymentDetails?.errorMessage) { + return { + type: emitResponse.responseTypes.ERROR, + message: processingResponse.paymentDetails.errorMessage, + messageContext: config.scriptData.continuation ? emitResponse.noticeContexts.PAYMENTS : emitResponse.noticeContexts.EXPRESS_PAYMENTS, + }; + } + return true; + }); + return unsubscribe; + }, [onCheckoutAfterProcessingWithError, onClose]); + + if (config.scriptData.continuation) { + return ( +
+ +
+ ) + } + + if (!loaded) { + return null; + } + + const PayPalButton = window.paypal.Buttons.driver("react", { React, ReactDOM }); + + return ( + + ); +} + +const features = ['products']; +let registerMethod = registerExpressPaymentMethod; +if (config.scriptData.continuation) { + features.push('ppcp_continuation'); + registerMethod = registerPaymentMethod; +} + +registerMethod({ + name: config.id, + label:
, + content: , + edit: TODO: editing, + ariaLabel: config.title, + canMakePayment: () => config.enabled, + supports: { + features: features, + }, +}); diff --git a/modules/ppcp-blocks/services.php b/modules/ppcp-blocks/services.php new file mode 100644 index 000000000..5b5a5159b --- /dev/null +++ b/modules/ppcp-blocks/services.php @@ -0,0 +1,57 @@ + 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' ) + ); + }, +); diff --git a/modules/ppcp-blocks/src/BlocksModule.php b/modules/ppcp-blocks/src/BlocksModule.php new file mode 100644 index 000000000..1766806b7 --- /dev/null +++ b/modules/ppcp-blocks/src/BlocksModule.php @@ -0,0 +1,100 @@ +

%1$s

', + wp_kses_post( + __( + 'PayPal checkout block initialization failed, possibly old WooCommerce version or disabled WooCommerce Blocks plugin.', + 'woocommerce-paypal-payments' + ) + ) + ); + } + ); + + return; + } + + add_action( + 'woocommerce_blocks_payment_method_type_registration', + function( PaymentMethodRegistry $payment_method_registry ) use ( $c ): void { + $payment_method_registry->register( $c->get( 'blocks.method' ) ); + } + ); + + woocommerce_store_api_register_payment_requirements( + array( + 'data_callback' => function() use ( $c ): array { + $smart_button = $c->get( 'button.smart-button' ); + assert( $smart_button instanceof SmartButtonInterface ); + + if ( isset( $smart_button->script_data()['continuation'] ) ) { + return array( 'ppcp_continuation' ); + } + + return array(); + }, + ) + ); + + add_action( + 'wc_ajax_' . UpdateShippingEndpoint::ENDPOINT, + static function () use ( $c ) { + $endpoint = $c->get( 'blocks.endpoint.update-shipping' ); + assert( $endpoint instanceof UpdateShippingEndpoint ); + + $endpoint->handle_request(); + } + ); + } + + /** + * Returns the key for the module. + * + * @return string|void + */ + public function getKey() { + } +} diff --git a/modules/ppcp-blocks/src/Endpoint/UpdateShippingEndpoint.php b/modules/ppcp-blocks/src/Endpoint/UpdateShippingEndpoint.php new file mode 100644 index 000000000..f9322700d --- /dev/null +++ b/modules/ppcp-blocks/src/Endpoint/UpdateShippingEndpoint.php @@ -0,0 +1,133 @@ +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; + } + } +} diff --git a/modules/ppcp-blocks/src/PayPalPaymentMethod.php b/modules/ppcp-blocks/src/PayPalPaymentMethod.php new file mode 100644 index 000000000..1da58914d --- /dev/null +++ b/modules/ppcp-blocks/src/PayPalPaymentMethod.php @@ -0,0 +1,188 @@ +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, + ); + } +} diff --git a/modules/ppcp-blocks/webpack.config.js b/modules/ppcp-blocks/webpack.config.js new file mode 100644 index 000000000..4695769ed --- /dev/null +++ b/modules/ppcp-blocks/webpack.config.js @@ -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'} + ] + }] + } +}; diff --git a/modules/ppcp-blocks/yarn.lock b/modules/ppcp-blocks/yarn.lock new file mode 100644 index 000000000..209adc4a4 --- /dev/null +++ b/modules/ppcp-blocks/yarn.lock @@ -0,0 +1,2201 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@ampproject/remapping@^2.1.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" + integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== + dependencies: + "@jridgewell/gen-mapping" "^0.1.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@babel/code-frame@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" + integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== + dependencies: + "@babel/highlight" "^7.18.6" + +"@babel/compat-data@^7.17.7", "@babel/compat-data@^7.20.0", "@babel/compat-data@^7.20.1": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.5.tgz#86f172690b093373a933223b4745deeb6049e733" + integrity sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g== + +"@babel/core@^7.19": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.5.tgz#45e2114dc6cd4ab167f81daf7820e8fa1250d113" + integrity sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.5" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-module-transforms" "^7.20.2" + "@babel/helpers" "^7.20.5" + "@babel/parser" "^7.20.5" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + +"@babel/generator@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.5.tgz#cb25abee3178adf58d6814b68517c62bdbfdda95" + integrity sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA== + dependencies: + "@babel/types" "^7.20.5" + "@jridgewell/gen-mapping" "^0.3.2" + jsesc "^2.5.1" + +"@babel/helper-annotate-as-pure@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" + integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-builder-binary-assignment-operator-visitor@^7.18.6": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz#acd4edfd7a566d1d51ea975dff38fd52906981bb" + integrity sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw== + dependencies: + "@babel/helper-explode-assignable-expression" "^7.18.6" + "@babel/types" "^7.18.9" + +"@babel/helper-compilation-targets@^7.17.7", "@babel/helper-compilation-targets@^7.18.9", "@babel/helper-compilation-targets@^7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz#6bf5374d424e1b3922822f1d9bdaa43b1a139d0a" + integrity sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ== + dependencies: + "@babel/compat-data" "^7.20.0" + "@babel/helper-validator-option" "^7.18.6" + browserslist "^4.21.3" + semver "^6.3.0" + +"@babel/helper-create-class-features-plugin@^7.18.6", "@babel/helper-create-class-features-plugin@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.5.tgz#327154eedfb12e977baa4ecc72e5806720a85a06" + integrity sha512-3RCdA/EmEaikrhayahwToF0fpweU/8o2p8vhc1c/1kftHOdTKuC65kik/TLc+qfbS8JKw4qqJbne4ovICDhmww== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-member-expression-to-functions" "^7.18.9" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-replace-supers" "^7.19.1" + "@babel/helper-split-export-declaration" "^7.18.6" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.20.5.tgz#5ea79b59962a09ec2acf20a963a01ab4d076ccca" + integrity sha512-m68B1lkg3XDGX5yCvGO0kPx3v9WIYLnzjKfPcQiwntEQa5ZeRkPmo2X/ISJc8qxWGfwUr+kvZAeEzAwLec2r2w== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + regexpu-core "^5.2.1" + +"@babel/helper-define-polyfill-provider@^0.3.3": + version "0.3.3" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz#8612e55be5d51f0cd1f36b4a5a83924e89884b7a" + integrity sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww== + dependencies: + "@babel/helper-compilation-targets" "^7.17.7" + "@babel/helper-plugin-utils" "^7.16.7" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + semver "^6.1.2" + +"@babel/helper-environment-visitor@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" + integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== + +"@babel/helper-explode-assignable-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz#41f8228ef0a6f1a036b8dfdfec7ce94f9a6bc096" + integrity sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-function-name@^7.18.9", "@babel/helper-function-name@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" + integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== + dependencies: + "@babel/template" "^7.18.10" + "@babel/types" "^7.19.0" + +"@babel/helper-hoist-variables@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" + integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-member-expression-to-functions@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz#1531661e8375af843ad37ac692c132841e2fd815" + integrity sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg== + dependencies: + "@babel/types" "^7.18.9" + +"@babel/helper-module-imports@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" + integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-module-transforms@^7.18.6", "@babel/helper-module-transforms@^7.19.6", "@babel/helper-module-transforms@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz#ac53da669501edd37e658602a21ba14c08748712" + integrity sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-simple-access" "^7.20.2" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/helper-validator-identifier" "^7.19.1" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.1" + "@babel/types" "^7.20.2" + +"@babel/helper-optimise-call-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" + integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.16.7", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.18.9", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" + integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== + +"@babel/helper-remap-async-to-generator@^7.18.6", "@babel/helper-remap-async-to-generator@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz#997458a0e3357080e54e1d79ec347f8a8cd28519" + integrity sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-wrap-function" "^7.18.9" + "@babel/types" "^7.18.9" + +"@babel/helper-replace-supers@^7.18.6", "@babel/helper-replace-supers@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz#e1592a9b4b368aa6bdb8784a711e0bcbf0612b78" + integrity sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-member-expression-to-functions" "^7.18.9" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/traverse" "^7.19.1" + "@babel/types" "^7.19.0" + +"@babel/helper-simple-access@^7.19.4", "@babel/helper-simple-access@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" + integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== + dependencies: + "@babel/types" "^7.20.2" + +"@babel/helper-skip-transparent-expression-wrappers@^7.18.9": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" + integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== + dependencies: + "@babel/types" "^7.20.0" + +"@babel/helper-split-export-declaration@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" + integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== + dependencies: + "@babel/types" "^7.18.6" + +"@babel/helper-string-parser@^7.19.4": + version "7.19.4" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" + integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== + +"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": + version "7.19.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" + integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== + +"@babel/helper-validator-option@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" + integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== + +"@babel/helper-wrap-function@^7.18.9": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.20.5.tgz#75e2d84d499a0ab3b31c33bcfe59d6b8a45f62e3" + integrity sha512-bYMxIWK5mh+TgXGVqAtnu5Yn1un+v8DDZtqyzKRLUzrh70Eal2O3aZ7aPYiMADO4uKlkzOiRiZ6GX5q3qxvW9Q== + dependencies: + "@babel/helper-function-name" "^7.19.0" + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" + +"@babel/helpers@^7.20.5": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.6.tgz#e64778046b70e04779dfbdf924e7ebb45992c763" + integrity sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w== + dependencies: + "@babel/template" "^7.18.10" + "@babel/traverse" "^7.20.5" + "@babel/types" "^7.20.5" + +"@babel/highlight@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" + integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== + dependencies: + "@babel/helper-validator-identifier" "^7.18.6" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/parser@^7.18.10", "@babel/parser@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.5.tgz#7f3c7335fe417665d929f34ae5dceae4c04015e8" + integrity sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA== + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2" + integrity sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz#a11af19aa373d68d561f08e0a57242350ed0ec50" + integrity sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + "@babel/plugin-proposal-optional-chaining" "^7.18.9" + +"@babel/plugin-proposal-async-generator-functions@^7.20.1": + version "7.20.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz#352f02baa5d69f4e7529bdac39aaa02d41146af9" + integrity sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g== + dependencies: + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-remap-async-to-generator" "^7.18.9" + "@babel/plugin-syntax-async-generators" "^7.8.4" + +"@babel/plugin-proposal-class-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" + integrity sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-class-static-block@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz#8aa81d403ab72d3962fc06c26e222dacfc9b9020" + integrity sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + +"@babel/plugin-proposal-dynamic-import@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz#72bcf8d408799f547d759298c3c27c7e7faa4d94" + integrity sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + +"@babel/plugin-proposal-export-namespace-from@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz#5f7313ab348cdb19d590145f9247540e94761203" + integrity sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + +"@babel/plugin-proposal-json-strings@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz#7e8788c1811c393aff762817e7dbf1ebd0c05f0b" + integrity sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-json-strings" "^7.8.3" + +"@babel/plugin-proposal-logical-assignment-operators@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz#8148cbb350483bf6220af06fa6db3690e14b2e23" + integrity sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + +"@babel/plugin-proposal-nullish-coalescing-operator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz#fdd940a99a740e577d6c753ab6fbb43fdb9467e1" + integrity sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + +"@babel/plugin-proposal-numeric-separator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz#899b14fbafe87f053d2c5ff05b36029c62e13c75" + integrity sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + +"@babel/plugin-proposal-object-rest-spread@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz#a556f59d555f06961df1e572bb5eca864c84022d" + integrity sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ== + dependencies: + "@babel/compat-data" "^7.20.1" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-transform-parameters" "^7.20.1" + +"@babel/plugin-proposal-optional-catch-binding@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz#f9400d0e6a3ea93ba9ef70b09e72dd6da638a2cb" + integrity sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + +"@babel/plugin-proposal-optional-chaining@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz#e8e8fe0723f2563960e4bf5e9690933691915993" + integrity sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + +"@babel/plugin-proposal-private-methods@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz#5209de7d213457548a98436fa2882f52f4be6bea" + integrity sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-proposal-private-property-in-object@^7.18.6": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.20.5.tgz#309c7668f2263f1c711aa399b5a9a6291eef6135" + integrity sha512-Vq7b9dUA12ByzB4EjQTPo25sFhY+08pQDBSZRtUAkj7lb7jahaHR5igera16QZ+3my1nYR4dKsNdYj5IjPHilQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-create-class-features-plugin" "^7.20.5" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + +"@babel/plugin-proposal-unicode-property-regex@^7.18.6", "@babel/plugin-proposal-unicode-property-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz#af613d2cd5e643643b65cded64207b15c85cb78e" + integrity sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-async-generators@^7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" + integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-class-properties@^7.12.13": + version "7.12.13" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" + integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== + dependencies: + "@babel/helper-plugin-utils" "^7.12.13" + +"@babel/plugin-syntax-class-static-block@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz#195df89b146b4b78b3bf897fd7a257c84659d406" + integrity sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-dynamic-import@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" + integrity sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-export-namespace-from@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz#028964a9ba80dbc094c915c487ad7c4e7a66465a" + integrity sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-import-assertions@^7.20.0": + version "7.20.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz#bb50e0d4bea0957235390641209394e87bdb9cc4" + integrity sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-syntax-json-strings@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" + integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-jsx@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" + integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-syntax-logical-assignment-operators@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" + integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" + integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-numeric-separator@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" + integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + +"@babel/plugin-syntax-object-rest-spread@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" + integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-catch-binding@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" + integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-optional-chaining@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" + integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.0" + +"@babel/plugin-syntax-private-property-in-object@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz#0dc6671ec0ea22b6e94a1114f857970cd39de1ad" + integrity sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-syntax-top-level-await@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" + integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== + dependencies: + "@babel/helper-plugin-utils" "^7.14.5" + +"@babel/plugin-transform-arrow-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz#19063fcf8771ec7b31d742339dac62433d0611fe" + integrity sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-async-to-generator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz#ccda3d1ab9d5ced5265fdb13f1882d5476c71615" + integrity sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag== + dependencies: + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-remap-async-to-generator" "^7.18.6" + +"@babel/plugin-transform-block-scoped-functions@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz#9187bf4ba302635b9d70d986ad70f038726216a8" + integrity sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-block-scoping@^7.20.2": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.5.tgz#401215f9dc13dc5262940e2e527c9536b3d7f237" + integrity sha512-WvpEIW9Cbj9ApF3yJCjIEEf1EiNJLtXagOrL5LNWEZOo3jv8pmPoYTSNJQvqej8OavVlgOoOPw6/htGZro6IkA== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-classes@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.2.tgz#c0033cf1916ccf78202d04be4281d161f6709bb2" + integrity sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-optimise-call-expression" "^7.18.6" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-replace-supers" "^7.19.1" + "@babel/helper-split-export-declaration" "^7.18.6" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz#2357a8224d402dad623caf6259b611e56aec746e" + integrity sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-destructuring@^7.20.2": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz#c23741cfa44ddd35f5e53896e88c75331b8b2792" + integrity sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-dotall-regex@^7.18.6", "@babel/plugin-transform-dotall-regex@^7.4.4": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz#b286b3e7aae6c7b861e45bed0a2fafd6b1a4fef8" + integrity sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-duplicate-keys@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz#687f15ee3cdad6d85191eb2a372c4528eaa0ae0e" + integrity sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-exponentiation-operator@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz#421c705f4521888c65e91fdd1af951bfefd4dacd" + integrity sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw== + dependencies: + "@babel/helper-builder-binary-assignment-operator-visitor" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-for-of@^7.18.8": + version "7.18.8" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz#6ef8a50b244eb6a0bdbad0c7c61877e4e30097c1" + integrity sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-function-name@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz#cc354f8234e62968946c61a46d6365440fc764e0" + integrity sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ== + dependencies: + "@babel/helper-compilation-targets" "^7.18.9" + "@babel/helper-function-name" "^7.18.9" + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz#72796fdbef80e56fba3c6a699d54f0de557444bc" + integrity sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-member-expression-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz#ac9fdc1a118620ac49b7e7a5d2dc177a1bfee88e" + integrity sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-modules-amd@^7.19.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz#aca391801ae55d19c4d8d2ebfeaa33df5f2a2cbd" + integrity sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg== + dependencies: + "@babel/helper-module-transforms" "^7.19.6" + "@babel/helper-plugin-utils" "^7.19.0" + +"@babel/plugin-transform-modules-commonjs@^7.19.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz#25b32feef24df8038fc1ec56038917eacb0b730c" + integrity sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ== + dependencies: + "@babel/helper-module-transforms" "^7.19.6" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-simple-access" "^7.19.4" + +"@babel/plugin-transform-modules-systemjs@^7.19.6": + version "7.19.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz#59e2a84064b5736a4471b1aa7b13d4431d327e0d" + integrity sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ== + dependencies: + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-module-transforms" "^7.19.6" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-validator-identifier" "^7.19.1" + +"@babel/plugin-transform-modules-umd@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz#81d3832d6034b75b54e62821ba58f28ed0aab4b9" + integrity sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ== + dependencies: + "@babel/helper-module-transforms" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.19.1": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.20.5.tgz#626298dd62ea51d452c3be58b285d23195ba69a8" + integrity sha512-mOW4tTzi5iTLnw+78iEq3gr8Aoq4WNRGpmSlrogqaiCBoR1HFhpU4JkpQFOHfeYx3ReVIFWOQJS4aZBRvuZ6mA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.20.5" + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-new-target@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz#d128f376ae200477f37c4ddfcc722a8a1b3246a8" + integrity sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-object-super@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz#fb3c6ccdd15939b6ff7939944b51971ddc35912c" + integrity sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-replace-supers" "^7.18.6" + +"@babel/plugin-transform-parameters@^7.20.1": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.5.tgz#f8f9186c681d10c3de7620c916156d893c8a019e" + integrity sha512-h7plkOmcndIUWXZFLgpbrh2+fXAi47zcUX7IrOQuZdLD0I0KvjJ6cvo3BEcAOsDOcZhVKGJqv07mkSqK0y2isQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + +"@babel/plugin-transform-property-literals@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz#e22498903a483448e94e032e9bbb9c5ccbfc93a3" + integrity sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-react-display-name@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.18.6.tgz#8b1125f919ef36ebdfff061d664e266c666b9415" + integrity sha512-TV4sQ+T013n61uMoygyMRm+xf04Bd5oqFpv2jAEQwSZ8NwQA7zeRPg1LMVg2PWi3zWBz+CLKD+v5bcpZ/BS0aA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-react-jsx-development@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.18.6.tgz#dbe5c972811e49c7405b630e4d0d2e1380c0ddc5" + integrity sha512-SA6HEjwYFKF7WDjWcMcMGUimmw/nhNRDWxr+KaLSCrkD/LMDBvWRmHAYgE1HDeF8KUuI8OAu+RT6EOtKxSW2qA== + dependencies: + "@babel/plugin-transform-react-jsx" "^7.18.6" + +"@babel/plugin-transform-react-jsx@^7.18.6": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.19.0.tgz#b3cbb7c3a00b92ec8ae1027910e331ba5c500eb9" + integrity sha512-UVEvX3tXie3Szm3emi1+G63jyw1w5IcMY0FSKM+CRnKRI5Mr1YbCNgsSTwoTwKphQEG9P+QqmuRFneJPZuHNhg== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-module-imports" "^7.18.6" + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/plugin-syntax-jsx" "^7.18.6" + "@babel/types" "^7.19.0" + +"@babel/plugin-transform-react-pure-annotations@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.18.6.tgz#561af267f19f3e5d59291f9950fd7b9663d0d844" + integrity sha512-I8VfEPg9r2TRDdvnHgPepTKvuRomzA8+u+nhY7qSI1fR2hRNebasZEETLyM5mAUr0Ku56OkXJ0I7NHJnO6cJiQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-regenerator@^7.18.6": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.20.5.tgz#57cda588c7ffb7f4f8483cc83bdcea02a907f04d" + integrity sha512-kW/oO7HPBtntbsahzQ0qSE3tFvkFwnbozz3NWFhLGqH75vLEg+sCGngLlhVkePlCs3Jv0dBBHDzCHxNiFAQKCQ== + dependencies: + "@babel/helper-plugin-utils" "^7.20.2" + regenerator-transform "^0.15.1" + +"@babel/plugin-transform-reserved-words@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz#b1abd8ebf8edaa5f7fe6bbb8d2133d23b6a6f76a" + integrity sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-shorthand-properties@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz#6d6df7983d67b195289be24909e3f12a8f664dc9" + integrity sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-spread@^7.19.0": + version "7.19.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz#dd60b4620c2fec806d60cfaae364ec2188d593b6" + integrity sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w== + dependencies: + "@babel/helper-plugin-utils" "^7.19.0" + "@babel/helper-skip-transparent-expression-wrappers" "^7.18.9" + +"@babel/plugin-transform-sticky-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz#c6706eb2b1524028e317720339583ad0f444adcc" + integrity sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-template-literals@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz#04ec6f10acdaa81846689d63fae117dd9c243a5e" + integrity sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-typeof-symbol@^7.18.9": + version "7.18.9" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz#c8cea68263e45addcd6afc9091429f80925762c0" + integrity sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-unicode-escapes@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz#1ecfb0eda83d09bbcb77c09970c2dd55832aa246" + integrity sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ== + dependencies: + "@babel/helper-plugin-utils" "^7.18.9" + +"@babel/plugin-transform-unicode-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz#194317225d8c201bbae103364ffe9e2cea36cdca" + integrity sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/preset-env@^7.19": + version "7.20.2" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.20.2.tgz#9b1642aa47bb9f43a86f9630011780dab7f86506" + integrity sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg== + dependencies: + "@babel/compat-data" "^7.20.1" + "@babel/helper-compilation-targets" "^7.20.0" + "@babel/helper-plugin-utils" "^7.20.2" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.18.6" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-async-generator-functions" "^7.20.1" + "@babel/plugin-proposal-class-properties" "^7.18.6" + "@babel/plugin-proposal-class-static-block" "^7.18.6" + "@babel/plugin-proposal-dynamic-import" "^7.18.6" + "@babel/plugin-proposal-export-namespace-from" "^7.18.9" + "@babel/plugin-proposal-json-strings" "^7.18.6" + "@babel/plugin-proposal-logical-assignment-operators" "^7.18.9" + "@babel/plugin-proposal-nullish-coalescing-operator" "^7.18.6" + "@babel/plugin-proposal-numeric-separator" "^7.18.6" + "@babel/plugin-proposal-object-rest-spread" "^7.20.2" + "@babel/plugin-proposal-optional-catch-binding" "^7.18.6" + "@babel/plugin-proposal-optional-chaining" "^7.18.9" + "@babel/plugin-proposal-private-methods" "^7.18.6" + "@babel/plugin-proposal-private-property-in-object" "^7.18.6" + "@babel/plugin-proposal-unicode-property-regex" "^7.18.6" + "@babel/plugin-syntax-async-generators" "^7.8.4" + "@babel/plugin-syntax-class-properties" "^7.12.13" + "@babel/plugin-syntax-class-static-block" "^7.14.5" + "@babel/plugin-syntax-dynamic-import" "^7.8.3" + "@babel/plugin-syntax-export-namespace-from" "^7.8.3" + "@babel/plugin-syntax-import-assertions" "^7.20.0" + "@babel/plugin-syntax-json-strings" "^7.8.3" + "@babel/plugin-syntax-logical-assignment-operators" "^7.10.4" + "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" + "@babel/plugin-syntax-numeric-separator" "^7.10.4" + "@babel/plugin-syntax-object-rest-spread" "^7.8.3" + "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" + "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-private-property-in-object" "^7.14.5" + "@babel/plugin-syntax-top-level-await" "^7.14.5" + "@babel/plugin-transform-arrow-functions" "^7.18.6" + "@babel/plugin-transform-async-to-generator" "^7.18.6" + "@babel/plugin-transform-block-scoped-functions" "^7.18.6" + "@babel/plugin-transform-block-scoping" "^7.20.2" + "@babel/plugin-transform-classes" "^7.20.2" + "@babel/plugin-transform-computed-properties" "^7.18.9" + "@babel/plugin-transform-destructuring" "^7.20.2" + "@babel/plugin-transform-dotall-regex" "^7.18.6" + "@babel/plugin-transform-duplicate-keys" "^7.18.9" + "@babel/plugin-transform-exponentiation-operator" "^7.18.6" + "@babel/plugin-transform-for-of" "^7.18.8" + "@babel/plugin-transform-function-name" "^7.18.9" + "@babel/plugin-transform-literals" "^7.18.9" + "@babel/plugin-transform-member-expression-literals" "^7.18.6" + "@babel/plugin-transform-modules-amd" "^7.19.6" + "@babel/plugin-transform-modules-commonjs" "^7.19.6" + "@babel/plugin-transform-modules-systemjs" "^7.19.6" + "@babel/plugin-transform-modules-umd" "^7.18.6" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.19.1" + "@babel/plugin-transform-new-target" "^7.18.6" + "@babel/plugin-transform-object-super" "^7.18.6" + "@babel/plugin-transform-parameters" "^7.20.1" + "@babel/plugin-transform-property-literals" "^7.18.6" + "@babel/plugin-transform-regenerator" "^7.18.6" + "@babel/plugin-transform-reserved-words" "^7.18.6" + "@babel/plugin-transform-shorthand-properties" "^7.18.6" + "@babel/plugin-transform-spread" "^7.19.0" + "@babel/plugin-transform-sticky-regex" "^7.18.6" + "@babel/plugin-transform-template-literals" "^7.18.9" + "@babel/plugin-transform-typeof-symbol" "^7.18.9" + "@babel/plugin-transform-unicode-escapes" "^7.18.10" + "@babel/plugin-transform-unicode-regex" "^7.18.6" + "@babel/preset-modules" "^0.1.5" + "@babel/types" "^7.20.2" + babel-plugin-polyfill-corejs2 "^0.3.3" + babel-plugin-polyfill-corejs3 "^0.6.0" + babel-plugin-polyfill-regenerator "^0.4.1" + core-js-compat "^3.25.1" + semver "^6.3.0" + +"@babel/preset-modules@^0.1.5": + version "0.1.5" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.5.tgz#ef939d6e7f268827e1841638dc6ff95515e115d9" + integrity sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/plugin-proposal-unicode-property-regex" "^7.4.4" + "@babel/plugin-transform-dotall-regex" "^7.4.4" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/preset-react@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.18.6.tgz#979f76d6277048dc19094c217b507f3ad517dd2d" + integrity sha512-zXr6atUmyYdiWRVLOZahakYmOBHtWc2WGCkP8PYTgZi0iJXDY2CN180TdrIW4OGOAdLc7TifzDIvtx6izaRIzg== + dependencies: + "@babel/helper-plugin-utils" "^7.18.6" + "@babel/helper-validator-option" "^7.18.6" + "@babel/plugin-transform-react-display-name" "^7.18.6" + "@babel/plugin-transform-react-jsx" "^7.18.6" + "@babel/plugin-transform-react-jsx-development" "^7.18.6" + "@babel/plugin-transform-react-pure-annotations" "^7.18.6" + +"@babel/runtime@^7.8.4": + version "7.20.6" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.6.tgz#facf4879bfed9b5326326273a64220f099b0fce3" + integrity sha512-Q+8MqP7TiHMWzSfwiJwXCjyf4GYA4Dgw3emg/7xmwsdLJOZUp+nMqcOwOzzYheuM1rhDu8FSj2l0aoMygEuXuA== + dependencies: + regenerator-runtime "^0.13.11" + +"@babel/template@^7.18.10": + version "7.18.10" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.18.10.tgz#6f9134835970d1dbf0835c0d100c9f38de0c5e71" + integrity sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/parser" "^7.18.10" + "@babel/types" "^7.18.10" + +"@babel/traverse@^7.19.1", "@babel/traverse@^7.20.1", "@babel/traverse@^7.20.5": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.5.tgz#78eb244bea8270fdda1ef9af22a5d5e5b7e57133" + integrity sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ== + dependencies: + "@babel/code-frame" "^7.18.6" + "@babel/generator" "^7.20.5" + "@babel/helper-environment-visitor" "^7.18.9" + "@babel/helper-function-name" "^7.19.0" + "@babel/helper-hoist-variables" "^7.18.6" + "@babel/helper-split-export-declaration" "^7.18.6" + "@babel/parser" "^7.20.5" + "@babel/types" "^7.20.5" + debug "^4.1.0" + globals "^11.1.0" + +"@babel/types@^7.18.10", "@babel/types@^7.18.6", "@babel/types@^7.18.9", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.5", "@babel/types@^7.4.4": + version "7.20.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.5.tgz#e206ae370b5393d94dfd1d04cd687cace53efa84" + integrity sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg== + dependencies: + "@babel/helper-string-parser" "^7.19.4" + "@babel/helper-validator-identifier" "^7.19.1" + to-fast-properties "^2.0.0" + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.1.0": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" + integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== + dependencies: + "@jridgewell/set-array" "^1.0.0" + "@jridgewell/sourcemap-codec" "^1.4.10" + +"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" + integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== + dependencies: + "@jridgewell/set-array" "^1.0.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/resolve-uri@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" + integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== + +"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" + integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== + +"@jridgewell/source-map@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" + integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.0" + "@jridgewell/trace-mapping" "^0.3.9" + +"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10": + version "1.4.14" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" + integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== + +"@jridgewell/trace-mapping@^0.3.14", "@jridgewell/trace-mapping@^0.3.9": + version "0.3.17" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" + integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== + dependencies: + "@jridgewell/resolve-uri" "3.1.0" + "@jridgewell/sourcemap-codec" "1.4.14" + +"@types/eslint-scope@^3.7.3": + version "3.7.4" + resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16" + integrity sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA== + dependencies: + "@types/eslint" "*" + "@types/estree" "*" + +"@types/eslint@*": + version "8.4.10" + resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.4.10.tgz#19731b9685c19ed1552da7052b6f668ed7eb64bb" + integrity sha512-Sl/HOqN8NKPmhWo2VBEPm0nvHnu2LL3v9vKo8MEq0EtbJ4eVzGPl41VNPvn5E1i5poMk4/XD8UriLHpJvEP/Nw== + dependencies: + "@types/estree" "*" + "@types/json-schema" "*" + +"@types/estree@*": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" + integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== + +"@types/estree@^0.0.51": + version "0.0.51" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.51.tgz#cfd70924a25a3fd32b218e5e420e6897e1ac4f40" + integrity sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ== + +"@types/json-schema@*", "@types/json-schema@^7.0.5", "@types/json-schema@^7.0.8": + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3" + integrity sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ== + +"@types/node@*": + version "18.11.11" + resolved "https://registry.yarnpkg.com/@types/node/-/node-18.11.11.tgz#1d455ac0211549a8409d3cdb371cd55cc971e8dc" + integrity sha512-KJ021B1nlQUBLopzZmPBVuGU9un7WJd/W4ya7Ih02B4Uwky5Nja0yGYav2EfYIk0RR2Q9oVhf60S2XR1BCWJ2g== + +"@webassemblyjs/ast@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.1.tgz#2bfd767eae1a6996f432ff7e8d7fc75679c0b6a7" + integrity sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + +"@webassemblyjs/floating-point-hex-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz#f6c61a705f0fd7a6aecaa4e8198f23d9dc179e4f" + integrity sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ== + +"@webassemblyjs/helper-api-error@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz#1a63192d8788e5c012800ba6a7a46c705288fd16" + integrity sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg== + +"@webassemblyjs/helper-buffer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz#832a900eb444884cde9a7cad467f81500f5e5ab5" + integrity sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA== + +"@webassemblyjs/helper-numbers@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz#64d81da219fbbba1e3bd1bfc74f6e8c4e10a62ae" + integrity sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz#f328241e41e7b199d0b20c18e88429c4433295e1" + integrity sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q== + +"@webassemblyjs/helper-wasm-section@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz#21ee065a7b635f319e738f0dd73bfbda281c097a" + integrity sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + +"@webassemblyjs/ieee754@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz#963929e9bbd05709e7e12243a099180812992614" + integrity sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.1.tgz#ce814b45574e93d76bae1fb2644ab9cdd9527aa5" + integrity sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.1.tgz#d1f8b764369e7c6e6bae350e854dec9a59f0a3ff" + integrity sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ== + +"@webassemblyjs/wasm-edit@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz#ad206ebf4bf95a058ce9880a8c092c5dec8193d6" + integrity sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/helper-wasm-section" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-opt" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + "@webassemblyjs/wast-printer" "1.11.1" + +"@webassemblyjs/wasm-gen@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz#86c5ea304849759b7d88c47a32f4f039ae3c8f76" + integrity sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wasm-opt@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz#657b4c2202f4cf3b345f8a4c6461c8c2418985f2" + integrity sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-buffer" "1.11.1" + "@webassemblyjs/wasm-gen" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + +"@webassemblyjs/wasm-parser@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz#86ca734534f417e9bd3c67c7a1c75d8be41fb199" + integrity sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/helper-api-error" "1.11.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.1" + "@webassemblyjs/ieee754" "1.11.1" + "@webassemblyjs/leb128" "1.11.1" + "@webassemblyjs/utf8" "1.11.1" + +"@webassemblyjs/wast-printer@1.11.1": + version "1.11.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz#d0c73beda8eec5426f10ae8ef55cee5e7084c2f0" + integrity sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg== + dependencies: + "@webassemblyjs/ast" "1.11.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" + integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== + +"@webpack-cli/info@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" + integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== + dependencies: + envinfo "^7.7.3" + +"@webpack-cli/serve@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" + integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== + +"@woocommerce/dependency-extraction-webpack-plugin@^2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@woocommerce/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-2.2.0.tgz#230d674a67585bc32e31bc28485bec99b41dbd1f" + integrity sha512-0wDY3EIUwWrPm0KrWvt1cf2SZDSX7CzBXvv4TyCqWOPuVPvC/ajyY8kD1HTFI80q6/RHoxWf3BYCmhuBzPbe9A== + dependencies: + "@wordpress/dependency-extraction-webpack-plugin" "^3.3.0" + +"@wordpress/dependency-extraction-webpack-plugin@^3.3.0": + version "3.7.0" + resolved "https://registry.yarnpkg.com/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-3.7.0.tgz#e52ef31f66b8c4add3d773a87e11007375127b04" + integrity sha512-SHyp88D1ICSaRVMfs/kKEicjKXWf1y2wecUeZIiMtkfAi8Bnk3JsnUo11LH7drJIXfjmDoer2B2rrBMZmRm8VA== + dependencies: + json2php "^0.0.4" + webpack-sources "^3.2.2" + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +acorn-import-assertions@^1.7.6: + version "1.8.0" + resolved "https://registry.yarnpkg.com/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz#ba2b5939ce62c238db6d93d81c9b111b29b855e9" + integrity sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw== + +acorn@^8.5.0, acorn@^8.7.1: + version "8.8.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.1.tgz#0a3f9cbecc4ec3bea6f0a80b66ae8dd2da250b73" + integrity sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA== + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv@^6.12.4, ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +babel-loader@^8.2: + version "8.3.0" + resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.3.0.tgz#124936e841ba4fe8176786d6ff28add1f134d6a8" + integrity sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q== + dependencies: + find-cache-dir "^3.3.1" + loader-utils "^2.0.0" + make-dir "^3.1.0" + schema-utils "^2.6.5" + +babel-plugin-polyfill-corejs2@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz#5d1bd3836d0a19e1b84bbf2d9640ccb6f951c122" + integrity sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q== + dependencies: + "@babel/compat-data" "^7.17.7" + "@babel/helper-define-polyfill-provider" "^0.3.3" + semver "^6.1.1" + +babel-plugin-polyfill-corejs3@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz#56ad88237137eade485a71b52f72dbed57c6230a" + integrity sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + core-js-compat "^3.25.1" + +babel-plugin-polyfill-regenerator@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz#390f91c38d90473592ed43351e801a9d3e0fd747" + integrity sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.3.3" + +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.14.5, browserslist@^4.21.3, browserslist@^4.21.4: + version "4.21.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.4.tgz#e7496bbc67b9e39dd0f98565feccdcb0d4ff6987" + integrity sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw== + dependencies: + caniuse-lite "^1.0.30001400" + electron-to-chromium "^1.4.251" + node-releases "^2.0.6" + update-browserslist-db "^1.0.9" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +caniuse-lite@^1.0.30001400: + version "1.0.30001436" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001436.tgz#22d7cbdbbbb60cdc4ca1030ccd6dea9f5de4848b" + integrity sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg== + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +"chokidar@>=3.0.0 <4.0.0": + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz#1015eced4741e15d06664a957dbbf50d041e26ac" + integrity sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg== + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== + +colorette@^2.0.14: + version "2.0.19" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" + integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +commondir@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" + integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== + +convert-source-map@^1.7.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" + integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== + +core-js-compat@^3.25.1: + version "3.26.1" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.26.1.tgz#0e710b09ebf689d719545ac36e49041850f943df" + integrity sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A== + dependencies: + browserslist "^4.21.4" + +core-js@^3.25.0: + version "3.26.1" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.26.1.tgz#7a9816dabd9ee846c1c0fe0e8fcad68f3709134e" + integrity sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA== + +cross-env@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" + integrity sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw== + dependencies: + cross-spawn "^7.0.1" + +cross-spawn@^7.0.1, cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@^4.1.0, debug@^4.1.1: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +electron-to-chromium@^1.4.251: + version "1.4.284" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" + integrity sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA== + +emojis-list@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-3.0.0.tgz#5570662046ad29e2e916e71aae260abdff4f6a78" + integrity sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q== + +enhanced-resolve@^5.10.0: + version "5.12.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" + integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +envinfo@^7.7.3: + version "7.8.1" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475" + integrity sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw== + +es-module-lexer@^0.9.0: + version "0.9.3" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-0.9.3.tgz#6f13db00cc38417137daf74366f535c8eb438f19" + integrity sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ== + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +fast-deep-equal@^3.1.1: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-cache-dir@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.2.tgz#b30c5b6eff0730731aea9bbd9dbecbd80256d64b" + integrity sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig== + dependencies: + commondir "^1.0.1" + make-dir "^3.0.2" + pkg-dir "^4.1.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +graceful-fs@^4.1.2, graceful-fs@^4.2.4, graceful-fs@^4.2.9: + version "4.2.10" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" + integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +immutable@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.1.0.tgz#f795787f0db780183307b9eb2091fcac1f6fafef" + integrity sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ== + +import-local@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" + integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.9.0: + version "2.11.0" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" + integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== + dependencies: + has "^1.0.3" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsesc@^2.5.1: + version "2.5.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" + integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + integrity sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA== + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json2php@^0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/json2php/-/json2php-0.0.4.tgz#6bd85a1dda6a5dd7e91022bb24403cc1b7c2ee34" + integrity sha512-hFzejhs28f70sGnutcsRS459MnAsjRVI85RgPAL1KQIZEpjiDitc27CZv4IgOtaR86vrqOVlu9vJNew2XyTH4g== + +json5@^2.1.2, json5@^2.2.1: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +klona@^2.0.4: + version "2.0.5" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" + integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +loader-utils@^2.0.0: + version "2.0.4" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-2.0.4.tgz#8b5cb38b5c34a9a018ee1fc0e6a066d1dfcc528c" + integrity sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw== + dependencies: + big.js "^5.2.2" + emojis-list "^3.0.0" + json5 "^2.1.2" + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +make-dir@^3.0.2, make-dir@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== + dependencies: + semver "^6.0.0" + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.27: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-releases@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.6.tgz#8a7088c63a55e493845683ebf3c828d8c51c5503" + integrity sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.1.0, pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + +regenerate-unicode-properties@^10.1.0: + version "10.1.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz#7c3192cab6dd24e21cb4461e5ddd7dd24fa8374c" + integrity sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regenerator-runtime@^0.13.11: + version "0.13.11" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" + integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== + +regenerator-transform@^0.15.1: + version "0.15.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.1.tgz#f6c4e99fc1b4591f780db2586328e4d9a9d8dc56" + integrity sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg== + dependencies: + "@babel/runtime" "^7.8.4" + +regexpu-core@^5.2.1: + version "5.2.2" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-5.2.2.tgz#3e4e5d12103b64748711c3aad69934d7718e75fc" + integrity sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.1.0" + regjsgen "^0.7.1" + regjsparser "^0.9.1" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsgen@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.7.1.tgz#ee5ef30e18d3f09b7c369b76e7c2373ed25546f6" + integrity sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA== + +regjsparser@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.9.1.tgz#272d05aa10c7c1f67095b1ff0addae8442fc5709" + integrity sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ== + dependencies: + jsesc "~0.5.0" + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.14.2, resolve@^1.9.0: + version "1.22.1" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" + integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== + dependencies: + is-core-module "^2.9.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +safe-buffer@^5.1.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +sass-loader@^12.1.0: + version "12.6.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-12.6.0.tgz#5148362c8e2cdd4b950f3c63ac5d16dbfed37bcb" + integrity sha512-oLTaH0YCtX4cfnJZxKSLAyglED0naiYfNG1iXfU5w1LNZ+ukoA5DtyDIN5zmKVZwYNJP4KRc5Y3hkWga+7tYfA== + dependencies: + klona "^2.0.4" + neo-async "^2.6.2" + +sass@^1.42.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.56.1.tgz#94d3910cd468fd075fa87f5bb17437a0b617d8a7" + integrity sha512-VpEyKpyBPCxE7qGDtOcdJ6fFbcpOM+Emu7uZLxVrkX8KVU/Dp5UF7WLvzqRuUhB6mqqQt1xffLoG+AndxTZrCQ== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +schema-utils@^2.6.5: + version "2.7.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.1.tgz#1ca4f32d1b24c590c203b8e7a50bf0ea4cd394d7" + integrity sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg== + dependencies: + "@types/json-schema" "^7.0.5" + ajv "^6.12.4" + ajv-keywords "^3.5.2" + +schema-utils@^3.0.0, schema-utils@^3.1.0, schema-utils@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.1.1.tgz#bc74c4b6b6995c1d88f76a8b77bea7219e0c8281" + integrity sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +serialize-javascript@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.0.tgz#efae5d88f45d7924141da8b5c3a7a7e663fefeb8" + integrity sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag== + dependencies: + randombytes "^2.1.0" + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +"source-map-js@>=0.6.2 <2.0.0": + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.1.3: + version "5.3.6" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.6.tgz#5590aec31aa3c6f771ce1b1acca60639eab3195c" + integrity sha512-kfLFk+PoLUQIbLmB1+PZDMRSZS99Mp+/MHqDNmMA6tOItzRt+Npe3E+fsMs5mfcM0wCtrrdU387UnV+vnSffXQ== + dependencies: + "@jridgewell/trace-mapping" "^0.3.14" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.0" + terser "^5.14.1" + +terser@^5.14.1: + version "5.16.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.1.tgz#5af3bc3d0f24241c7fb2024199d5c461a1075880" + integrity sha512-xvQfyfA1ayT0qdK47zskQgRZeWLoOQ8JQ6mIgRGVNwZKdQMU+5FkCBjmv4QjcrTzyZquRw2FVtlJSRUmMKQslw== + dependencies: + "@jridgewell/source-map" "^0.3.2" + acorn "^8.5.0" + commander "^2.20.0" + source-map-support "~0.5.20" + +to-fast-properties@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" + integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz#301acdc525631670d39f6146e0e77ff6bbdebddc" + integrity sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz#cb5fffdcd16a05124f5a4b0bf7c3770208acbbe0" + integrity sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +update-browserslist-db@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" + integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +watchpack@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d" + integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +webpack-cli@^4.10: + version "4.10.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" + integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.2.0" + "@webpack-cli/info" "^1.5.0" + "@webpack-cli/serve" "^1.7.0" + colorette "^2.0.14" + commander "^7.0.0" + cross-spawn "^7.0.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + webpack-merge "^5.7.3" + +webpack-merge@^5.7.3: + version "5.8.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.8.0.tgz#2b39dbf22af87776ad744c390223731d30a68f61" + integrity sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q== + dependencies: + clone-deep "^4.0.1" + wildcard "^2.0.0" + +webpack-sources@^3.2.2, webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.76: + version "5.76.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.76.0.tgz#f9fb9fb8c4a7dbdcd0d56a98e56b8a942ee2692c" + integrity sha512-l5sOdYBDunyf72HW8dF23rFtWq/7Zgvt/9ftMof71E/yUb1YLOBmTgA2K4vQthB3kotMrSj609txVE0dnr2fjA== + dependencies: + "@types/eslint-scope" "^3.7.3" + "@types/estree" "^0.0.51" + "@webassemblyjs/ast" "1.11.1" + "@webassemblyjs/wasm-edit" "1.11.1" + "@webassemblyjs/wasm-parser" "1.11.1" + acorn "^8.7.1" + acorn-import-assertions "^1.7.6" + browserslist "^4.14.5" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.10.0" + es-module-lexer "^0.9.0" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.9" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.1.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.1.3" + watchpack "^2.4.0" + webpack-sources "^3.2.3" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.0.tgz#a77d20e5200c6faaac979e4b3aadc7b3dd7f8fec" + integrity sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw== diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CartActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CartActionHandler.js index 00966ec6c..4b7b20e9e 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CartActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CartActionHandler.js @@ -9,6 +9,39 @@ class CartActionHandler { this.errorHandler = errorHandler; } + subscriptionsConfiguration() { + return { + createSubscription: (data, actions) => { + return actions.subscription.create({ + 'plan_id': this.config.subscription_plan_id + }); + }, + onApprove: (data, actions) => { + fetch(this.config.ajax.approve_subscription.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify({ + nonce: this.config.ajax.approve_subscription.nonce, + order_id: data.orderID, + subscription_id: data.subscriptionID + }) + }).then((res)=>{ + return res.json(); + }).then((data) => { + if (!data.success) { + console.log(data) + throw Error(data.data.message); + } + + location.href = this.config.redirect; + }); + }, + onError: (err) => { + console.error(err); + } + } + } + configuration() { const createOrder = (data, actions) => { const payer = payerData(); diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js index b45b9f8b9..f0654c4f0 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/CheckoutActionHandler.js @@ -11,6 +11,34 @@ class CheckoutActionHandler { this.spinner = spinner; } + subscriptionsConfiguration() { + return { + createSubscription: (data, actions) => { + return actions.subscription.create({ + 'plan_id': this.config.subscription_plan_id + }); + }, + onApprove: (data, actions) => { + fetch(this.config.ajax.approve_subscription.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify({ + nonce: this.config.ajax.approve_subscription.nonce, + order_id: data.orderID, + subscription_id: data.subscriptionID + }) + }).then((res)=>{ + return res.json(); + }).then((data) => { + document.querySelector('#place_order').click(); + }); + }, + onError: (err) => { + console.error(err); + } + } + } + configuration() { const spinner = this.spinner; const createOrder = (data, actions) => { @@ -81,7 +109,7 @@ class CheckoutActionHandler { const input = document.createElement('input'); input.setAttribute('type', 'hidden'); input.setAttribute('name', 'ppcp-resume-order'); - input.setAttribute('value', data.data.purchase_units[0].custom_id); + input.setAttribute('value', data.data.custom_id); document.querySelector(formSelector).appendChild(input); return data.data.id; }); diff --git a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js index f1456a5b0..3c3371542 100644 --- a/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js +++ b/modules/ppcp-button/resources/js/modules/ActionHandler/SingleProductActionHandler.js @@ -18,6 +18,56 @@ class SingleProductActionHandler { this.errorHandler = errorHandler; } + subscriptionsConfiguration() { + return { + createSubscription: (data, actions) => { + return actions.subscription.create({ + 'plan_id': this.config.subscription_plan_id + }); + }, + onApprove: (data, actions) => { + fetch(this.config.ajax.approve_subscription.endpoint, { + method: 'POST', + credentials: 'same-origin', + body: JSON.stringify({ + nonce: this.config.ajax.approve_subscription.nonce, + order_id: data.orderID, + subscription_id: data.subscriptionID + }) + }).then((res)=>{ + return res.json(); + }).then(() => { + const id = document.querySelector('[name="add-to-cart"]').value; + const products = [new Product(id, 1, null)]; + + fetch(this.config.ajax.change_cart.endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'same-origin', + body: JSON.stringify({ + nonce: this.config.ajax.change_cart.nonce, + products, + }) + }).then((result) => { + return result.json(); + }).then((result) => { + if (!result.success) { + console.log(result) + throw Error(result.data.message); + } + + location.href = this.config.redirect; + }) + }); + }, + onError: (err) => { + console.error(err); + } + } + } + configuration() { return { diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js index bd4242e07..72d28932b 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CartBootstap.js @@ -50,6 +50,14 @@ class CartBootstrap { this.errorHandler, ); + if( + PayPalCommerceGateway.data_client_id.has_subscriptions + && PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled + ) { + this.renderer.render(actionHandler.subscriptionsConfiguration()); + return; + } + this.renderer.render( actionHandler.configuration() ); diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js index af04b6811..59c6197a3 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/CheckoutBootstap.js @@ -64,9 +64,15 @@ class CheckoutBootstap { this.spinner ); - this.renderer.render( - actionHandler.configuration() - ); + if( + PayPalCommerceGateway.data_client_id.has_subscriptions + && PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled + ) { + this.renderer.render(actionHandler.subscriptionsConfiguration(), {}, actionHandler.configuration()); + return; + } + + this.renderer.render(actionHandler.configuration(), {}, actionHandler.configuration()); } updateUi() { diff --git a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js index a36c1d408..376962772 100644 --- a/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js +++ b/modules/ppcp-button/resources/js/modules/ContextBootstrap/SingleProductBootstap.js @@ -110,6 +110,14 @@ class SingleProductBootstap { this.errorHandler, ); + if( + PayPalCommerceGateway.data_client_id.has_subscriptions + && PayPalCommerceGateway.data_client_id.paypal_subscriptions_enabled + ) { + this.renderer.render(actionHandler.subscriptionsConfiguration()); + return; + } + this.renderer.render( actionHandler.configuration() ); diff --git a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js index d5d295f23..17f58aff3 100644 --- a/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js +++ b/modules/ppcp-button/resources/js/modules/Renderer/Renderer.js @@ -10,7 +10,7 @@ class Renderer { this.renderedSources = new Set(); } - render(contextConfig, settingsOverride = {}) { + render(contextConfig, settingsOverride = {}, contextConfigOverride = () => {}) { const settings = merge(this.defaultSettings, settingsOverride); const enabledSeparateGateways = Object.fromEntries(Object.entries( @@ -50,7 +50,7 @@ class Renderer { } if (this.creditCardRenderer) { - this.creditCardRenderer.render(settings.hosted_fields.wrapper, contextConfig); + this.creditCardRenderer.render(settings.hosted_fields.wrapper, contextConfigOverride); } for (const [fundingSource, data] of Object.entries(enabledSeparateGateways)) { diff --git a/modules/ppcp-button/services.php b/modules/ppcp-button/services.php index 53e01f193..7f8c50b14 100644 --- a/modules/ppcp-button/services.php +++ b/modules/ppcp-button/services.php @@ -9,11 +9,13 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button; +use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; use WooCommerce\PayPalCommerce\Button\Helper\CheckoutFormSaver; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Validation\CheckoutFormValidator; use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint; +use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\Button\Assets\DisabledSmartButton; use WooCommerce\PayPalCommerce\Button\Assets\SmartButton; @@ -65,25 +67,37 @@ return array( return $dummy_ids[ $shop_country ] ?? $container->get( 'button.client_id' ); }, - 'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface { + 'button.is_paypal_continuation' => static function( ContainerInterface $container ): bool { + $session_handler = $container->get( 'session.handler' ); + $order = $session_handler->order(); + if ( ! $order ) { + return false; + } + $source = $order->payment_source(); + if ( $source && $source->card() ) { + return false; // Ignore for DCC. + } + if ( 'card' === $session_handler->funding_source() ) { + return false; // Ignore for card buttons. + } + + return true; + }, + 'button.smart-button' => static function ( ContainerInterface $container ): SmartButtonInterface { $state = $container->get( 'onboarding.state' ); - /** - * The state. - * - * @var State $state - */ if ( $state->current_state() !== State::STATE_ONBOARDED ) { return new DisabledSmartButton(); } + $settings = $container->get( 'wcgateway.settings' ); $paypal_disabled = ! $settings->has( 'enabled' ) || ! $settings->get( 'enabled' ); if ( $paypal_disabled ) { return new DisabledSmartButton(); } + $payer_factory = $container->get( 'api.factory.payer' ); $request_data = $container->get( 'button.request-data' ); - $client_id = $container->get( 'button.client_id' ); $dcc_applies = $container->get( 'api.helpers.dccapplies' ); $subscription_helper = $container->get( 'subscription.helper' ); @@ -110,6 +124,7 @@ return array( $container->get( 'wcgateway.all-funding-sources' ), $container->get( 'button.basic-checkout-validation-enabled' ), $container->get( 'button.early-wc-checkout-validation-enabled' ), + $container->get( 'button.pay-now-contexts' ), $container->get( 'woocommerce.logger.woocommerce' ) ); }, @@ -119,6 +134,9 @@ return array( 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 { return new RequestData(); }, @@ -156,6 +174,8 @@ return array( $registration_needed, $container->get( 'wcgateway.settings.card_billing_data_mode' ), $container->get( 'button.early-wc-checkout-validation-enabled' ), + $container->get( 'button.pay-now-contexts' ), + $container->get( 'button.handle-shipping-in-paypal' ), $logger ); }, @@ -187,6 +207,13 @@ return array( $logger ); }, + 'button.endpoint.approve-subscription' => static function( ContainerInterface $container ): ApproveSubscriptionEndpoint { + return new ApproveSubscriptionEndpoint( + $container->get( 'button.request-data' ), + $container->get( 'api.endpoint.order' ), + $container->get( 'session.handler' ) + ); + }, 'button.checkout-form-saver' => static function ( ContainerInterface $container ): CheckoutFormSaver { return new CheckoutFormSaver(); }, @@ -266,4 +293,12 @@ return array( 'button.validation.wc-checkout-validator' => static function ( ContainerInterface $container ): 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; + }, ); diff --git a/modules/ppcp-button/src/Assets/DisabledSmartButton.php b/modules/ppcp-button/src/Assets/DisabledSmartButton.php index 04cee3eab..71889c7cc 100644 --- a/modules/ppcp-button/src/Assets/DisabledSmartButton.php +++ b/modules/ppcp-button/src/Assets/DisabledSmartButton.php @@ -26,7 +26,7 @@ class DisabledSmartButton implements SmartButtonInterface { /** * Whether the scripts should be loaded. */ - public function should_load(): bool { + public function should_load_ppcp_script(): bool { return false; } diff --git a/modules/ppcp-button/src/Assets/SmartButton.php b/modules/ppcp-button/src/Assets/SmartButton.php index 1aae5e3fa..f0a5a6e47 100644 --- a/modules/ppcp-button/src/Assets/SmartButton.php +++ b/modules/ppcp-button/src/Assets/SmartButton.php @@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory; use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies; use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint; +use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint; @@ -68,13 +69,6 @@ class SmartButton implements SmartButtonInterface { */ private $version; - /** - * The Session Handler. - * - * @var SessionHandler - */ - private $session_handler; - /** * The settings. * @@ -166,13 +160,6 @@ class SmartButton implements SmartButtonInterface { */ protected $early_validation_enabled; - /** - * The logger. - * - * @var LoggerInterface - */ - private $logger; - /** * Cached payment tokens. * @@ -180,12 +167,33 @@ class SmartButton implements SmartButtonInterface { */ private $payment_tokens = null; + /** + * The contexts that should have the Pay Now button. + * + * @var string[] + */ + private $pay_now_contexts; + + /** + * The logger. + * + * @var LoggerInterface + */ + private $logger; + + /** + * Session handler. + * + * @var SessionHandler + */ + private $session_handler; + /** * SmartButton constructor. * * @param string $module_url The URL to the module. - * @param string $version The assets version. - * @param SessionHandler $session_handler The Session Handler. + * @param string $version The assets version. + * @param SessionHandler $session_handler The Session handler. * @param Settings $settings The Settings. * @param PayerFactory $payer_factory The Payer factory. * @param string $client_id The client ID. @@ -200,6 +208,7 @@ class SmartButton implements SmartButtonInterface { * @param array $all_funding_sources All existing funding sources. * @param bool $basic_checkout_validation_enabled Whether the basic JS validation of the form iss enabled. * @param bool $early_validation_enabled Whether to execute WC validation of the checkout form. + * @param array $pay_now_contexts The contexts that should have the Pay Now button. * @param LoggerInterface $logger The logger. */ public function __construct( @@ -220,6 +229,7 @@ class SmartButton implements SmartButtonInterface { array $all_funding_sources, bool $basic_checkout_validation_enabled, bool $early_validation_enabled, + array $pay_now_contexts, LoggerInterface $logger ) { @@ -240,6 +250,7 @@ class SmartButton implements SmartButtonInterface { $this->all_funding_sources = $all_funding_sources; $this->basic_checkout_validation_enabled = $basic_checkout_validation_enabled; $this->early_validation_enabled = $early_validation_enabled; + $this->pay_now_contexts = $pay_now_contexts; $this->logger = $logger; } @@ -254,10 +265,6 @@ class SmartButton implements SmartButtonInterface { $this->render_message_wrapper_registrar(); } - if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) { - return false; - } - if ( $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) @@ -284,7 +291,7 @@ class SmartButton implements SmartButtonInterface { add_filter( 'woocommerce_credit_card_form_fields', function ( array $default_fields, $id ) use ( $subscription_helper ) : array { - if ( is_user_logged_in() && $this->settings->has( 'vault_enabled' ) && $this->settings->get( 'vault_enabled' ) && CreditCardGateway::ID === $id ) { + if ( is_user_logged_in() && $this->settings->has( 'vault_enabled_dcc' ) && $this->settings->get( 'vault_enabled_dcc' ) && CreditCardGateway::ID === $id ) { $default_fields['card-vault'] = sprintf( '

', @@ -455,10 +462,6 @@ class SmartButton implements SmartButtonInterface { add_action( $this->mini_cart_button_renderer_hook(), function () { - if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) { - return; - } - if ( $this->is_cart_price_total_zero() || $this->is_free_trial_cart() ) { return; } @@ -509,31 +512,70 @@ class SmartButton implements SmartButtonInterface { } /** - * Whether the scripts should be loaded. + * Whether any of our scripts (for DCC or product, mini-cart, non-block cart/checkout) should be loaded. */ - public function should_load(): bool { + public function should_load_ppcp_script(): bool { $buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); - if ( ! is_checkout() && ! $buttons_enabled ) { + if ( ! $buttons_enabled ) { 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 { - if ( ! $this->should_load() ) { - return; + public function should_load_buttons() : bool { + $buttons_enabled = $this->settings->has( 'enabled' ) && $this->settings->get( 'enabled' ); + if ( ! $buttons_enabled ) { + return false; } - $load_script = false; - if ( is_checkout() && $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) ) { - $load_script = true; + $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 '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 ) ) { @@ -543,31 +585,21 @@ class SmartButton implements SmartButtonInterface { array(), $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( - 'ppcp-smart-button', - 'PayPalCommerceGateway', - $this->script_data() - ); - } + wp_enqueue_script( + 'ppcp-smart-button', + untrailingslashit( $this->module_url ) . '/assets/js/button.js', + array( 'jquery' ), + $this->version, + true + ); + + wp_localize_script( + 'ppcp-smart-button', + 'PayPalCommerceGateway', + $this->script_data() + ); } /** @@ -577,10 +609,6 @@ class SmartButton implements SmartButtonInterface { */ public function button_renderer( string $gateway_id ) { - if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) { - return; - } - $available_gateways = WC()->payment_gateways->get_available_payment_gateways(); if ( ! isset( $available_gateways[ $gateway_id ] ) ) { @@ -596,9 +624,6 @@ class SmartButton implements SmartButtonInterface { * Renders the HTML for the credit messaging. */ public function message_renderer() { - if ( ! $this->can_save_vault_token() && $this->has_subscriptions() ) { - return false; - } $product = wc_get_product(); @@ -666,18 +691,6 @@ class SmartButton implements SmartButtonInterface { } - /** - * Whether DCC fields can be rendered. - * - * @return bool - * @throws NotFoundException When a setting was not found. - */ - private function can_render_dcc() : bool { - - $can_render = $this->settings->has( 'dcc_enabled' ) && $this->settings->get( 'dcc_enabled' ) && $this->settings->has( 'client_id' ) && $this->settings->get( 'client_id' ) && $this->dcc_applies->for_country_currency(); - return $can_render; - } - /** * Renders the HTML for the DCC fields. */ @@ -710,7 +723,6 @@ class SmartButton implements SmartButtonInterface { * Whether we can store vault tokens or not. * * @return bool - * @throws NotFoundException If a setting hasn't been found. */ public function can_save_vault_token(): bool { @@ -744,6 +756,25 @@ class SmartButton implements SmartButtonInterface { return $this->subscription_helper->cart_contains_subscription(); } + /** + * Whether PayPal subscriptions is enabled or not. + * + * @return bool + */ + private function paypal_subscriptions_enabled(): bool { + if ( defined( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) && ! PPCP_FLAG_SUBSCRIPTIONS_API ) { + return false; + } + + try { + $subscriptions_mode = $this->settings->get( 'subscriptions_mode' ); + } catch ( NotFoundException $exception ) { + return false; + } + + return $subscriptions_mode === 'subscriptions_api'; + } + /** * Retrieves the 3D Secure contingency settings. * @@ -764,7 +795,6 @@ class SmartButton implements SmartButtonInterface { * The configuration for the smart buttons. * * @return array - * @throws NotFoundException If a setting hasn't been found. */ public function script_data(): array { $is_free_trial_cart = $this->is_free_trial_cart(); @@ -777,43 +807,49 @@ class SmartButton implements SmartButtonInterface { 'url_params' => $url_params, 'script_attributes' => $this->attributes(), 'data_client_id' => array( - 'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(), - 'endpoint' => \WC_AJAX::get_endpoint( DataClientIdEndpoint::ENDPOINT ), - 'nonce' => wp_create_nonce( DataClientIdEndpoint::nonce() ), - 'user' => get_current_user_id(), - 'has_subscriptions' => $this->has_subscriptions(), + 'set_attribute' => ( is_checkout() && $this->dcc_is_enabled() ) || $this->can_save_vault_token(), + 'endpoint' => \WC_AJAX::get_endpoint( DataClientIdEndpoint::ENDPOINT ), + 'nonce' => wp_create_nonce( DataClientIdEndpoint::nonce() ), + 'user' => get_current_user_id(), + 'has_subscriptions' => $this->has_subscriptions(), + 'paypal_subscriptions_enabled' => $this->paypal_subscriptions_enabled(), ), 'redirect' => wc_get_checkout_url(), 'context' => $this->context(), 'ajax' => array( - 'change_cart' => array( + 'change_cart' => array( 'endpoint' => \WC_AJAX::get_endpoint( ChangeCartEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ChangeCartEndpoint::nonce() ), ), - 'create_order' => array( + 'create_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( CreateOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( CreateOrderEndpoint::nonce() ), ), - 'approve_order' => array( + 'approve_order' => array( 'endpoint' => \WC_AJAX::get_endpoint( ApproveOrderEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ApproveOrderEndpoint::nonce() ), ), - 'vault_paypal' => array( + 'approve_subscription' => array( + 'endpoint' => \WC_AJAX::get_endpoint( ApproveSubscriptionEndpoint::ENDPOINT ), + 'nonce' => wp_create_nonce( ApproveSubscriptionEndpoint::nonce() ), + ), + 'vault_paypal' => array( 'endpoint' => \WC_AJAX::get_endpoint( StartPayPalVaultingEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( StartPayPalVaultingEndpoint::nonce() ), ), - 'save_checkout_form' => array( + 'save_checkout_form' => array( 'endpoint' => \WC_AJAX::get_endpoint( SaveCheckoutFormEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( SaveCheckoutFormEndpoint::nonce() ), ), - 'validate_checkout' => array( + 'validate_checkout' => array( 'endpoint' => \WC_AJAX::get_endpoint( ValidateCheckoutEndpoint::ENDPOINT ), 'nonce' => wp_create_nonce( ValidateCheckoutEndpoint::nonce() ), ), - 'cart_script_params' => array( + 'cart_script_params' => array( 'endpoint' => \WC_AJAX::get_endpoint( CartScriptParamsEndpoint::ENDPOINT ), ), ), + 'subscription_plan_id' => $this->paypal_subscription_id(), 'enforce_vault' => $this->has_subscriptions(), 'can_save_vault_token' => $this->can_save_vault_token(), 'is_free_trial_cart' => $is_free_trial_cart, @@ -911,6 +947,15 @@ class SmartButton implements SmartButtonInterface { $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(); return $localize; } @@ -933,21 +978,24 @@ class SmartButton implements SmartButtonInterface { * The JavaScript SDK url parameters. * * @return array - * @throws NotFoundException If a setting was not found. */ private function url_params(): array { - $intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture'; - $product_intent = $this->subscription_helper->current_product_is_subscription() ? 'authorize' : $intent; - $other_context_intent = $this->subscription_helper->cart_contains_subscription() ? 'authorize' : $intent; + $context = $this->context(); + try { + $intent = $this->intent(); + } catch ( NotFoundException $exception ) { + $intent = 'capture'; + } - $params = array( + $subscription_mode = $this->settings->has( 'subscriptions_mode' ) ? $this->settings->get( 'subscriptions_mode' ) : ''; + $params = array( 'client-id' => $this->client_id, 'currency' => $this->currency, 'integration-date' => PAYPAL_INTEGRATION_DATE, 'components' => implode( ',', $this->components() ), - 'vault' => $this->can_save_vault_token() ? 'true' : 'false', - 'commit' => is_checkout() ? 'true' : 'false', - 'intent' => $this->context() === 'product' ? $product_intent : $other_context_intent, + 'vault' => ( $this->can_save_vault_token() || $this->subscription_helper->need_subscription_intent( $subscription_mode ) ) ? 'true' : 'false', + 'commit' => in_array( $context, $this->pay_now_contexts, true ) ? 'true' : 'false', + 'intent' => $intent, ); if ( $this->environment->current_environment_is( Environment::SANDBOX ) @@ -991,6 +1039,13 @@ class SmartButton implements SmartButtonInterface { } } + if ( in_array( $context, array( 'checkout-block', 'cart-block' ), true ) ) { + $disable_funding = array_diff( + array_keys( $this->all_funding_sources ), + array( 'venmo', 'paylater' ) + ); + } + if ( $this->is_free_trial_cart() ) { $all_sources = array_keys( $this->all_funding_sources ); if ( $is_dcc_enabled || $is_separate_card_enabled ) { @@ -1001,8 +1056,8 @@ class SmartButton implements SmartButtonInterface { $enable_funding = array( 'venmo' ); - if ( $this->settings_status->is_pay_later_button_enabled_for_location( $this->context() ) || - $this->settings_status->is_pay_later_messaging_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( $context ) ) { $enable_funding[] = 'paylater'; } else { @@ -1071,7 +1126,7 @@ class SmartButton implements SmartButtonInterface { private function components(): array { $components = array(); - if ( $this->load_button_component() ) { + if ( $this->should_load_buttons() ) { $components[] = 'buttons'; $components[] = 'funding-eligibility'; } @@ -1087,34 +1142,10 @@ class SmartButton implements SmartButtonInterface { 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. * * @return bool - * @throws NotFoundException If a setting has not been found. */ private function dcc_is_enabled(): bool { if ( ! is_checkout() ) { @@ -1143,9 +1174,11 @@ class SmartButton implements SmartButtonInterface { * @param string $context The context. * * @return string - * @throws NotFoundException When a setting hasn't been found. */ private function style_for_context( string $style, string $context ): string { + // Use the cart/checkout styles for blocks. + $context = str_replace( '-block', '', $context ); + $defaults = array( 'layout' => 'vertical', 'size' => 'responsive', @@ -1362,6 +1395,57 @@ class SmartButton implements SmartButtonInterface { return false; } + /** + * Returns PayPal subscription plan id from WC subscription product. + * + * @return string + */ + private function paypal_subscription_id(): string { + if ( $this->subscription_helper->current_product_is_subscription() ) { + $product = wc_get_product(); + assert( $product instanceof WC_Product ); + + if ( $product->get_type() === 'subscription' && $product->meta_exists( 'ppcp_subscription_plan' ) ) { + return $product->get_meta( 'ppcp_subscription_plan' )['id']; + } + } + + $cart = WC()->cart ?? null; + if ( ! $cart || $cart->is_empty() ) { + return ''; + } + $items = $cart->get_cart_contents(); + foreach ( $items as $item ) { + $product = wc_get_product( $item['product_id'] ); + assert( $product instanceof WC_Product ); + + if ( $product->get_type() === 'subscription' && $product->meta_exists( 'ppcp_subscription_plan' ) ) { + return $product->get_meta( 'ppcp_subscription_plan' )['id']; + } + } + + return ''; + } + + /** + * Returns the intent. + * + * @return string + * @throws NotFoundException If intent is not found. + */ + private function intent(): string { + $intent = ( $this->settings->has( 'intent' ) ) ? $this->settings->get( 'intent' ) : 'capture'; + $product_intent = $this->subscription_helper->current_product_is_subscription() ? 'authorize' : $intent; + $other_context_intent = $this->subscription_helper->cart_contains_subscription() ? 'authorize' : $intent; + + $subscription_mode = $this->settings->has( 'subscriptions_mode' ) ? $this->settings->get( 'subscriptions_mode' ) : ''; + if ( $this->subscription_helper->need_subscription_intent( $subscription_mode ) ) { + return 'subscription'; + } + + return $this->context() === 'product' ? $product_intent : $other_context_intent; + } + /** * Returns the ID of WC order on the order-pay page, or 0. * diff --git a/modules/ppcp-button/src/Assets/SmartButtonInterface.php b/modules/ppcp-button/src/Assets/SmartButtonInterface.php index be5f5d015..3bd6ddb32 100644 --- a/modules/ppcp-button/src/Assets/SmartButtonInterface.php +++ b/modules/ppcp-button/src/Assets/SmartButtonInterface.php @@ -22,22 +22,15 @@ interface SmartButtonInterface { 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; - /** - * Whether the running installation could save vault tokens or not. - * - * @return bool - */ - public function can_save_vault_token(): bool; - /** * The configuration for the smart buttons. * diff --git a/modules/ppcp-button/src/ButtonModule.php b/modules/ppcp-button/src/ButtonModule.php index db3e67245..2c7337297 100644 --- a/modules/ppcp-button/src/ButtonModule.php +++ b/modules/ppcp-button/src/ButtonModule.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Button; +use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveSubscriptionEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint; use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint; @@ -63,14 +64,12 @@ class ButtonModule implements ModuleInterface { add_action( 'wp_enqueue_scripts', static function () use ( $c ) { - $smart_button = $c->get( 'button.smart-button' ); - /** - * The Smart Button. - * - * @var SmartButtonInterface $smart_button - */ - $smart_button->enqueue(); + assert( $smart_button instanceof SmartButtonInterface ); + + if ( $smart_button->should_load_ppcp_script() ) { + $smart_button->enqueue(); + } } ); @@ -147,6 +146,16 @@ class ButtonModule implements ModuleInterface { } ); + add_action( + 'wc_ajax_' . ApproveSubscriptionEndpoint::ENDPOINT, + static function () use ( $container ) { + $endpoint = $container->get( 'button.endpoint.approve-subscription' ); + assert( $endpoint instanceof ApproveSubscriptionEndpoint ); + + $endpoint->handle_request(); + } + ); + add_action( 'wc_ajax_' . CreateOrderEndpoint::ENDPOINT, static function () use ( $container ) { diff --git a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php index 7554f1731..7e4f7c4ee 100644 --- a/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/ApproveOrderEndpoint.php @@ -180,7 +180,7 @@ class ApproveOrderEndpoint implements EndpointInterface { ); } $this->session_handler->replace_order( $order ); - wp_send_json_success( $order ); + wp_send_json_success(); } if ( $this->order_helper->contains_physical_goods( $order ) && ! $order->status()->is( OrderStatus::APPROVED ) && ! $order->status()->is( OrderStatus::CREATED ) ) { @@ -198,7 +198,7 @@ class ApproveOrderEndpoint implements EndpointInterface { $this->session_handler->replace_funding_source( $funding_source ); $this->session_handler->replace_order( $order ); - wp_send_json_success( $order ); + wp_send_json_success(); return true; } catch ( Exception $error ) { $this->logger->error( 'Order approve failed: ' . $error->getMessage() ); diff --git a/modules/ppcp-button/src/Endpoint/ApproveSubscriptionEndpoint.php b/modules/ppcp-button/src/Endpoint/ApproveSubscriptionEndpoint.php new file mode 100644 index 000000000..d2ca73b83 --- /dev/null +++ b/modules/ppcp-button/src/Endpoint/ApproveSubscriptionEndpoint.php @@ -0,0 +1,94 @@ +request_data = $request_data; + $this->order_endpoint = $order_endpoint; + $this->session_handler = $session_handler; + } + + /** + * The nonce. + * + * @return string + */ + public static function nonce(): string { + return self::ENDPOINT; + } + + /** + * Handles the request. + * + * @return bool + * @throws RuntimeException When order not found or handling failed. + */ + public function handle_request(): bool { + $data = $this->request_data->read_request( $this->nonce() ); + if ( ! isset( $data['order_id'] ) ) { + throw new RuntimeException( + __( 'No order id given', 'woocommerce-paypal-payments' ) + ); + } + + $order = $this->order_endpoint->order( $data['order_id'] ); + $this->session_handler->replace_order( $order ); + + if ( isset( $data['subscription_id'] ) ) { + WC()->session->set( 'ppcp_subscription_id', $data['subscription_id'] ); + } + + wp_send_json_success(); + return true; + } +} diff --git a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php index 86c280be1..83d5f89b1 100644 --- a/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php +++ b/modules/ppcp-button/src/Endpoint/CreateOrderEndpoint.php @@ -138,6 +138,20 @@ class CreateOrderEndpoint implements EndpointInterface { */ 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. * @@ -159,6 +173,8 @@ class CreateOrderEndpoint implements EndpointInterface { * @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 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. */ public function __construct( @@ -173,6 +189,8 @@ class CreateOrderEndpoint implements EndpointInterface { bool $registration_needed, string $card_billing_data_mode, bool $early_validation_enabled, + array $pay_now_contexts, + bool $handle_shipping_in_paypal, LoggerInterface $logger ) { @@ -187,6 +205,8 @@ class CreateOrderEndpoint implements EndpointInterface { $this->registration_needed = $registration_needed; $this->card_billing_data_mode = $card_billing_data_mode; $this->early_validation_enabled = $early_validation_enabled; + $this->pay_now_contexts = $pay_now_contexts; + $this->handle_shipping_in_paypal = $handle_shipping_in_paypal; $this->logger = $logger; } @@ -226,7 +246,7 @@ class CreateOrderEndpoint implements EndpointInterface { } $this->purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); } 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. if ( ( @@ -272,7 +292,7 @@ class CreateOrderEndpoint implements EndpointInterface { ! $this->early_order_handler->should_create_early_order() || $this->registration_needed || isset( $data['createaccount'] ) && '1' === $data['createaccount'] ) { - wp_send_json_success( $order->to_array() ); + wp_send_json_success( $this->make_response( $order ) ); } $this->early_order_handler->register_for_order( $order ); @@ -284,7 +304,7 @@ class CreateOrderEndpoint implements EndpointInterface { $wc_order->save_meta_data(); } - wp_send_json_success( $order->to_array() ); + wp_send_json_success( $this->make_response( $order ) ); return true; } catch ( ValidationException $error ) { @@ -342,7 +362,7 @@ class CreateOrderEndpoint implements EndpointInterface { * during the "onApprove"-JS callback or the webhook listener. */ if ( ! $this->early_order_handler->should_create_early_order() ) { - wp_send_json_success( $order->to_array() ); + wp_send_json_success( $this->make_response( $order ) ); } $this->early_order_handler->register_for_order( $order ); return $data; @@ -385,6 +405,9 @@ class CreateOrderEndpoint implements EndpointInterface { $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 ( CardBillingMode::MINIMAL_INPUT === $this->card_billing_data_mode ) { if ( ApplicationContext::SHIPPING_PREFERENCE_SET_PROVIDED_ADDRESS === $shipping_preference ) { @@ -410,7 +433,9 @@ class CreateOrderEndpoint implements EndpointInterface { $shipping_preference, $payer, null, - $this->payment_method() + $this->payment_method(), + '', + $action ); } catch ( PayPalApiException $exception ) { // Looks like currently there is no proper way to validate the shipping address for PayPal, @@ -544,4 +569,17 @@ class CreateOrderEndpoint implements EndpointInterface { ); } } + + /** + * Returns the response data for success response. + * + * @param Order $order The PayPal order. + * @return array + */ + private function make_response( Order $order ): array { + return array( + 'id' => $order->id(), + 'custom_id' => $order->purchase_units()[0]->custom_id(), + ); + } } diff --git a/modules/ppcp-button/src/Helper/ContextTrait.php b/modules/ppcp-button/src/Helper/ContextTrait.php index 79abfa181..daddbb0a9 100644 --- a/modules/ppcp-button/src/Helper/ContextTrait.php +++ b/modules/ppcp-button/src/Helper/ContextTrait.php @@ -17,24 +17,32 @@ trait ContextTrait { * @return string */ protected function context(): string { - $context = 'mini-cart'; 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() ) { - $context = 'cart'; - } - - if ( is_checkout() && ! $this->is_paypal_continuation() ) { - $context = 'checkout'; + return 'cart'; } 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'; } /** diff --git a/modules/ppcp-compat/services.php b/modules/ppcp-compat/services.php index d345f2832..36dcf5f4c 100644 --- a/modules/ppcp-compat/services.php +++ b/modules/ppcp-compat/services.php @@ -49,6 +49,9 @@ return array( 'ppcp-gateway-settings', 'ppcp-webhooks-status-page', 'ppcp-tracking', + 'ppcp-fraudnet', + 'ppcp-gzd-compat', + 'ppcp-clear-db', ); }, diff --git a/modules/ppcp-compat/src/PPEC/PPECHelper.php b/modules/ppcp-compat/src/PPEC/PPECHelper.php index 71e16e544..70e882457 100644 --- a/modules/ppcp-compat/src/PPEC/PPECHelper.php +++ b/modules/ppcp-compat/src/PPEC/PPECHelper.php @@ -67,8 +67,12 @@ class PPECHelper { * @return bool */ public static function site_has_ppec_subscriptions() { - global $wpdb; + $has_ppec_subscriptions = get_transient( 'ppcp_has_ppec_subscriptions' ); + if ( $has_ppec_subscriptions !== false ) { + return $has_ppec_subscriptions === 'true'; + } + global $wpdb; $result = $wpdb->get_var( $wpdb->prepare( "SELECT 1 FROM {$wpdb->posts} p JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID @@ -80,6 +84,12 @@ class PPECHelper { ) ); + set_transient( + 'ppcp_has_ppec_subscriptions', + ! empty( $result ) ? 'true' : 'false', + 3 * MONTH_IN_SECONDS + ); + return ! empty( $result ); } @@ -92,7 +102,9 @@ class PPECHelper { /** * The filter returning whether the compatibility layer for PPEC Subscriptions should be initialized. */ - return ( ! self::is_gateway_available() ) && self::site_has_ppec_subscriptions() && apply_filters( 'woocommerce_paypal_payments_process_legacy_subscriptions', true ); + return ( ! self::is_gateway_available() ) + && self::site_has_ppec_subscriptions() + && apply_filters( 'woocommerce_paypal_payments_process_legacy_subscriptions', true ); } } diff --git a/modules/ppcp-compat/src/PPEC/SubscriptionsHandler.php b/modules/ppcp-compat/src/PPEC/SubscriptionsHandler.php index c61d036e3..bfb5b3f41 100644 --- a/modules/ppcp-compat/src/PPEC/SubscriptionsHandler.php +++ b/modules/ppcp-compat/src/PPEC/SubscriptionsHandler.php @@ -69,7 +69,7 @@ class SubscriptionsHandler { /** * Adds a mock gateway to disguise as PPEC when needed. Hooked onto `woocommerce_payment_gateways`. * The mock gateway fixes display issues where subscriptions paid via PPEC appear as "via Manual Renewal" and also - * prevents Subscriptions from automatically changing the payment method to "manual" when a subscription is edited. + * prevents subscriptions from automatically changing the payment method to "manual" when a subscription is edited. * * @param array $gateways List of gateways. * @return array diff --git a/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php b/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php index b7a6cdddb..11cc7bbfd 100644 --- a/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php +++ b/modules/ppcp-onboarding/src/Endpoint/LoginSellerEndpoint.php @@ -123,6 +123,11 @@ class LoginSellerEndpoint implements EndpointInterface { public function handle_request(): bool { try { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'Not admin.', 403 ); + return false; + } + $data = $this->request_data->read_request( $this->nonce() ); $is_sandbox = isset( $data['env'] ) && 'sandbox' === $data['env']; $this->settings->set( 'sandbox_on', $is_sandbox ); diff --git a/modules/ppcp-onboarding/src/Endpoint/PayUponInvoiceEndpoint.php b/modules/ppcp-onboarding/src/Endpoint/PayUponInvoiceEndpoint.php index 695e54b83..bc1607aa4 100644 --- a/modules/ppcp-onboarding/src/Endpoint/PayUponInvoiceEndpoint.php +++ b/modules/ppcp-onboarding/src/Endpoint/PayUponInvoiceEndpoint.php @@ -107,6 +107,11 @@ class PayUponInvoiceEndpoint implements EndpointInterface { * @throws NotFoundException When order not found or handling failed. */ public function handle_request(): bool { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'Not admin.', 403 ); + return false; + } + $signup_links = array(); try { diff --git a/modules/ppcp-order-tracking/src/Endpoint/OrderTrackingEndpoint.php b/modules/ppcp-order-tracking/src/Endpoint/OrderTrackingEndpoint.php index fb281ecef..065149288 100644 --- a/modules/ppcp-order-tracking/src/Endpoint/OrderTrackingEndpoint.php +++ b/modules/ppcp-order-tracking/src/Endpoint/OrderTrackingEndpoint.php @@ -85,6 +85,11 @@ class OrderTrackingEndpoint { * Handles the request. */ public function handle_request(): void { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'Not admin.', 403 ); + return; + } + try { $data = $this->request_data->read_request( $this->nonce() ); $action = $data['action']; diff --git a/modules/ppcp-session/services.php b/modules/ppcp-session/services.php index 0b36eb95b..0d9e25ccd 100644 --- a/modules/ppcp-session/services.php +++ b/modules/ppcp-session/services.php @@ -15,17 +15,7 @@ use WooCommerce\PayPalCommerce\Session\Cancellation\CancelView; return array( 'session.handler' => function ( ContainerInterface $container ) : SessionHandler { - - if ( is_null( WC()->session ) ) { - return new SessionHandler(); - } - $result = WC()->session->get( SessionHandler::ID ); - if ( is_a( $result, SessionHandler::class ) ) { - return $result; - } - $session_handler = new SessionHandler(); - WC()->session->set( SessionHandler::ID, $session_handler ); - return $session_handler; + return new SessionHandler(); }, 'session.cancellation.view' => function ( ContainerInterface $container ) : CancelView { return new CancelView( diff --git a/modules/ppcp-session/src/Cancellation/CancelController.php b/modules/ppcp-session/src/Cancellation/CancelController.php index 3c23983f5..79dbd4dcf 100644 --- a/modules/ppcp-session/src/Cancellation/CancelController.php +++ b/modules/ppcp-session/src/Cancellation/CancelController.php @@ -16,6 +16,8 @@ use WooCommerce\PayPalCommerce\Session\SessionHandler; */ class CancelController { + public const NONCE = 'ppcp-cancel'; + /** * The Session handler. * @@ -49,12 +51,11 @@ class CancelController { * Runs the controller. */ public function run() { - $param_name = 'ppcp-cancel'; - $nonce = 'ppcp-cancel-' . get_current_user_id(); + $param_name = self::NONCE; if ( isset( $_GET[ $param_name ] ) && // Input var ok. wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET[ $param_name ] ) ), // Input var ok. - $nonce + self::NONCE ) ) { // Input var ok. $this->session_handler->destroy_session_data(); @@ -74,11 +75,12 @@ class CancelController { 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( 'woocommerce_review_order_after_submit', 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() ); } ); } diff --git a/modules/ppcp-session/src/Cancellation/CancelView.php b/modules/ppcp-session/src/Cancellation/CancelView.php index cdaf9b41e..2242b15c1 100644 --- a/modules/ppcp-session/src/Cancellation/CancelView.php +++ b/modules/ppcp-session/src/Cancellation/CancelView.php @@ -50,7 +50,8 @@ class CancelView { * @param string $url The URL. * @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(); ?>

load_session(); + return $this->order; } @@ -60,13 +62,13 @@ class SessionHandler { * Replaces the current order. * * @param Order $order The new order. - * - * @return SessionHandler */ - public function replace_order( Order $order ) : SessionHandler { + public function replace_order( Order $order ): void { + $this->load_session(); + $this->order = $order; + $this->store_session(); - return $this; } /** @@ -75,6 +77,8 @@ class SessionHandler { * @return string */ public function bn_code() : string { + $this->load_session(); + return $this->bn_code; } @@ -82,13 +86,13 @@ class SessionHandler { * Replaces the BN Code. * * @param string $bn_code The new BN Code. - * - * @return SessionHandler */ - public function replace_bn_code( string $bn_code ) : SessionHandler { + public function replace_bn_code( string $bn_code ) : void { + $this->load_session(); + $this->bn_code = $bn_code; + $this->store_session(); - return $this; } /** @@ -97,6 +101,8 @@ class SessionHandler { * @return string|null */ public function funding_source(): ?string { + $this->load_session(); + return $this->funding_source; } @@ -104,13 +110,13 @@ class SessionHandler { * Replaces the funding source of the current checkout. * * @param string|null $funding_source The funding source. - * - * @return SessionHandler */ - public function replace_funding_source( ?string $funding_source ): SessionHandler { + public function replace_funding_source( ?string $funding_source ): void { + $this->load_session(); + $this->funding_source = $funding_source; + $this->store_session(); - return $this; } /** @@ -119,18 +125,20 @@ class SessionHandler { * @return int */ public function insufficient_funding_tries() : int { + $this->load_session(); + return $this->insufficient_funding_tries; } /** * Increments the number of tries, the customer has done in this session. - * - * @return SessionHandler */ - public function increment_insufficient_funding_tries() : SessionHandler { + public function increment_insufficient_funding_tries(): void { + $this->load_session(); + $this->insufficient_funding_tries++; + $this->store_session(); - return $this; } /** @@ -148,9 +156,52 @@ class SessionHandler { } /** - * Stores the session. + * Stores the data into the WC session. */ - private function store_session() { - WC()->session->set( self::ID, $this ); + private function store_session(): void { + WC()->session->set( self::SESSION_KEY, self::make_array( $this ) ); + } + + /** + * Loads the data from the session. + */ + private function load_session(): void { + if ( isset( WC()->session ) ) { + $data = WC()->session->get( self::SESSION_KEY ); + } else { + $data = array(); + } + + if ( $data instanceof SessionHandler ) { + $data = self::make_array( $data ); + } elseif ( ! is_array( $data ) ) { + $data = array(); + } + + $this->order = $data['order'] ?? null; + if ( ! $this->order instanceof Order ) { + $this->order = null; + } + $this->bn_code = (string) ( $data['bn_code'] ?? '' ); + $this->insufficient_funding_tries = (int) ( $data['insufficient_funding_tries'] ?? '' ); + $this->funding_source = $data['funding_source'] ?? null; + if ( ! is_string( $this->funding_source ) ) { + $this->funding_source = null; + } + } + + /** + * Converts given SessionHandler object into an array. + * + * @param SessionHandler $obj The object to convert. + * @return array + */ + private static function make_array( SessionHandler $obj ): array { + return array( + 'order' => $obj->order, + 'bn_code' => $obj->bn_code, + 'insufficient_funding_tries' => $obj->insufficient_funding_tries, + 'funding_source' => $obj->funding_source, + ); } } diff --git a/modules/ppcp-subscription/services.php b/modules/ppcp-subscription/services.php index a24002869..b9ce27543 100644 --- a/modules/ppcp-subscription/services.php +++ b/modules/ppcp-subscription/services.php @@ -43,4 +43,15 @@ return array( $endpoint = $container->get( 'api.endpoint.payment-token' ); return new PaymentTokenRepository( $factory, $endpoint ); }, + 'subscription.api-handler' => static function( ContainerInterface $container ): SubscriptionsApiHandler { + return new SubscriptionsApiHandler( + $container->get( 'api.endpoint.catalog-products' ), + $container->get( 'api.factory.product' ), + $container->get( 'api.endpoint.billing-plans' ), + $container->get( 'api.factory.billing-cycle' ), + $container->get( 'api.factory.payment-preferences' ), + $container->get( 'api.shop.currency' ), + $container->get( 'woocommerce.logger.woocommerce' ) + ); + }, ); diff --git a/modules/ppcp-subscription/src/Helper/SubscriptionHelper.php b/modules/ppcp-subscription/src/Helper/SubscriptionHelper.php index 5d4984081..286fc2bc1 100644 --- a/modules/ppcp-subscription/src/Helper/SubscriptionHelper.php +++ b/modules/ppcp-subscription/src/Helper/SubscriptionHelper.php @@ -127,4 +127,27 @@ class SubscriptionHelper { return true; } + + /** + * Checks whether subscription needs subscription intent. + * + * @param string $subscription_mode The subscriptiopn mode. + * @return bool + */ + public function need_subscription_intent( string $subscription_mode ): bool { + if ( defined( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) && ! PPCP_FLAG_SUBSCRIPTIONS_API ) { + return false; + } + + if ( $subscription_mode === 'subscriptions_api' ) { + if ( + $this->current_product_is_subscription() + || ( ( is_cart() || is_checkout() ) && $this->cart_contains_subscription() ) + ) { + return true; + } + } + + return false; + } } diff --git a/modules/ppcp-subscription/src/RenewalHandler.php b/modules/ppcp-subscription/src/RenewalHandler.php index 851ea5f7d..3253c2f54 100644 --- a/modules/ppcp-subscription/src/RenewalHandler.php +++ b/modules/ppcp-subscription/src/RenewalHandler.php @@ -162,13 +162,6 @@ class RenewalHandler { return; } - - $this->logger->info( - sprintf( - 'Renewal for order %d is completed.', - $wc_order->get_id() - ) - ); } /** @@ -186,6 +179,7 @@ class RenewalHandler { if ( ! $token ) { return; } + $purchase_unit = $this->purchase_unit_factory->from_wc_order( $wc_order ); $payer = $this->payer_factory->from_customer( $customer ); $shipping_preference = $this->shipping_preference_factory->from_state( @@ -217,6 +211,13 @@ class RenewalHandler { if ( $this->capture_authorized_downloads( $order ) ) { $this->authorized_payments_processor->capture_authorized_payment( $wc_order ); } + + $this->logger->info( + sprintf( + 'Renewal for order %d is completed.', + $wc_order->get_id() + ) + ); } /** diff --git a/modules/ppcp-subscription/src/SubscriptionModule.php b/modules/ppcp-subscription/src/SubscriptionModule.php index ae21ad42a..099ee18ff 100644 --- a/modules/ppcp-subscription/src/SubscriptionModule.php +++ b/modules/ppcp-subscription/src/SubscriptionModule.php @@ -9,6 +9,11 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Subscription; +use Exception; +use WC_Product; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions; +use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; +use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use Psr\Log\LoggerInterface; @@ -102,8 +107,19 @@ class SubscriptionModule implements ModuleInterface { function( array $data ) use ( $c ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing $wc_order_action = wc_clean( wp_unslash( $_POST['wc_order_action'] ?? '' ) ); + + // phpcs:ignore WordPress.Security.NonceVerification.Missing + $subscription_id = wc_clean( wp_unslash( $_POST['post_ID'] ?? '' ) ); + if ( ! $subscription_id ) { + return $data; + } + $subscription = wc_get_order( $subscription_id ); + if ( ! is_a( $subscription, WC_Subscription::class ) ) { + return $data; + } + if ( - $wc_order_action === 'wcs_process_renewal' + $wc_order_action === 'wcs_process_renewal' && $subscription->get_payment_method() === CreditCardGateway::ID && isset( $data['payment_source']['token'] ) && $data['payment_source']['token']['type'] === 'PAYMENT_METHOD_TOKEN' && isset( $data['payment_source']['token']['source']->card ) ) { @@ -135,6 +151,10 @@ class SubscriptionModule implements ModuleInterface { return $data; } ); + + if ( defined( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) && PPCP_FLAG_SUBSCRIPTIONS_API ) { + $this->subscriptions_api_integration( $c ); + } } /** @@ -265,8 +285,8 @@ class SubscriptionModule implements ModuleInterface { SubscriptionHelper $subscription_helper ) { - if ( $settings->has( 'vault_enabled' ) - && $settings->get( 'vault_enabled' ) + if ( $settings->has( 'vault_enabled_dcc' ) + && $settings->get( 'vault_enabled_dcc' ) && $subscription_helper->is_subscription_change_payment() && CreditCardGateway::ID === $id ) { @@ -303,4 +323,406 @@ class SubscriptionModule implements ModuleInterface { return $default_fields; } + + /** + * Adds PayPal subscriptions API integration. + * + * @param ContainerInterface $c The container. + * @return void + * @throws Exception When something went wrong. + */ + protected function subscriptions_api_integration( ContainerInterface $c ): void { + add_action( + 'save_post', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + function( $product_id ) use ( $c ) { + $settings = $c->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + try { + $subscriptions_mode = $settings->get( 'subscriptions_mode' ); + } catch ( NotFoundException $exception ) { + return; + } + + $nonce = wc_clean( wp_unslash( $_POST['_wcsnonce'] ?? '' ) ); + if ( + $subscriptions_mode !== 'subscriptions_api' + || ! is_string( $nonce ) + || ! wp_verify_nonce( $nonce, 'wcs_subscription_meta' ) ) { + return; + } + + $product = wc_get_product( $product_id ); + if ( ! is_a( $product, WC_Product::class ) ) { + return; + } + + $enable_subscription_product = wc_clean( wp_unslash( $_POST['_ppcp_enable_subscription_product'] ?? '' ) ); + $product->update_meta_data( '_ppcp_enable_subscription_product', $enable_subscription_product ); + $product->save(); + + if ( $product->get_type() === 'subscription' && $enable_subscription_product === 'yes' ) { + $subscriptions_api_handler = $c->get( 'subscription.api-handler' ); + assert( $subscriptions_api_handler instanceof SubscriptionsApiHandler ); + + if ( $product->meta_exists( 'ppcp_subscription_product' ) && $product->meta_exists( 'ppcp_subscription_plan' ) ) { + $subscriptions_api_handler->update_product( $product ); + $subscriptions_api_handler->update_plan( $product ); + return; + } + + if ( ! $product->meta_exists( 'ppcp_subscription_product' ) ) { + $subscriptions_api_handler->create_product( $product ); + } + + if ( $product->meta_exists( 'ppcp_subscription_product' ) && ! $product->meta_exists( 'ppcp_subscription_plan' ) ) { + $subscription_plan_name = wc_clean( wp_unslash( $_POST['_ppcp_subscription_plan_name'] ?? '' ) ); + if ( ! is_string( $subscription_plan_name ) ) { + return; + } + + $product->update_meta_data( '_ppcp_subscription_plan_name', $subscription_plan_name ); + $product->save(); + + $subscriptions_api_handler->create_plan( $subscription_plan_name, $product ); + } + } + }, + 12 + ); + + add_action( + 'woocommerce_process_shop_subscription_meta', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + function( $id, $subscription ) use ( $c ) { + $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; + if ( $subscription_id ) { + $subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' ); + assert( $subscriptions_endpoint instanceof BillingSubscriptions ); + + if ( $subscription->get_status() === 'cancelled' ) { + try { + $subscriptions_endpoint->cancel( $subscription_id ); + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + $logger->error( 'Could not cancel subscription product on PayPal. ' . $error ); + } + } + + if ( $subscription->get_status() === 'pending-cancel' ) { + try { + $subscriptions_endpoint->suspend( $subscription_id ); + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + $logger->error( 'Could not suspend subscription product on PayPal. ' . $error ); + } + } + + if ( $subscription->get_status() === 'active' ) { + try { + $current_subscription = $subscriptions_endpoint->subscription( $subscription_id ); + if ( $current_subscription->status === 'SUSPENDED' ) { + $subscriptions_endpoint->activate( $subscription_id ); + } + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + $logger->error( 'Could not reactivate subscription product on PayPal. ' . $error ); + } + } + } + }, + 20, + 2 + ); + + add_filter( + 'woocommerce_order_actions', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + function( $actions, $subscription ): array { + if ( ! is_a( $subscription, WC_Subscription::class ) ) { + return $actions; + } + + $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; + if ( $subscription_id && isset( $actions['wcs_process_renewal'] ) ) { + unset( $actions['wcs_process_renewal'] ); + } + + return $actions; + }, + 20, + 2 + ); + + add_filter( + 'wcs_view_subscription_actions', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + function( $actions, $subscription ): array { + if ( ! is_a( $subscription, WC_Subscription::class ) ) { + return $actions; + } + + $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; + if ( $subscription_id && $subscription->get_status() === 'active' ) { + $url = wp_nonce_url( + add_query_arg( + array( + 'change_subscription_to' => 'cancelled', + 'ppcp_cancel_subscription' => $subscription->get_id(), + ) + ), + 'ppcp_cancel_subscription_nonce' + ); + + array_unshift( + $actions, + array( + 'url' => esc_url( $url ), + 'name' => esc_html__( 'Cancel', 'woocommerce-paypal-payments' ), + ) + ); + + $actions['cancel']['name'] = esc_html__( 'Suspend', 'woocommerce-paypal-payments' ); + unset( $actions['subscription_renewal_early'] ); + } + + return $actions; + }, + 11, + 2 + ); + + add_action( + 'wp_loaded', + function() use ( $c ) { + if ( ! function_exists( 'wcs_get_subscription' ) ) { + return; + } + + $cancel_subscription_id = wc_clean( wp_unslash( $_GET['ppcp_cancel_subscription'] ?? '' ) ); + $subscription = wcs_get_subscription( absint( $cancel_subscription_id ) ); + if ( ! wcs_is_subscription( $subscription ) || $subscription === false ) { + return; + } + + $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; + $nonce = wc_clean( wp_unslash( $_GET['_wpnonce'] ?? '' ) ); + if ( ! is_string( $nonce ) ) { + return; + } + + if ( + $subscription_id + && $cancel_subscription_id + && $nonce + ) { + if ( + ! wp_verify_nonce( $nonce, 'ppcp_cancel_subscription_nonce' ) + || ! user_can( get_current_user_id(), 'edit_shop_subscription_status', $subscription->get_id() ) + ) { + return; + } + + $subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' ); + $subscription_id = $subscription->get_meta( 'ppcp_subscription' ); + try { + $subscriptions_endpoint->cancel( $subscription_id ); + + $subscription->update_status( 'cancelled' ); + $subscription->add_order_note( __( 'Subscription cancelled by the subscriber from their account page.', 'woocommerce-paypal-payments' ) ); + wc_add_notice( __( 'Your subscription has been cancelled.', 'woocommerce-paypal-payments' ) ); + + wp_safe_redirect( $subscription->get_view_order_url() ); + exit; + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + $logger->error( 'Could not cancel subscription product on PayPal. ' . $error ); + } + } + }, + 100 + ); + + add_action( + 'woocommerce_subscription_before_actions', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + function( $subscription ) use ( $c ) { + $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; + if ( $subscription_id ) { + $environment = $c->get( 'onboarding.environment' ); + $host = $environment->current_environment_is( Environment::SANDBOX ) ? 'https://www.sandbox.paypal.com' : 'https://www.paypal.com'; + ?> + + + + " id="ppcp-subscription-id" target="_blank"> + + + get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + + try { + $subscriptions_mode = $settings->get( 'subscriptions_mode' ); + if ( $subscriptions_mode === 'subscriptions_api' ) { + /** + * Needed for getting global post object. + * + * @psalm-suppress InvalidGlobal + */ + global $post; + $product = wc_get_product( $post->ID ); + if ( ! is_a( $product, WC_Product::class ) ) { + return; + } + + $enable_subscription_product = $product->get_meta( '_ppcp_enable_subscription_product' ); + $subscription_plan_name = $product->get_meta( '_ppcp_subscription_plan_name' ); + + echo ''; + } + } catch ( NotFoundException $exception ) { + return; + } + } + ); + + add_filter( + 'woocommerce_order_data_store_cpt_get_orders_query', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + function( $query, $query_vars ): array { + if ( ! empty( $query_vars['ppcp_subscription'] ) ) { + $query['meta_query'][] = array( + 'key' => 'ppcp_subscription', + 'value' => esc_attr( $query_vars['ppcp_subscription'] ), + ); + } + + return $query; + }, + 10, + 2 + ); + + add_action( + 'woocommerce_customer_changed_subscription_to_cancelled', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + function( $subscription ) use ( $c ) { + $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; + if ( $subscription_id ) { + $subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' ); + assert( $subscriptions_endpoint instanceof BillingSubscriptions ); + + try { + $subscriptions_endpoint->suspend( $subscription_id ); + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + $logger->error( 'Could not suspend subscription product on PayPal. ' . $error ); + } + } + } + ); + + add_action( + 'woocommerce_customer_changed_subscription_to_active', + /** + * Param types removed to avoid third-party issues. + * + * @psalm-suppress MissingClosureParamType + */ + function( $subscription ) use ( $c ) { + $subscription_id = $subscription->get_meta( 'ppcp_subscription' ) ?? ''; + if ( $subscription_id ) { + $subscriptions_endpoint = $c->get( 'api.endpoint.billing-subscriptions' ); + assert( $subscriptions_endpoint instanceof BillingSubscriptions ); + + try { + $subscriptions_endpoint->activate( $subscription_id ); + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $logger = $c->get( 'woocommerce.logger.woocommerce' ); + $logger->error( 'Could not active subscription product on PayPal. ' . $error ); + } + } + } + ); + } } diff --git a/modules/ppcp-subscription/src/SubscriptionsApiHandler.php b/modules/ppcp-subscription/src/SubscriptionsApiHandler.php new file mode 100644 index 000000000..bdfa9f9b5 --- /dev/null +++ b/modules/ppcp-subscription/src/SubscriptionsApiHandler.php @@ -0,0 +1,277 @@ +products_endpoint = $products_endpoint; + $this->product_factory = $product_factory; + $this->billing_plans_endpoint = $billing_plans_endpoint; + $this->billing_cycle_factory = $billing_cycle_factory; + $this->payment_preferences_factory = $payment_preferences_factory; + $this->currency = $currency; + $this->logger = $logger; + } + + /** + * Creates a Catalog Product and adds it as WC product meta. + * + * @param WC_Product $product The WC product. + * @return void + */ + public function create_product( WC_Product $product ) { + try { + $subscription_product = $this->products_endpoint->create( $product->get_title(), $product->get_description() ); + $product->update_meta_data( 'ppcp_subscription_product', $subscription_product->to_array() ); + $product->save(); + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $this->logger->error( 'Could not create catalog product on PayPal. ' . $error ); + } + } + + /** + * Creates a subscription plan. + * + * @param string $plan_name The plan name. + * @param WC_Product $product The WC product. + * @return void + */ + public function create_plan( string $plan_name, WC_Product $product ): void { + try { + $subscription_plan = $this->billing_plans_endpoint->create( + $plan_name, + $product->get_meta( 'ppcp_subscription_product' )['id'] ?? '', + $this->billing_cycles( $product ), + $this->payment_preferences_factory->from_wc_product( $product )->to_array() + ); + + $product->update_meta_data( 'ppcp_subscription_plan', $subscription_plan->to_array() ); + $product->save(); + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $this->logger->error( 'Could not create subscription plan on PayPal. ' . $error ); + } + } + + /** + * Updates a product. + * + * @param WC_Product $product The WC product. + * @return void + */ + public function update_product( WC_Product $product ): void { + try { + $catalog_product_id = $product->get_meta( 'ppcp_subscription_product' )['id'] ?? ''; + if ( $catalog_product_id ) { + $catalog_product = $this->products_endpoint->product( $catalog_product_id ); + $catalog_product_name = $catalog_product->name() ?: ''; + $catalog_product_description = $catalog_product->description() ?: ''; + if ( $catalog_product_name !== $product->get_title() || $catalog_product_description !== $product->get_description() ) { + $data = array(); + if ( $catalog_product_name !== $product->get_title() ) { + $data[] = (object) array( + 'op' => 'replace', + 'path' => '/name', + 'value' => $product->get_title(), + ); + } + if ( $catalog_product_description !== $product->get_description() ) { + $data[] = (object) array( + 'op' => 'replace', + 'path' => '/description', + 'value' => $product->get_description(), + ); + } + + $this->products_endpoint->update( $catalog_product_id, $data ); + } + } + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $this->logger->error( 'Could not update catalog product on PayPal. ' . $error ); + } + } + + /** + * Updates a plan. + * + * @param WC_Product $product The WC product. + * @return void + */ + public function update_plan( WC_Product $product ): void { + try { + $subscription_plan_id = $product->get_meta( 'ppcp_subscription_plan' )['id'] ?? ''; + if ( $subscription_plan_id ) { + $subscription_plan = $this->billing_plans_endpoint->plan( $subscription_plan_id ); + + $price = $subscription_plan->billing_cycles()[0]->pricing_scheme()['fixed_price']['value'] ?? ''; + if ( $price && round( $price, 2 ) !== round( (float) $product->get_price(), 2 ) ) { + $this->billing_plans_endpoint->update_pricing( + $subscription_plan_id, + $this->billing_cycle_factory->from_wc_product( $product ) + ); + } + } + } catch ( RuntimeException $exception ) { + $error = $exception->getMessage(); + if ( is_a( $exception, PayPalApiException::class ) ) { + $error = $exception->get_details( $error ); + } + + $this->logger->error( 'Could not update subscription plan on PayPal. ' . $error ); + } + } + + /** + * Returns billing cycles based on WC Subscription product. + * + * @param WC_Product $product The WC Subscription product. + * @return array + */ + private function billing_cycles( WC_Product $product ): array { + $billing_cycles = array(); + $sequence = 1; + + $trial_length = $product->get_meta( '_subscription_trial_length' ) ?? ''; + if ( $trial_length ) { + $billing_cycles[] = ( new BillingCycle( + array( + 'interval_unit' => $product->get_meta( '_subscription_trial_period' ), + 'interval_count' => $product->get_meta( '_subscription_trial_length' ), + ), + $sequence, + 'TRIAL', + array( + 'fixed_price' => array( + 'value' => '0', + 'currency_code' => $this->currency, + ), + ), + 1 + ) )->to_array(); + + $sequence++; + } + + $billing_cycles[] = ( new BillingCycle( + array( + 'interval_unit' => $product->get_meta( '_subscription_period' ), + 'interval_count' => $product->get_meta( '_subscription_period_interval' ), + ), + $sequence, + 'REGULAR', + array( + 'fixed_price' => array( + 'value' => $product->get_meta( '_subscription_price' ), + 'currency_code' => $this->currency, + ), + ), + (int) $product->get_meta( '_subscription_length' ) + ) )->to_array(); + + return $billing_cycles; + } +} diff --git a/modules/ppcp-uninstall/src/UninstallModule.php b/modules/ppcp-uninstall/src/UninstallModule.php index 1d740e4b2..649be5c49 100644 --- a/modules/ppcp-uninstall/src/UninstallModule.php +++ b/modules/ppcp-uninstall/src/UninstallModule.php @@ -81,8 +81,14 @@ class UninstallModule implements ModuleInterface { "wc_ajax_{$nonce}", static function () use ( $request_data, $clear_db, $nonce, $option_names, $scheduled_action_names ) { try { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'Not admin.', 403 ); + return false; + } + // Validate nonce. $request_data->read_request( $nonce ); + $clear_db->delete_options( $option_names ); $clear_db->clear_scheduled_actions( $scheduled_action_names ); diff --git a/modules/ppcp-vaulting/src/PaymentTokenACDC.php b/modules/ppcp-vaulting/src/PaymentTokenACDC.php deleted file mode 100644 index 43530daa4..000000000 --- a/modules/ppcp-vaulting/src/PaymentTokenACDC.php +++ /dev/null @@ -1,72 +0,0 @@ - '', - 'card_type' => '', - ); - - /** - * Returns the last four digits. - * - * @param string $context The context. - * @return mixed|null - */ - public function get_last4( $context = 'view' ) { - return $this->get_prop( 'last4', $context ); - } - - /** - * Set the last four digits. - * - * @param string $last4 Last four digits. - */ - public function set_last4( $last4 ) { - $this->set_prop( 'last4', $last4 ); - } - - /** - * Returns the card type (mastercard, visa, ...). - * - * @param string $context The context. - * @return string Card type - */ - public function get_card_type( $context = 'view' ) { - return $this->get_prop( 'card_type', $context ); - } - - /** - * Set the card type (mastercard, visa, ...). - * - * @param string $type Credit card type (mastercard, visa, ...). - */ - public function set_card_type( $type ) { - $this->set_prop( 'card_type', $type ); - } -} diff --git a/modules/ppcp-vaulting/src/PaymentTokenFactory.php b/modules/ppcp-vaulting/src/PaymentTokenFactory.php index 9dcd3185e..86aa11bd5 100644 --- a/modules/ppcp-vaulting/src/PaymentTokenFactory.php +++ b/modules/ppcp-vaulting/src/PaymentTokenFactory.php @@ -19,14 +19,12 @@ class PaymentTokenFactory { * * @param string $type The type of WC payment token. * - * @return void|PaymentTokenACDC|PaymentTokenPayPal + * @return void|PaymentTokenPayPal */ public function create( string $type ) { switch ( $type ) { case 'paypal': return new PaymentTokenPayPal(); - case 'acdc': - return new PaymentTokenACDC(); } } } diff --git a/modules/ppcp-vaulting/src/PaymentTokensMigration.php b/modules/ppcp-vaulting/src/PaymentTokensMigration.php index 2ceb70c6b..69b6c6fda 100644 --- a/modules/ppcp-vaulting/src/PaymentTokensMigration.php +++ b/modules/ppcp-vaulting/src/PaymentTokensMigration.php @@ -11,6 +11,7 @@ namespace WooCommerce\PayPalCommerce\Vaulting; use Exception; use Psr\Log\LoggerInterface; +use WC_Payment_Token_CC; use WC_Payment_Tokens; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; @@ -64,23 +65,26 @@ class PaymentTokensMigration { * @param int $id WooCommerce customer id. */ public function migrate_payment_tokens_for_user( int $id ):void { - $tokens = $this->payment_token_repository->all_for_user_id( $id ); - $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $id ); + $tokens = $this->payment_token_repository->all_for_user_id( $id ); + $total_tokens = count( $tokens ); + $this->logger->info( 'Migrating ' . (string) $total_tokens . ' tokens for user ' . (string) $id ); foreach ( $tokens as $token ) { - if ( $this->token_exist( $wc_tokens, $token ) ) { - continue; - } - if ( isset( $token->source()->card ) ) { - $payment_token_acdc = $this->payment_token_factory->create( 'acdc' ); - assert( $payment_token_acdc instanceof PaymentTokenACDC ); + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $id, CreditCardGateway::ID ); + if ( $this->token_exist( $wc_tokens, $token ) ) { + $this->logger->info( 'Token already exist for user ' . (string) $id ); + continue; + } + $payment_token_acdc = new WC_Payment_Token_CC(); $payment_token_acdc->set_token( $token->id() ); $payment_token_acdc->set_user_id( $id ); $payment_token_acdc->set_gateway_id( CreditCardGateway::ID ); $payment_token_acdc->set_last4( $token->source()->card->last_digits ); $payment_token_acdc->set_card_type( $token->source()->card->brand ); + $payment_token_acdc->set_expiry_year( '0000' ); + $payment_token_acdc->set_expiry_month( '00' ); try { $payment_token_acdc->save(); @@ -92,6 +96,12 @@ class PaymentTokensMigration { continue; } } elseif ( $token->source()->paypal ) { + $wc_tokens = WC_Payment_Tokens::get_customer_tokens( $id, PayPalGateway::ID ); + if ( $this->token_exist( $wc_tokens, $token ) ) { + $this->logger->info( 'Token already exist for user ' . (string) $id ); + continue; + } + $payment_token_paypal = $this->payment_token_factory->create( 'paypal' ); assert( $payment_token_paypal instanceof PaymentTokenPayPal ); diff --git a/modules/ppcp-vaulting/src/VaultingModule.php b/modules/ppcp-vaulting/src/VaultingModule.php index f1bcc7ea6..824f206e8 100644 --- a/modules/ppcp-vaulting/src/VaultingModule.php +++ b/modules/ppcp-vaulting/src/VaultingModule.php @@ -22,6 +22,7 @@ use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; +use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WP_User_Query; /** @@ -47,11 +48,6 @@ class VaultingModule implements ModuleInterface { * @throws NotFoundException When service could not be found. */ public function run( ContainerInterface $container ): void { - $settings = $container->get( 'wcgateway.settings' ); - if ( ! $settings->has( 'vault_enabled' ) || ! $settings->get( 'vault_enabled' ) ) { - return; - } - $listener = $container->get( 'vaulting.customer-approval-listener' ); assert( $listener instanceof CustomerApprovalListener ); @@ -92,10 +88,6 @@ class VaultingModule implements ModuleInterface { * @psalm-suppress MissingClosureParamType */ function ( $type ) { - if ( $type === 'WC_Payment_Token_ACDC' ) { - return PaymentTokenACDC::class; - } - if ( $type === 'WC_Payment_Token_PayPal' ) { return PaymentTokenPayPal::class; } @@ -116,13 +108,6 @@ class VaultingModule implements ModuleInterface { return $item; } - if ( strtolower( $payment_token->get_type() ) === 'acdc' ) { - assert( $payment_token instanceof PaymentTokenACDC ); - $item['method']['brand'] = $payment_token->get_card_type() . ' ...' . $payment_token->get_last4(); - - return $item; - } - if ( strtolower( $payment_token->get_type() ) === 'paypal' ) { assert( $payment_token instanceof PaymentTokenPayPal ); $item['method']['brand'] = $payment_token->get_email(); @@ -179,23 +164,37 @@ class VaultingModule implements ModuleInterface { add_action( 'woocommerce_paypal_payments_gateway_migrate_on_update', function () use ( $container ) { - // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key - // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query - $customers = new WP_User_Query( - array( - 'fields' => 'ID', - 'limit' => -1, - 'meta_key' => 'ppcp-vault-token', - ) - ); - // phpcs:enable - - $migrate = $container->get( 'vaulting.payment-tokens-migration' ); - assert( $migrate instanceof PaymentTokensMigration ); - - foreach ( $customers->get_results() as $id ) { - $migrate->migrate_payment_tokens_for_user( (int) $id ); + $settings = $container->get( 'wcgateway.settings' ); + assert( $settings instanceof Settings ); + if ( $settings->has( 'vault_enabled' ) && $settings->get( 'vault_enabled' ) && $settings->has( 'vault_enabled_dcc' ) ) { + $settings->set( 'vault_enabled_dcc', true ); + $settings->persist(); } + + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + + $this->migrate_payment_tokens( $logger ); + } + ); + + add_action( + 'pcp_migrate_payment_tokens', + function() use ( $container ) { + $logger = $container->get( 'woocommerce.logger.woocommerce' ); + assert( $logger instanceof LoggerInterface ); + + $this->migrate_payment_tokens( $logger ); + } + ); + + add_action( + 'woocommerce_paypal_payments_payment_tokens_migration', + function( int $customer_id ) use ( $container ) { + $migration = $container->get( 'vaulting.payment-tokens-migration' ); + assert( $migration instanceof PaymentTokensMigration ); + + $migration->migrate_payment_tokens_for_user( $customer_id ); } ); @@ -212,6 +211,51 @@ class VaultingModule implements ModuleInterface { ); } + /** + * Runs the payment tokens migration for users with saved payments. + * + * @param LoggerInterface $logger The logger. + * @return void + */ + public function migrate_payment_tokens( LoggerInterface $logger ): void { + // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_key + // phpcs:disable WordPress.DB.SlowDBQuery.slow_db_query_meta_query + $customers = new WP_User_Query( + array( + 'fields' => 'ID', + 'limit' => -1, + 'meta_key' => 'ppcp-vault-token', + ) + ); + // phpcs:enable + + $customers = $customers->get_results(); + if ( count( $customers ) === 0 ) { + $logger->info( 'No customers for payment tokens migration.' ); + return; + } + + $logger->info( 'Starting payment tokens migration for ' . (string) count( $customers ) . ' users' ); + + $interval_in_seconds = 5; + $timestamp = time(); + + foreach ( $customers as $id ) { + /** + * Function already exist in WooCommerce + * + * @psalm-suppress UndefinedFunction + */ + as_schedule_single_action( + $timestamp, + 'woocommerce_paypal_payments_payment_tokens_migration', + array( 'customer_id' => $id ) + ); + + $timestamp += $interval_in_seconds; + } + } + /** * Filters the emails when vaulting is failed for subscription orders. * diff --git a/modules/ppcp-wc-gateway/services.php b/modules/ppcp-wc-gateway/services.php index 6a95c98a2..901c1f94a 100644 --- a/modules/ppcp-wc-gateway/services.php +++ b/modules/ppcp-wc-gateway/services.php @@ -11,6 +11,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint; use WooCommerce\PayPalCommerce\Session\SessionHandler; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer; @@ -32,13 +33,13 @@ use WooCommerce\PayPalCommerce\WcGateway\Admin\RenderAuthorizeAction; use WooCommerce\PayPalCommerce\WcGateway\Assets\FraudNetAssets; use WooCommerce\PayPalCommerce\WcGateway\Checkout\CheckoutPayPalAddressPreset; use WooCommerce\PayPalCommerce\WcGateway\Checkout\DisableGateways; +use WooCommerce\PayPalCommerce\WcGateway\Cli\SettingsCommand; use WooCommerce\PayPalCommerce\WcGateway\Endpoint\ReturnUrlEndpoint; use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CardButtonGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\GatewayRepository; use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXO; -use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOEndpoint; use WooCommerce\PayPalCommerce\WcGateway\Gateway\OXXO\OXXOGateway; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayPalGateway; use WooCommerce\PayPalCommerce\WcGateway\FraudNet\FraudNet; @@ -95,7 +96,8 @@ return array( $environment, $payment_token_repository, $logger, - $api_shop_country + $api_shop_country, + $container->get( 'api.endpoint.order' ) ); }, 'wcgateway.credit-card-gateway' => static function ( ContainerInterface $container ): CreditCardGateway { @@ -386,8 +388,6 @@ return array( $state = $container->get( 'onboarding.state' ); assert( $state instanceof State ); - $messages_disclaimers = $container->get( 'button.helper.messages-disclaimers' ); - $dcc_applies = $container->get( 'api.helpers.dccapplies' ); assert( $dcc_applies instanceof DccApplies ); @@ -610,40 +610,6 @@ return array( 'requirements' => array(), 'gateway' => 'paypal', ), - 'vault_enabled' => array( - 'title' => __( 'Vaulting', 'woocommerce-paypal-payments' ), - 'type' => 'checkbox', - 'desc_tip' => true, - 'label' => $container->get( 'button.helper.vaulting-label' ), - 'description' => __( 'Allow registered buyers to save PayPal and Credit Card accounts. Allow Subscription renewals.', 'woocommerce-paypal-payments' ), - 'default' => false, - 'screens' => array( - State::STATE_ONBOARDED, - ), - 'requirements' => array(), - 'gateway' => array( 'paypal', 'dcc' ), - 'input_class' => $container->get( 'wcgateway.helper.vaulting-scope' ) ? array() : array( 'ppcp-disabled-checkbox' ), - ), - 'subscription_behavior_when_vault_fails' => array( - 'title' => __( 'Subscription capture behavior if Vault fails', 'woocommerce-paypal-payments' ), - 'type' => 'select', - 'classes' => $subscription_helper->plugin_is_active() ? array() : array( 'hide' ), - 'input_class' => array( 'wc-enhanced-select' ), - 'default' => 'void_auth', - 'desc_tip' => true, - 'description' => __( 'By default, subscription payments are captured only when saving the payment method was successful. Without a saved payment method, automatic renewal payments are not possible.', 'woocommerce-paypal-payments' ), - 'description_with_tip' => __( 'Determines whether authorized payments for subscription orders are captured or voided if there is no saved payment method. This only applies when the intent Capture is used for the subscription order.', 'woocommerce-paypal-payments' ), - 'options' => array( - 'void_auth' => __( 'Void authorization & fail the order/subscription', 'woocommerce-paypal-payments' ), - 'capture_auth' => __( 'Capture authorized payment & set subscription to Manual Renewal', 'woocommerce-paypal-payments' ), - 'capture_auth_ignore' => __( 'Capture authorized payment & disregard missing payment method', 'woocommerce-paypal-payments' ), - ), - 'screens' => array( - State::STATE_ONBOARDED, - ), - 'requirements' => array(), - 'gateway' => array( 'paypal', 'dcc' ), - ), 'card_billing_data_mode' => array( 'title' => __( 'Card billing data handling', 'woocommerce-paypal-payments' ), 'type' => 'select', @@ -737,6 +703,25 @@ return array( ), 'gateway' => 'dcc', ), + 'vault_enabled_dcc' => array( + 'title' => __( 'Vaulting', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'desc_tip' => true, + 'label' => sprintf( + // translators: %1$s and %2$s are the opening and closing of HTML tag. + __( 'Securely store your customers’ credit cards for a seamless checkout experience and subscription features. Payment methods are saved in the secure %1$sPayPal Vault%2$s.', 'woocommerce-paypal-payments' ), + '', + '' + ), + 'description' => __( 'Allow registered buyers to save Credit Card payments.', 'woocommerce-paypal-payments' ), + 'default' => false, + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'dcc', + 'input_class' => $container->get( 'wcgateway.helper.vaulting-scope' ) ? array() : array( 'ppcp-disabled-checkbox' ), + ), '3d_secure_heading' => array( 'heading' => __( '3D Secure', 'woocommerce-paypal-payments' ), 'type' => 'ppcp-heading', @@ -793,9 +778,85 @@ return array( ), 'gateway' => 'dcc', ), + 'paypal_saved_payments' => array( + 'heading' => __( 'Saved payments', 'woocommerce-paypal-payments' ), + 'description' => __( 'PayPal can save your customers’ payment methods.', 'woocommerce-paypal-payments' ), + 'type' => 'ppcp-heading', + 'screens' => array( + State::STATE_START, + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'paypal', + ), + 'subscriptions_mode' => array( + 'title' => __( 'Subscriptions Mode', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'class' => array(), + 'input_class' => array( 'wc-enhanced-select' ), + 'desc_tip' => true, + 'description' => __( 'Utilize PayPal Vaulting for flexible subscription processing with saved payment methods, create “PayPal Subscriptions” to bill customers at regular intervals, or disable PayPal for subscription-type products.', 'woocommerce-paypal-payments' ), + 'default' => 'vaulting_api', + 'options' => array( + 'vaulting_api' => __( 'PayPal Vaulting', 'woocommerce-paypal-payments' ), + 'subscriptions_api' => __( 'PayPal Subscriptions', 'woocommerce-paypal-payments' ), + 'disable_paypal_subscriptions' => __( 'Disable PayPal for subscriptions', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'paypal', + ), + 'vault_enabled' => array( + 'title' => __( 'Vaulting', 'woocommerce-paypal-payments' ), + 'type' => 'checkbox', + 'desc_tip' => true, + 'label' => sprintf( + // translators: %1$s and %2$s are the opening and closing of HTML tag. + __( 'Securely store your customers’ PayPal accounts for a seamless checkout experience. Payment methods are saved in the secure %1$sPayPal Vault%2$s.', 'woocommerce-paypal-payments' ), + '', + '' + ) . $container->get( 'button.helper.vaulting-label' ), + 'description' => __( 'Allow registered buyers to save PayPal payments.', 'woocommerce-paypal-payments' ), + 'default' => false, + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => 'paypal', + 'input_class' => $container->get( 'wcgateway.helper.vaulting-scope' ) ? array() : array( 'ppcp-disabled-checkbox' ), + ), + 'subscription_behavior_when_vault_fails' => array( + 'title' => __( 'Subscription capture behavior if Vault fails', 'woocommerce-paypal-payments' ), + 'type' => 'select', + 'classes' => $subscription_helper->plugin_is_active() ? array() : array( 'hide' ), + 'input_class' => array( 'wc-enhanced-select' ), + 'default' => 'void_auth', + 'desc_tip' => true, + 'description' => __( 'By default, subscription payments are captured only when saving the payment method was successful. Without a saved payment method, automatic renewal payments are not possible.', 'woocommerce-paypal-payments' ), + 'description_with_tip' => __( 'Determines whether authorized payments for subscription orders are captured or voided if there is no saved payment method. This only applies when the intent Capture is used for the subscription order.', 'woocommerce-paypal-payments' ), + 'options' => array( + 'void_auth' => __( 'Void authorization & fail the order/subscription', 'woocommerce-paypal-payments' ), + 'capture_auth' => __( 'Capture authorized payment & set subscription to Manual Renewal', 'woocommerce-paypal-payments' ), + 'capture_auth_ignore' => __( 'Capture authorized payment & disregard missing payment method', 'woocommerce-paypal-payments' ), + ), + 'screens' => array( + State::STATE_ONBOARDED, + ), + 'requirements' => array(), + 'gateway' => array( 'paypal' ), + ), ); - if ( ! defined( 'PPCP_FLAG_SUBSCRIPTION' ) || ! PPCP_FLAG_SUBSCRIPTION ) { + + if ( defined( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) && ! PPCP_FLAG_SUBSCRIPTIONS_API || ! $subscription_helper->plugin_is_active() ) { + unset( $fields['subscriptions_mode'] ); + } + + $billing_agreements_endpoint = $container->get( 'api.endpoint.billing-agreements' ); + if ( ! $billing_agreements_endpoint->reference_transaction_enabled() ) { unset( $fields['vault_enabled'] ); + unset( $fields['vault_enabled_dcc'] ); } /** @@ -1014,15 +1075,6 @@ return array( $container->get( 'woocommerce.logger.woocommerce' ) ); }, - 'wcgateway.endpoint.oxxo' => static function ( ContainerInterface $container ): OXXOEndpoint { - return new OXXOEndpoint( - $container->get( 'button.request-data' ), - $container->get( 'api.endpoint.order' ), - $container->get( 'api.factory.purchase-unit' ), - $container->get( 'api.factory.shipping-preference' ), - $container->get( 'woocommerce.logger.woocommerce' ) - ); - }, 'wcgateway.logging.is-enabled' => function ( ContainerInterface $container ) : bool { $settings = $container->get( 'wcgateway.settings' ); @@ -1045,13 +1097,7 @@ return array( }, 'button.helper.vaulting-label' => static function ( ContainerInterface $container ): string { - $vaulting_label = sprintf( - // translators: %1$s and %2$s are the opening and closing of HTML tag. - __( 'Enable saved cards, PayPal accounts, and subscription features on your store. Payment methods are saved in the secure %1$sPayPal Vault%2$s.', 'woocommerce-paypal-payments' ), - '', - '' - ); - + $vaulting_label = ''; if ( ! $container->get( 'wcgateway.helper.vaulting-scope' ) ) { $vaulting_label .= sprintf( // translators: %1$s and %2$s are the opening and closing of HTML tag. @@ -1219,6 +1265,11 @@ return array( return 'https://www.paypal.com/bizsignup/entry?country.x=DE&product=payment_methods&capabilities=PAY_UPON_INVOICE'; }, 'wcgateway.settings.connection.dcc-status-text' => static function ( ContainerInterface $container ): string { + $state = $container->get( 'onboarding.state' ); + if ( $state->current_state() < State::STATE_ONBOARDED ) { + return ''; + } + $dcc_product_status = $container->get( 'wcgateway.helper.dcc-product-status' ); assert( $dcc_product_status instanceof DCCProductStatus ); @@ -1252,6 +1303,11 @@ return array( ); }, 'wcgateway.settings.connection.pui-status-text' => static function ( ContainerInterface $container ): string { + $state = $container->get( 'onboarding.state' ); + if ( $state->current_state() < State::STATE_ONBOARDED ) { + return ''; + } + $pui_product_status = $container->get( 'wcgateway.pay-upon-invoice-product-status' ); assert( $pui_product_status instanceof PayUponInvoiceProductStatus ); @@ -1348,4 +1404,9 @@ return array( $container->get( 'wcgateway.is-fraudnet-enabled' ) ); }, + 'wcgateway.cli.settings.command' => function( ContainerInterface $container ) : SettingsCommand { + return new SettingsCommand( + $container->get( 'wcgateway.settings' ) + ); + }, ); diff --git a/modules/ppcp-wc-gateway/src/Checkout/CheckoutPayPalAddressPreset.php b/modules/ppcp-wc-gateway/src/Checkout/CheckoutPayPalAddressPreset.php index 77f3e6dc1..5dae650ca 100644 --- a/modules/ppcp-wc-gateway/src/Checkout/CheckoutPayPalAddressPreset.php +++ b/modules/ppcp-wc-gateway/src/Checkout/CheckoutPayPalAddressPreset.php @@ -137,7 +137,7 @@ class CheckoutPayPalAddressPreset { } $shipping = null; - foreach ( $this->session_handler->order()->purchase_units() as $unit ) { + foreach ( $order->purchase_units() as $unit ) { $shipping = $unit->shipping(); if ( $shipping ) { break; diff --git a/modules/ppcp-wc-gateway/src/Cli/SettingsCommand.php b/modules/ppcp-wc-gateway/src/Cli/SettingsCommand.php new file mode 100644 index 000000000..f67c7c6f1 --- /dev/null +++ b/modules/ppcp-wc-gateway/src/Cli/SettingsCommand.php @@ -0,0 +1,70 @@ +settings = $settings; + } + + /** + * Updates the specified settings. + * + * ## OPTIONS + * + * + * : The setting key. + * + * + * : 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}'." ); + } +} diff --git a/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php index 3357bd5e4..2f994bf46 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/CardButtonGateway.php @@ -174,12 +174,7 @@ class CardButtonGateway extends \WC_Payment_Gateway { if ( $this->onboarded ) { $this->supports = array( 'refunds' ); } - if ( - defined( 'PPCP_FLAG_SUBSCRIPTION' ) - && PPCP_FLAG_SUBSCRIPTION - && $this->gateways_enabled() - && $this->vault_setting_enabled() - ) { + if ( $this->gateways_enabled() ) { $this->supports = array( 'refunds', 'products', diff --git a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php index 5d2cc322f..7236e687b 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/CreditCardGateway.php @@ -174,26 +174,30 @@ class CreditCardGateway extends \WC_Payment_Gateway_CC { if ( $state->current_state() === State::STATE_ONBOARDED ) { $this->supports = array( 'refunds' ); } - if ( - defined( 'PPCP_FLAG_SUBSCRIPTION' ) - && PPCP_FLAG_SUBSCRIPTION - && $this->gateways_enabled() - && $this->vault_setting_enabled() - ) { + if ( $this->config->has( 'dcc_enabled' ) && $this->config->get( 'dcc_enabled' ) ) { $this->supports = array( 'refunds', 'products', - 'subscriptions', - 'subscription_cancellation', - 'subscription_suspension', - 'subscription_reactivation', - 'subscription_amount_changes', - 'subscription_date_changes', - 'subscription_payment_method_change', - 'subscription_payment_method_change_customer', - 'subscription_payment_method_change_admin', - 'multiple_subscriptions', ); + + if ( + ( $this->config->has( 'vault_enabled_dcc' ) && $this->config->get( 'vault_enabled_dcc' ) ) + || ( $this->config->has( 'subscriptions_mode' ) && $this->config->get( 'subscriptions_mode' ) === 'subscriptions_api' ) + ) { + array_push( + $this->supports, + 'subscriptions', + 'subscription_cancellation', + 'subscription_suspension', + 'subscription_reactivation', + 'subscription_amount_changes', + 'subscription_date_changes', + 'subscription_payment_method_change', + 'subscription_payment_method_change_customer', + 'subscription_payment_method_change_admin', + 'multiple_subscriptions' + ); + } } $this->method_title = __( diff --git a/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXO.php b/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXO.php index da512fd2c..243588254 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXO.php +++ b/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXO.php @@ -228,24 +228,5 @@ class OXXO { true ); } - - wp_localize_script( - 'ppcp-oxxo', - 'OXXOConfig', - array( - 'oxxo_endpoint' => \WC_AJAX::get_endpoint( 'ppc-oxxo' ), - 'oxxo_nonce' => wp_create_nonce( 'ppc-oxxo' ), - 'error' => array( - 'generic' => __( - 'Something went wrong. Please try again or choose another payment source.', - 'woocommerce-paypal-payments' - ), - 'js_validation' => __( - 'Required form fields are not filled or invalid.', - 'woocommerce-paypal-payments' - ), - ), - ) - ); } } diff --git a/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXOEndpoint.php b/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXOEndpoint.php deleted file mode 100644 index 64d8e713f..000000000 --- a/modules/ppcp-wc-gateway/src/Gateway/OXXO/OXXOEndpoint.php +++ /dev/null @@ -1,156 +0,0 @@ -request_data = $request_data; - $this->purchase_unit_factory = $purchase_unit_factory; - $this->shipping_preference_factory = $shipping_preference_factory; - $this->order_endpoint = $order_endpoint; - $this->logger = $logger; - } - - /** - * The nonce - * - * @return string - */ - public static function nonce(): string { - return 'ppc-oxxo'; - } - - /** - * Handles the request. - * - * @return bool - */ - public function handle_request(): bool { - $purchase_unit = $this->purchase_unit_factory->from_wc_cart(); - $payer_action = ''; - - try { - $shipping_preference = $this->shipping_preference_factory->from_state( - $purchase_unit, - 'checkout' - ); - - $order = $this->order_endpoint->create( array( $purchase_unit ), $shipping_preference ); - - $payment_source = array( - 'oxxo' => array( - 'name' => 'John Doe', - 'email' => 'foo@bar.com', - 'country_code' => 'MX', - ), - ); - - $payment_method = $this->order_endpoint->confirm_payment_source( $order->id(), $payment_source ); - - foreach ( $payment_method->links as $link ) { - if ( $link->rel === 'payer-action' ) { - $payer_action = $link->href; - } - } - } catch ( RuntimeException $exception ) { - $error = $exception->getMessage(); - - if ( is_a( $exception, PayPalApiException::class ) && is_array( $exception->details() ) ) { - $details = ''; - foreach ( $exception->details() as $detail ) { - $issue = $detail->issue ?? ''; - $field = $detail->field ?? ''; - $description = $detail->description ?? ''; - $details .= $issue . ' ' . $field . ' ' . $description . '
'; - } - - $error = $details; - } - - $this->logger->error( $error ); - wc_add_notice( $error, 'error' ); - - wp_send_json_error( 'Could not get OXXO payer action.' ); - return false; - } - - WC()->session->set( 'ppcp_payer_action', $payer_action ); - - wp_send_json_success( - array( 'payer_action' => $payer_action ) - ); - - return true; - } -} diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php index 0e71fa812..0a5835c31 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayPalGateway.php @@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Exception; use Psr\Log\LoggerInterface; use WC_Order; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\PaymentToken; use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; @@ -24,8 +25,11 @@ use WooCommerce\PayPalCommerce\Vaulting\PaymentTokenRepository; use WooCommerce\PayPalCommerce\WcGateway\Exception\GatewayGenericException; use WooCommerce\PayPalCommerce\WcGateway\FundingSource\FundingSourceRenderer; use WooCommerce\PayPalCommerce\WcGateway\Gateway\PayUponInvoice\PayUponInvoiceGateway; +use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderMetaTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\OrderProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\PaymentsStatusHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Processor\RefundProcessor; +use WooCommerce\PayPalCommerce\WcGateway\Processor\TransactionIdHandlingTrait; use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsRenderer; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; @@ -35,7 +39,7 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; */ class PayPalGateway extends \WC_Payment_Gateway { - use ProcessPaymentTrait, FreeTrialHandlerTrait, GatewaySettingsRendererTrait; + use ProcessPaymentTrait, FreeTrialHandlerTrait, GatewaySettingsRendererTrait, OrderMetaTrait, TransactionIdHandlingTrait, PaymentsStatusHandlingTrait; const ID = 'ppcp-gateway'; const INTENT_META_KEY = '_ppcp_paypal_intent'; @@ -150,6 +154,13 @@ class PayPalGateway extends \WC_Payment_Gateway { */ protected $api_shop_country; + /** + * The order endpoint. + * + * @var OrderEndpoint + */ + private $order_endpoint; + /** * PayPalGateway constructor. * @@ -165,8 +176,9 @@ class PayPalGateway extends \WC_Payment_Gateway { * @param string $page_id ID of the current PPCP gateway settings page, or empty if it is not such page. * @param Environment $environment The environment. * @param PaymentTokenRepository $payment_token_repository The payment token repository. - * @param LoggerInterface $logger The logger. + * @param LoggerInterface $logger The logger. * @param string $api_shop_country The api shop country. + * @param OrderEndpoint $order_endpoint The order endpoint. */ public function __construct( SettingsRenderer $settings_renderer, @@ -182,7 +194,8 @@ class PayPalGateway extends \WC_Payment_Gateway { Environment $environment, PaymentTokenRepository $payment_token_repository, LoggerInterface $logger, - string $api_shop_country + string $api_shop_country, + OrderEndpoint $order_endpoint ) { $this->id = self::ID; $this->settings_renderer = $settings_renderer; @@ -204,27 +217,31 @@ class PayPalGateway extends \WC_Payment_Gateway { if ( $this->onboarded ) { $this->supports = array( 'refunds', 'tokenization' ); } - if ( - defined( 'PPCP_FLAG_SUBSCRIPTION' ) - && PPCP_FLAG_SUBSCRIPTION - && $this->gateways_enabled() - && $this->vault_setting_enabled() - ) { + if ( $this->config->has( 'enabled' ) && $this->config->get( 'enabled' ) ) { $this->supports = array( 'refunds', 'products', - 'subscriptions', - 'subscription_cancellation', - 'subscription_suspension', - 'subscription_reactivation', - 'subscription_amount_changes', - 'subscription_date_changes', - 'subscription_payment_method_change', - 'subscription_payment_method_change_customer', - 'subscription_payment_method_change_admin', - 'multiple_subscriptions', - 'tokenization', ); + + if ( + ( $this->config->has( 'vault_enabled' ) && $this->config->get( 'vault_enabled' ) ) + || ( $this->config->has( 'subscriptions_mode' ) && $this->config->get( 'subscriptions_mode' ) === 'subscriptions_api' ) + ) { + array_push( + $this->supports, + 'tokenization', + 'subscriptions', + 'subscription_cancellation', + 'subscription_suspension', + 'subscription_reactivation', + 'subscription_amount_changes', + 'subscription_date_changes', + 'subscription_payment_method_change', + 'subscription_payment_method_change_customer', + 'subscription_payment_method_change_admin', + 'multiple_subscriptions' + ); + } } $this->method_title = $this->define_method_title(); @@ -250,6 +267,8 @@ class PayPalGateway extends \WC_Payment_Gateway { 'process_admin_options', ) ); + + $this->order_endpoint = $order_endpoint; } /** @@ -425,7 +444,13 @@ class PayPalGateway extends \WC_Payment_Gateway { } // 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 ) ) { $user_id = (int) $wc_order->get_customer_id(); $tokens = $this->payment_token_repository->all_for_user_id( $user_id ); @@ -467,6 +492,28 @@ class PayPalGateway extends \WC_Payment_Gateway { //phpcs:enable WordPress.Security.NonceVerification.Recommended try { + $paypal_subscription_id = WC()->session->get( 'ppcp_subscription_id' ) ?? ''; + if ( $paypal_subscription_id ) { + $order = $this->session_handler->order(); + $this->add_paypal_meta( $wc_order, $order, $this->environment ); + + $subscriptions = wcs_get_subscriptions_for_order( $order_id ); + foreach ( $subscriptions as $subscription ) { + $subscription->update_meta_data( 'ppcp_subscription', $paypal_subscription_id ); + $subscription->save(); + + $subscription->add_order_note( "PayPal subscription {$paypal_subscription_id} added." ); + } + + $transaction_id = $this->get_paypal_order_transaction_id( $order ); + if ( $transaction_id ) { + $this->update_transaction_id( $transaction_id, $wc_order ); + } + + $wc_order->payment_complete(); + return $this->handle_payment_success( $wc_order ); + } + if ( ! $this->order_processor->process( $wc_order ) ) { return $this->handle_payment_failure( $wc_order, diff --git a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php index 17a9b9df7..b9193910f 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php +++ b/modules/ppcp-wc-gateway/src/Gateway/PayUponInvoice/PayUponInvoice.php @@ -440,6 +440,7 @@ class PayUponInvoice { } if ( + // phpcs:ignore WordPress.Security.NonceVerification isset( $_GET['pay_for_order'] ) && $_GET['pay_for_order'] === 'true' && ! $this->pui_helper->is_pay_for_order_ready_for_pui() ) { diff --git a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php index da17f822b..1772b2838 100644 --- a/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php +++ b/modules/ppcp-wc-gateway/src/Gateway/ProcessPaymentTrait.php @@ -33,19 +33,6 @@ trait ProcessPaymentTrait { return false; } - /** - * Checks if vault setting is enabled. - * - * @return bool Whether vault settings are enabled or not. - * @throws \WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException When a setting hasn't been found. - */ - protected function vault_setting_enabled(): bool { - if ( $this->config->has( 'vault_enabled' ) && $this->config->get( 'vault_enabled' ) ) { - return true; - } - return false; - } - /** * Scheduled the vaulted payment check. * @@ -94,8 +81,9 @@ trait ProcessPaymentTrait { wc_add_notice( $error->getMessage(), 'error' ); return array( - 'result' => 'failure', - 'redirect' => wc_get_checkout_url(), + 'result' => 'failure', + 'redirect' => wc_get_checkout_url(), + 'errorMessage' => $error->getMessage(), ); } diff --git a/modules/ppcp-wc-gateway/src/Helper/SettingsStatus.php b/modules/ppcp-wc-gateway/src/Helper/SettingsStatus.php index 02f5e7082..d1f848d19 100644 --- a/modules/ppcp-wc-gateway/src/Helper/SettingsStatus.php +++ b/modules/ppcp-wc-gateway/src/Helper/SettingsStatus.php @@ -98,6 +98,9 @@ class SettingsStatus { if ( 'pay-now' === $location ) { $location = 'checkout'; } + if ( 'checkout-block' === $location ) { + $location = 'checkout-block-express'; + } return $location; } diff --git a/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php b/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php index ce8d48583..6cc9c7ce4 100644 --- a/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php +++ b/modules/ppcp-wc-gateway/src/Processor/OrderProcessor.php @@ -162,8 +162,13 @@ class OrderProcessor { * * @return bool */ - public function process( WC_Order $wc_order ): bool { - $order = $this->session_handler->order(); + public function process( \WC_Order $wc_order ): bool { + // 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 ) { $order_id = $wc_order->get_meta( PayPalGateway::ORDER_ID_META_KEY ); if ( ! $order_id ) { diff --git a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php index 3ece9ba40..6ac84583a 100644 --- a/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php +++ b/modules/ppcp-wc-gateway/src/Settings/Fields/connection-tab-fields.php @@ -408,7 +408,6 @@ return function ( ContainerInterface $container, array $fields ): array { 'gateway' => Settings::CONNECTION_TAB_ID, 'input_class' => $container->get( 'wcgateway.settings.should-disable-fraudnet-checkbox' ) ? array( 'ppcp-disabled-checkbox' ) : array(), ), - 'credentials_integration_configuration_heading' => array( 'heading' => __( 'General integration configuration', 'woocommerce-paypal-payments' ), 'type' => 'ppcp-heading', diff --git a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php index 9bd20ba37..0756e73f9 100644 --- a/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php +++ b/modules/ppcp-wc-gateway/src/Settings/SettingsListener.php @@ -225,11 +225,13 @@ class SettingsListener { $token = $this->bearer->bearer(); if ( ! $token->vaulting_available() ) { $this->settings->set( 'vault_enabled', false ); + $this->settings->set( 'vault_enabled_dcc', false ); $this->settings->persist(); return; } } catch ( RuntimeException $exception ) { $this->settings->set( 'vault_enabled', false ); + $this->settings->set( 'vault_enabled_dcc', false ); $this->settings->persist(); throw $exception; @@ -336,6 +338,11 @@ class SettingsListener { $this->dcc_status_cache->delete( DCCProductStatus::DCC_STATUS_CACHE_KEY ); } + $ppcp_reference_transaction_enabled = get_transient( 'ppcp_reference_transaction_enabled' ) ?? ''; + if ( $ppcp_reference_transaction_enabled ) { + delete_transient( 'ppcp_reference_transaction_enabled' ); + } + $redirect_url = false; if ( $credentials_change_status && self::CREDENTIALS_UNCHANGED !== $credentials_change_status ) { $redirect_url = $this->get_onboarding_redirect_url(); diff --git a/modules/ppcp-wc-gateway/src/WCGatewayModule.php b/modules/ppcp-wc-gateway/src/WCGatewayModule.php index f19a244e6..f49fb8f08 100644 --- a/modules/ppcp-wc-gateway/src/WCGatewayModule.php +++ b/modules/ppcp-wc-gateway/src/WCGatewayModule.php @@ -13,6 +13,7 @@ use Psr\Log\LoggerInterface; use Throwable; use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException; use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache; +use WooCommerce\PayPalCommerce\Subscription\Helper\SubscriptionHelper; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use WC_Order; @@ -322,14 +323,6 @@ class WCGatewayModule implements ModuleInterface { 2 ); - add_action( - 'wc_ajax_ppc-oxxo', - static function () use ( $c ) { - $endpoint = $c->get( 'wcgateway.endpoint.oxxo' ); - $endpoint->handle_request(); - } - ); - add_action( 'woocommerce_order_status_changed', static function ( int $order_id, string $from, string $to ) use ( $c ) { @@ -380,6 +373,13 @@ class WCGatewayModule implements ModuleInterface { 10, 3 ); + + if ( defined( 'WP_CLI' ) && WP_CLI ) { + \WP_CLI::add_command( + 'pcp settings', + $c->get( 'wcgateway.cli.settings.command' ) + ); + } } /** diff --git a/modules/ppcp-webhooks/services.php b/modules/ppcp-webhooks/services.php index 83eabf144..ce0338228 100644 --- a/modules/ppcp-webhooks/services.php +++ b/modules/ppcp-webhooks/services.php @@ -14,19 +14,24 @@ use Psr\Log\LoggerInterface; use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint; use WooCommerce\PayPalCommerce\ApiClient\Entity\Webhook; use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory; +use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Webhooks\Endpoint\ResubscribeEndpoint; use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulateEndpoint; use WooCommerce\PayPalCommerce\Webhooks\Endpoint\SimulationStateEndpoint; +use WooCommerce\PayPalCommerce\Webhooks\Handler\BillingPlanPricingChangeActivated; +use WooCommerce\PayPalCommerce\Webhooks\Handler\BillingPlanUpdated; +use WooCommerce\PayPalCommerce\Webhooks\Handler\BillingSubscriptionCancelled; +use WooCommerce\PayPalCommerce\Webhooks\Handler\CatalogProductUpdated; use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutOrderApproved; use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutOrderCompleted; use WooCommerce\PayPalCommerce\Webhooks\Handler\CheckoutPaymentApprovalReversed; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureCompleted; -use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureDenied; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCapturePending; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureRefunded; use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentCaptureReversed; use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface; -use WooCommerce\PayPalCommerce\Webhooks\Handler\VaultCreditCardCreated; +use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentSaleCompleted; +use WooCommerce\PayPalCommerce\Webhooks\Handler\PaymentSaleRefunded; use WooCommerce\PayPalCommerce\Webhooks\Handler\VaultPaymentTokenCreated; use WooCommerce\PayPalCommerce\Webhooks\Handler\VaultPaymentTokenDeleted; use WooCommerce\PayPalCommerce\Webhooks\Status\Assets\WebhooksStatusPageAssets; @@ -86,6 +91,12 @@ return array( new VaultPaymentTokenCreated( $logger, $prefix, $authorized_payments_processor, $payment_token_factory ), new VaultPaymentTokenDeleted( $logger ), new PaymentCapturePending( $logger ), + new PaymentSaleCompleted( $logger ), + new PaymentSaleRefunded( $logger ), + new BillingSubscriptionCancelled( $logger ), + new BillingPlanPricingChangeActivated( $logger ), + new CatalogProductUpdated( $logger ), + new BillingPlanUpdated( $logger ), ); }, @@ -116,7 +127,12 @@ return array( $endpoint = $container->get( 'api.endpoint.webhook' ); assert( $endpoint instanceof WebhookEndpoint ); - return $endpoint->list(); + $state = $container->get( 'onboarding.state' ); + if ( $state->current_state() >= State::STATE_ONBOARDED ) { + return $endpoint->list(); + } + + return array(); }, 'webhook.status.registered-webhooks-data' => function( ContainerInterface $container ) : array { diff --git a/modules/ppcp-webhooks/src/Endpoint/ResubscribeEndpoint.php b/modules/ppcp-webhooks/src/Endpoint/ResubscribeEndpoint.php index 9fe20a1ed..804f9e306 100644 --- a/modules/ppcp-webhooks/src/Endpoint/ResubscribeEndpoint.php +++ b/modules/ppcp-webhooks/src/Endpoint/ResubscribeEndpoint.php @@ -58,6 +58,11 @@ class ResubscribeEndpoint { * Handles the incoming request. */ public function handle_request() { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'Not admin.', 403 ); + return false; + } + try { // Validate nonce. $this->request_data->read_request( $this->nonce() ); diff --git a/modules/ppcp-webhooks/src/Endpoint/SimulateEndpoint.php b/modules/ppcp-webhooks/src/Endpoint/SimulateEndpoint.php index f6b2cf17e..869c8cbdc 100644 --- a/modules/ppcp-webhooks/src/Endpoint/SimulateEndpoint.php +++ b/modules/ppcp-webhooks/src/Endpoint/SimulateEndpoint.php @@ -61,6 +61,11 @@ class SimulateEndpoint { * Handles the incoming request. */ public function handle_request() { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'Not admin.', 403 ); + return false; + } + try { // Validate nonce. $this->request_data->read_request( $this->nonce() ); diff --git a/modules/ppcp-webhooks/src/Endpoint/SimulationStateEndpoint.php b/modules/ppcp-webhooks/src/Endpoint/SimulationStateEndpoint.php index 089722ebc..17a4d5cbf 100644 --- a/modules/ppcp-webhooks/src/Endpoint/SimulationStateEndpoint.php +++ b/modules/ppcp-webhooks/src/Endpoint/SimulationStateEndpoint.php @@ -51,6 +51,11 @@ class SimulationStateEndpoint { * Handles the incoming request. */ public function handle_request() { + if ( ! current_user_can( 'manage_woocommerce' ) ) { + wp_send_json_error( 'Not admin.', 403 ); + return false; + } + try { $state = $this->simulation->get_state(); diff --git a/modules/ppcp-webhooks/src/Handler/BillingPlanPricingChangeActivated.php b/modules/ppcp-webhooks/src/Handler/BillingPlanPricingChangeActivated.php new file mode 100644 index 000000000..96d0cd3e4 --- /dev/null +++ b/modules/ppcp-webhooks/src/Handler/BillingPlanPricingChangeActivated.php @@ -0,0 +1,98 @@ +logger = $logger; + } + + /** + * The event types a handler handles. + * + * @return string[] + */ + public function event_types(): array { + return array( + 'BILLING.PLAN.PRICING-CHANGE.ACTIVATED', + ); + } + + /** + * Whether a handler is responsible for a given request or not. + * + * @param WP_REST_Request $request The request. + * + * @return bool + */ + public function responsible_for_request( WP_REST_Request $request ): bool { + return in_array( $request['event_type'], $this->event_types(), true ); + } + + /** + * Responsible for handling the request. + * + * @param WP_REST_Request $request The request. + * + * @return WP_REST_Response + */ + public function handle_request( WP_REST_Request $request ): WP_REST_Response { + $response = array( 'success' => false ); + if ( is_null( $request['resource'] ) ) { + return new WP_REST_Response( $response ); + } + + $plan_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); + if ( $plan_id && ! empty( $request['resource']['billing_cycles'] ) ) { + $this->logger->info( 'Starting stuff...' ); + $args = array( + // phpcs:ignore WordPress.DB.SlowDBQuery + 'meta_key' => 'ppcp_subscription_plan', + ); + + $products = wc_get_products( $args ); + if ( is_array( $products ) ) { + foreach ( $products as $product ) { + if ( $product->get_meta( 'ppcp_subscription_plan' )['id'] === $plan_id ) { + foreach ( $request['resource']['billing_cycles'] as $cycle ) { + if ( $cycle['tenure_type'] === 'REGULAR' ) { + $product->update_meta_data( '_subscription_price', $cycle['pricing_scheme']['fixed_price']['value'] ); + $product->save(); + } + } + } + } + } + } + + $response['success'] = true; + return new WP_REST_Response( $response ); + } +} diff --git a/modules/ppcp-webhooks/src/Handler/BillingPlanUpdated.php b/modules/ppcp-webhooks/src/Handler/BillingPlanUpdated.php new file mode 100644 index 000000000..1c6aa14c9 --- /dev/null +++ b/modules/ppcp-webhooks/src/Handler/BillingPlanUpdated.php @@ -0,0 +1,115 @@ +logger = $logger; + } + + /** + * The event types a handler handles. + * + * @return string[] + */ + public function event_types(): array { + return array( + 'BILLING.PLAN.UPDATED', + ); + } + + /** + * Whether a handler is responsible for a given request or not. + * + * @param WP_REST_Request $request The request. + * + * @return bool + */ + public function responsible_for_request( WP_REST_Request $request ): bool { + return in_array( $request['event_type'], $this->event_types(), true ); + } + + /** + * Responsible for handling the request. + * + * @param WP_REST_Request $request The request. + * + * @return WP_REST_Response + */ + public function handle_request( WP_REST_Request $request ): WP_REST_Response { + $response = array( 'success' => false ); + if ( is_null( $request['resource'] ) ) { + return new WP_REST_Response( $response ); + } + + $plan_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); + if ( $plan_id ) { + $products = wc_get_products( + array( + // phpcs:ignore WordPress.DB.SlowDBQuery + 'meta_key' => 'ppcp_subscription_product', + ) + ); + + if ( is_array( $products ) ) { + foreach ( $products as $product ) { + if ( $product->meta_exists( 'ppcp_subscription_plan' ) ) { + $plan_name = wc_clean( wp_unslash( $request['resource']['name'] ?? '' ) ); + if ( $plan_name !== $product->get_meta( '_ppcp_subscription_plan_name' ) ) { + $product->update_meta_data( '_ppcp_subscription_plan_name', $plan_name ); + $product->save(); + } + + $billing_cycles = wc_clean( wp_unslash( $request['resource']['billing_cycles'] ?? array() ) ); + if ( $billing_cycles ) { + $price = $billing_cycles[0]['pricing_scheme']['fixed_price']['value'] ?? ''; + if ( $price && round( $price, 2 ) !== round( $product->get_meta( '_subscription_price' ), 2 ) ) { + $product->update_meta_data( '_subscription_price', $price ); + $product->save(); + } + } + + $payment_preferences = wc_clean( wp_unslash( $request['resource']['payment_preferences'] ?? array() ) ); + if ( $payment_preferences ) { + $setup_fee = $payment_preferences['setup_fee']['value'] ?? ''; + if ( $setup_fee && round( $setup_fee, 2 ) !== round( $product->get_meta( '_subscription_sign_up_fee' ), 2 ) ) { + $product->update_meta_data( '_subscription_sign_up_fee', $setup_fee ); + $product->save(); + } + } + } + } + } + } + + $response['success'] = true; + return new WP_REST_Response( $response ); + } +} diff --git a/modules/ppcp-webhooks/src/Handler/BillingSubscriptionCancelled.php b/modules/ppcp-webhooks/src/Handler/BillingSubscriptionCancelled.php new file mode 100644 index 000000000..8b06f06fe --- /dev/null +++ b/modules/ppcp-webhooks/src/Handler/BillingSubscriptionCancelled.php @@ -0,0 +1,93 @@ +logger = $logger; + } + + /** + * The event types a handler handles. + * + * @return string[] + */ + public function event_types(): array { + return array( + 'BILLING.SUBSCRIPTION.CANCELLED', + ); + } + + /** + * Whether a handler is responsible for a given request or not. + * + * @param WP_REST_Request $request The request. + * + * @return bool + */ + public function responsible_for_request( WP_REST_Request $request ): bool { + return in_array( $request['event_type'], $this->event_types(), true ); + } + + /** + * Responsible for handling the request. + * + * @param WP_REST_Request $request The request. + * + * @return WP_REST_Response + */ + public function handle_request( WP_REST_Request $request ): WP_REST_Response { + $response = array( 'success' => false ); + if ( is_null( $request['resource'] ) ) { + return new WP_REST_Response( $response ); + } + + $subscription_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); + if ( $subscription_id ) { + $args = array( + // phpcs:ignore WordPress.DB.SlowDBQuery + 'meta_query' => array( + array( + 'key' => 'ppcp_subscription', + 'value' => $subscription_id, + 'compare' => '=', + ), + ), + ); + $subscriptions = wcs_get_subscriptions( $args ); + foreach ( $subscriptions as $subscription ) { + $subscription->update_status( 'cancelled' ); + } + } + + $response['success'] = true; + return new WP_REST_Response( $response ); + } +} diff --git a/modules/ppcp-webhooks/src/Handler/CatalogProductUpdated.php b/modules/ppcp-webhooks/src/Handler/CatalogProductUpdated.php new file mode 100644 index 000000000..21e5423c3 --- /dev/null +++ b/modules/ppcp-webhooks/src/Handler/CatalogProductUpdated.php @@ -0,0 +1,110 @@ +logger = $logger; + } + + /** + * The event types a handler handles. + * + * @return string[] + */ + public function event_types(): array { + return array( + 'CATALOG.PRODUCT.UPDATED', + ); + } + + /** + * Whether a handler is responsible for a given request or not. + * + * @param WP_REST_Request $request The request. + * + * @return bool + */ + public function responsible_for_request( WP_REST_Request $request ): bool { + return in_array( $request['event_type'], $this->event_types(), true ); + } + + /** + * Responsible for handling the request. + * + * @param WP_REST_Request $request The request. + * + * @return WP_REST_Response + */ + public function handle_request( WP_REST_Request $request ): WP_REST_Response { + $response = array( 'success' => false ); + if ( is_null( $request['resource'] ) ) { + return new WP_REST_Response( $response ); + } + + $product_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); + $name = wc_clean( wp_unslash( $request['resource']['name'] ?? '' ) ); + if ( $product_id && $name ) { + $args = array( + // phpcs:ignore WordPress.DB.SlowDBQuery + 'meta_key' => 'ppcp_subscription_product', + ); + $products = wc_get_products( $args ); + + if ( is_array( $products ) ) { + foreach ( $products as $product ) { + if ( + $product->meta_exists( 'ppcp_subscription_product' ) + && isset( $product->get_meta( 'ppcp_subscription_product' )['id'] ) + && $product->get_meta( 'ppcp_subscription_product' )['id'] === $product_id + && $product->get_title() !== $name + ) { + /** + * Suppress ArgumentTypeCoercion + * + * @psalm-suppress ArgumentTypeCoercion + */ + wp_update_post( + array( + 'ID' => $product->get_id(), + 'post_title' => $name, + ) + ); + + break; + } + } + } + } + + $response['success'] = true; + return new WP_REST_Response( $response ); + } +} diff --git a/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php new file mode 100644 index 000000000..6f5cb491d --- /dev/null +++ b/modules/ppcp-webhooks/src/Handler/PaymentSaleCompleted.php @@ -0,0 +1,105 @@ +logger = $logger; + } + + /** + * The event types a handler handles. + * + * @return string[] + */ + public function event_types(): array { + return array( 'PAYMENT.SALE.COMPLETED' ); + } + + /** + * Whether a handler is responsible for a given request or not. + * + * @param WP_REST_Request $request The request. + * + * @return bool + */ + public function responsible_for_request( WP_REST_Request $request ): bool { + return in_array( $request['event_type'], $this->event_types(), true ); + + } + + /** + * Responsible for handling the request. + * + * @param WP_REST_Request $request The request. + * + * @return WP_REST_Response + */ + public function handle_request( WP_REST_Request $request ): WP_REST_Response { + $response = array( 'success' => false ); + if ( is_null( $request['resource'] ) ) { + return new WP_REST_Response( $response ); + } + + $billing_agreement_id = wc_clean( wp_unslash( $request['resource']['billing_agreement_id'] ?? '' ) ); + if ( ! $billing_agreement_id ) { + $message = 'Could not retrieve billing agreement id for subscription.'; + $this->logger->warning( $message, array( 'request' => $request ) ); + $response['message'] = $message; + return new WP_REST_Response( $response ); + } + + $args = array( + // phpcs:ignore WordPress.DB.SlowDBQuery + 'meta_query' => array( + array( + 'key' => 'ppcp_subscription', + 'value' => $billing_agreement_id, + 'compare' => '=', + ), + ), + ); + $subscriptions = wcs_get_subscriptions( $args ); + foreach ( $subscriptions as $subscription ) { + $parent_order = wc_get_order( $subscription->get_parent() ); + $transaction_id = wc_clean( wp_unslash( $request['resource']['id'] ?? '' ) ); + if ( $transaction_id && is_string( $transaction_id ) && is_a( $parent_order, WC_Order::class ) ) { + $this->update_transaction_id( $transaction_id, $parent_order, $this->logger ); + } + } + + $response['success'] = true; + return new WP_REST_Response( $response ); + } +} diff --git a/modules/ppcp-webhooks/src/Handler/PaymentSaleRefunded.php b/modules/ppcp-webhooks/src/Handler/PaymentSaleRefunded.php new file mode 100644 index 000000000..ff3c1dc6a --- /dev/null +++ b/modules/ppcp-webhooks/src/Handler/PaymentSaleRefunded.php @@ -0,0 +1,136 @@ +logger = $logger; + } + + /** + * The event types a handler handles. + * + * @return string[] + */ + public function event_types(): array { + return array( 'PAYMENT.SALE.REFUNDED' ); + } + + /** + * Whether a handler is responsible for a given request or not. + * + * @param WP_REST_Request $request The request. + * + * @return bool + */ + public function responsible_for_request( WP_REST_Request $request ): bool { + return in_array( $request['event_type'], $this->event_types(), true ); + + } + + /** + * Responsible for handling the request. + * + * @param WP_REST_Request $request The request. + * + * @return WP_REST_Response + */ + public function handle_request( WP_REST_Request $request ): WP_REST_Response { + $response = array( 'success' => false ); + if ( is_null( $request['resource'] ) ) { + return new WP_REST_Response( $response ); + } + + $refund_id = (string) ( $request['resource']['id'] ?? '' ); + $transaction_id = $request['resource']['sale_id'] ?? ''; + $total_refunded_amount = $request['resource']['total_refunded_amount']['value'] ?? ''; + if ( ! $refund_id || ! $transaction_id || ! $total_refunded_amount ) { + return new WP_REST_Response( $response ); + } + + $args = array( + // phpcs:disable WordPress.DB.SlowDBQuery + 'meta_key' => '_transaction_id', + 'meta_value' => $transaction_id, + 'meta_compare' => '=', + // phpcs:enable + ); + $wc_orders = wc_get_orders( $args ); + + if ( ! is_array( $wc_orders ) ) { + return new WP_REST_Response( $response ); + } + + foreach ( $wc_orders as $wc_order ) { + $refund = wc_create_refund( + array( + 'order_id' => $wc_order->get_id(), + 'amount' => $total_refunded_amount, + ) + ); + + if ( $refund instanceof WP_Error ) { + $this->logger->warning( + sprintf( + // translators: %s is the order id. + __( 'Order %s could not be refunded', 'woocommerce-paypal-payments' ), + (string) $wc_order->get_id() + ) + ); + + $response['message'] = $refund->get_error_message(); + return new WP_REST_Response( $response ); + } + + $order_refunded_message = sprintf( + // translators: %1$s is the order id %2$s is the amount which has been refunded. + __( + 'Order %1$s has been refunded with %2$s through PayPal.', + 'woocommerce-paypal-payments' + ), + (string) $wc_order->get_id(), + (string) $total_refunded_amount + ); + $this->logger->info( $order_refunded_message ); + $wc_order->add_order_note( $order_refunded_message ); + + $this->update_transaction_id( $refund_id, $wc_order, $this->logger ); + $this->add_refund_to_meta( $wc_order, $refund_id ); + } + + $response['success'] = true; + return new WP_REST_Response( $response ); + } +} diff --git a/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php b/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php index 4a7631572..b228139e0 100644 --- a/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php +++ b/modules/ppcp-webhooks/src/IncomingWebhookEndpoint.php @@ -83,6 +83,14 @@ class IncomingWebhookEndpoint { */ private $last_webhook_event_storage; + /** + * Cached webhook verification results + * to avoid repeating requests when permission_callback is called multiple times. + * + * @var array + */ + private $verification_results = array(); + /** * IncomingWebhookEndpoint constructor. * @@ -160,7 +168,17 @@ class IncomingWebhookEndpoint { try { $event = $this->event_from_request( $request ); + } catch ( RuntimeException $exception ) { + $this->logger->error( 'Webhook parsing failed: ' . $exception->getMessage() ); + return false; + } + $cache_key = $event->id(); + if ( isset( $this->verification_results[ $cache_key ] ) ) { + return $this->verification_results[ $cache_key ]; + } + + try { if ( $this->simulation->is_simulation_event( $event ) ) { return true; } @@ -169,9 +187,11 @@ class IncomingWebhookEndpoint { if ( ! $result ) { $this->logger->error( 'Webhook verification failed.' ); } + $this->verification_results[ $cache_key ] = $result; return $result; } catch ( RuntimeException $exception ) { $this->logger->error( 'Webhook verification failed: ' . $exception->getMessage() ); + $this->verification_results[ $cache_key ] = false; return false; } } diff --git a/modules/ppcp-webhooks/src/WebhookModule.php b/modules/ppcp-webhooks/src/WebhookModule.php index e9cc66909..2458e5b4d 100644 --- a/modules/ppcp-webhooks/src/WebhookModule.php +++ b/modules/ppcp-webhooks/src/WebhookModule.php @@ -9,6 +9,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\Webhooks; +use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider; use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface; use Exception; @@ -126,8 +127,8 @@ class WebhookModule implements ModuleInterface { try { $webhooks = $container->get( 'webhook.status.registered-webhooks' ); - - if ( empty( $webhooks ) ) { + $state = $container->get( 'onboarding.state' ); + if ( empty( $webhooks ) && $state->current_state() >= State::STATE_ONBOARDED ) { $registrar = $container->get( 'webhook.registrar' ); assert( $registrar instanceof WebhookRegistrar ); diff --git a/package.json b/package.json index 9c8ee26c9..ccc69d50d 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,13 @@ { "name": "woocommerce-paypal-payments", - "version": "2.0.4", + "version": "2.1.0", "description": "WooCommerce PayPal Payments", "repository": "https://github.com/woocommerce/woocommerce-paypal-payments", "license": "GPL-2.0", "author": "WooCommerce", "scripts": { "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-wc-gateway": "cd modules/ppcp-wc-gateway && 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-compat": "cd modules/ppcp-compat && 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-wc-gateway": "cd modules/ppcp-wc-gateway && 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-uninstall": "cd modules/ppcp-uninstall && yarn run build", "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-wc-gateway": "cd modules/ppcp-wc-gateway && 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:composer-update": "ddev composer update && ddev composer update --lock", "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-tests": "ddev yarn playwright test", "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", "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", - "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": { "wp_org_slug": "woocommerce-paypal-payments" diff --git a/phpcs.xml.dist b/phpcs.xml.dist index e7c5693ae..8e32114f5 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -53,4 +53,5 @@ */vendor/* ./tests/* */resources/* + *.asset.php diff --git a/psalm.xml.dist b/psalm.xml.dist index 4be6e9690..092361998 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -32,8 +32,10 @@ + + diff --git a/readme.txt b/readme.txt index 8b9923c0e..e05cde449 100644 --- a/readme.txt +++ b/readme.txt @@ -2,9 +2,9 @@ Contributors: woocommerce, automattic, inpsyde Tags: woocommerce, paypal, payments, ecommerce, e-commerce, store, sales, sell, shop, shopping, cart, checkout Requires at least: 5.3 -Tested up to: 6.1 +Tested up to: 6.2 Requires PHP: 7.2 -Stable tag: 2.0.4 +Stable tag: 2.1.0 License: GPLv2 License URI: http://www.gnu.org/licenses/gpl-2.0.html @@ -81,6 +81,23 @@ Follow the steps below to connect the plugin to your PayPal account: == Changelog == += 2.1.0 - TBD = +* Fix - Performance issue #1182 +* Fix - Webhooks not registered when onboarding with manual credentials #1223 +* Fix - Boolean false type sent as empty value when setting cache #1313 +* Fix - Ajax vulnerabilities #1411 +* Enhancement - Save and display vaulted payment methods in WC Payment Token API #1059 +* Enhancement - Cache webhook verification results #1379 +* Enhancement - Refresh checkout totals after validation if needed #1294 +* Enhancement - Improve Divi and Elementor Pro compatibility #1254 +* Enhancement - Add MX and JP to ACDC #1415 +* Enhancement - Add fraudnet script to SGO filter #1366 +* Feature preview - Add express cart/checkout block #1346 +* Feature preview - Integrate PayPal Subscriptions API #1217 + += 2.0.5 - 2023-05-31 = +* Fix - Potential invalidation of merchant credentials #1339 + = 2.0.4 - 2023-04-03 = * Fix - Allow Pay Later in mini-cart #1221 * Fix - Duplicated auth error when credentials become wrong #1229 diff --git a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php index 82d0ce881..5041bf16a 100644 --- a/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php +++ b/tests/PHPUnit/ApiClient/Endpoint/OrderEndpointTest.php @@ -647,7 +647,7 @@ class OrderEndpointTest extends TestCase $intent = 'CAPTURE'; $logger = Mockery::mock(LoggerInterface::class); - $logger->shouldReceive('log'); + $logger->shouldReceive('warning'); $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $subscription_helper = Mockery::mock(SubscriptionHelper::class); @@ -742,7 +742,7 @@ class OrderEndpointTest extends TestCase $intent = 'CAPTURE'; $logger = Mockery::mock(LoggerInterface::class); - $logger->shouldReceive('log'); + $logger->shouldReceive('warning'); $logger->shouldReceive('debug'); $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); @@ -882,7 +882,7 @@ class OrderEndpointTest extends TestCase $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $applicationContextRepository ->expects('current_context') - ->with(Matchers::identicalTo(ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING)) + ->with(ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING, ApplicationContext::USER_ACTION_CONTINUE) ->andReturn($applicationContext); $subscription_helper = Mockery::mock(SubscriptionHelper::class); $subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true); @@ -985,7 +985,7 @@ class OrderEndpointTest extends TestCase $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $applicationContextRepository ->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); $subscription_helper = Mockery::mock(SubscriptionHelper::class); $subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true); @@ -1065,7 +1065,7 @@ class OrderEndpointTest extends TestCase $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $applicationContextRepository ->expects('current_context') - ->with(Matchers::identicalTo(ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING)) + ->with(ApplicationContext::SHIPPING_PREFERENCE_NO_SHIPPING, ApplicationContext::USER_ACTION_CONTINUE) ->andReturn($applicationContext); $subscription_helper = Mockery::mock(SubscriptionHelper::class); $subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true); @@ -1156,7 +1156,7 @@ class OrderEndpointTest extends TestCase $applicationContextRepository = Mockery::mock(ApplicationContextRepository::class); $applicationContextRepository ->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); $subscription_helper = Mockery::mock(SubscriptionHelper::class); $subscription_helper->shouldReceive('cart_contains_subscription')->andReturn(true); diff --git a/tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php b/tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php index 72c664c91..54be500fc 100644 --- a/tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php +++ b/tests/PHPUnit/ApiClient/Factory/PurchaseUnitFactoryTest.php @@ -294,7 +294,7 @@ class PurchaseUnitFactoryTest extends TestCase $shippingFactory = Mockery::mock(ShippingFactory::class); $shippingFactory ->expects('from_wc_customer') - ->with($wcCustomer) + ->with($wcCustomer, false) ->andReturn($shipping); $paymentsFacory = Mockery::mock(PaymentsFactory::class); $testee = new PurchaseUnitFactory( diff --git a/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php b/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php index 4e6d82a6c..d2fc1a4fa 100644 --- a/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php +++ b/tests/PHPUnit/Button/Endpoint/CreateOrderEndpointTest.php @@ -167,6 +167,8 @@ class CreateOrderEndpointTest extends TestCase false, CardBillingMode::MINIMAL_INPUT, false, + ['checkout'], + false, new NullLogger() ); return array($payer_factory, $testee); diff --git a/tests/PHPUnit/TestCase.php b/tests/PHPUnit/TestCase.php index f13497885..2289dbc20 100644 --- a/tests/PHPUnit/TestCase.php +++ b/tests/PHPUnit/TestCase.php @@ -33,6 +33,11 @@ class TestCase extends \PHPUnit\Framework\TestCase }); when('get_plugin_data')->justReturn(['Version' => '1.0']); when('plugin_basename')->justReturn('woocommerce-paypal-payments/woocommerce-paypal-payments.php'); + when('get_transient')->returnArg(); + + when('wc_clean')->returnArg(); + when('get_transient')->returnArg(); + when('delete_transient')->returnArg(); setUp(); } diff --git a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php index 26107abd0..cfe7a2eec 100644 --- a/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php +++ b/tests/PHPUnit/WcGateway/Gateway/WcGatewayTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace WooCommerce\PayPalCommerce\WcGateway\Gateway; use Psr\Log\LoggerInterface; +use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint; use WooCommerce\PayPalCommerce\Onboarding\Environment; use WooCommerce\PayPalCommerce\Onboarding\State; use WooCommerce\PayPalCommerce\Session\SessionHandler; @@ -39,6 +40,7 @@ class WcGatewayTest extends TestCase private $paymentTokenRepository; private $logger; private $apiShopCountry; + private $orderEndpoint; public function setUp(): void { parent::setUp(); @@ -64,6 +66,7 @@ class WcGatewayTest extends TestCase ['venmo' => 'Venmo', 'paylater' => 'Pay Later', 'blik' => 'BLIK'] ); $this->apiShopCountry = 'DE'; + $this->orderEndpoint = Mockery::mock(OrderEndpoint::class); $this->onboardingState->shouldReceive('current_state')->andReturn(State::STATE_ONBOARDED); @@ -95,7 +98,8 @@ class WcGatewayTest extends TestCase $this->environment, $this->paymentTokenRepository, $this->logger, - $this->apiShopCountry + $this->apiShopCountry, + $this->orderEndpoint ); } @@ -136,6 +140,9 @@ class WcGatewayTest extends TestCase when('WC')->justReturn($woocommerce); $woocommerce->cart = $cart; $cart->shouldReceive('empty_cart'); + $session = Mockery::mock(\WC_Session::class); + $woocommerce->session = $session; + $session->shouldReceive('get'); $result = $testee->process_payment($orderId); @@ -164,12 +171,17 @@ class WcGatewayTest extends TestCase expect('wc_add_notice'); + $result = $testee->process_payment($orderId); + + $this->assertArrayHasKey('errorMessage', $result); + unset($result['errorMessage']); + $this->assertEquals( [ 'result' => 'failure', - 'redirect' => $redirectUrl + 'redirect' => $redirectUrl, ], - $testee->process_payment($orderId) + $result ); } @@ -203,11 +215,21 @@ class WcGatewayTest extends TestCase when('wc_get_checkout_url') ->justReturn($redirectUrl); - $result = $testee->process_payment($orderId); - $this->assertEquals( - [ - 'result' => 'failure', - 'redirect' => $redirectUrl + $woocommerce = Mockery::mock(\WooCommerce::class); + when('WC')->justReturn($woocommerce); + $session = Mockery::mock(\WC_Session::class); + $woocommerce->session = $session; + $session->shouldReceive('get'); + + $result = $testee->process_payment($orderId); + + $this->assertArrayHasKey('errorMessage', $result); + unset($result['errorMessage']); + + $this->assertEquals( + [ + 'result' => 'failure', + 'redirect' => $redirectUrl, ], $result ); diff --git a/tests/playwright/place-order.spec.js b/tests/playwright/place-order.spec.js index c1701fef3..b0028fafa 100644 --- a/tests/playwright/place-order.spec.js +++ b/tests/playwright/place-order.spec.js @@ -1,101 +1,149 @@ const {test, expect} = require('@playwright/test'); +const {serverExec} = require("./utils/server"); +const {fillCheckoutForm, expectOrderReceivedPage} = require("./utils/checkout"); +const {openPaypalPopup, loginIntoPaypal, waitForPaypalShippingList, completePaypalPayment} = require("./utils/paypal-popup"); const { - CUSTOMER_EMAIL, - CUSTOMER_PASSWORD, CREDIT_CARD_NUMBER, CREDIT_CARD_EXPIRATION, CREDIT_CARD_CVV, PRODUCT_URL, + PRODUCT_ID, + CHECKOUT_URL, + CHECKOUT_PAGE_ID, + BLOCK_CHECKOUT_URL, + BLOCK_CHECKOUT_PAGE_ID, + BLOCK_CART_URL, } = process.env; -async function fillCheckoutForm(page) { - await page.fill('#billing_first_name', 'John'); - await page.fill('#billing_last_name', 'Doe'); - await page.selectOption('select#billing_country', 'DE'); - await page.fill('#billing_address_1', 'Badensche Str. 24'); - await page.fill('#billing_postcode', '10715'); - await page.fill('#billing_city', '10715'); - await page.fill('#billing_phone', '1234567890'); - await page.fill('#billing_email', CUSTOMER_EMAIL); +async function completeBlockContinuation(page) { + await expect(page.locator('#radio-control-wc-payment-method-options-ppcp-gateway')).toBeChecked(); - const differentShippingLocator = page.locator('[name="ship_to_different_address"]'); - if (await differentShippingLocator.count() > 0) { - await differentShippingLocator.uncheck(); - } + await expect(page.locator('.component-frame')).toHaveCount(0); - const termsLocator = page.locator('[name="terms"]'); - if (await termsLocator.count() > 0) { - await termsLocator.check(); - } + await page.locator('.wc-block-components-checkout-place-order-button').click(); + + await page.waitForNavigation(); + + await expectOrderReceivedPage(page); } -async function openPaypalPopup(page) { - await page.locator('.component-frame').scrollIntoViewIfNeeded(); +test.describe('Classic checkout', () => { + test.beforeAll(async ({ browser }) => { + await serverExec('wp option update woocommerce_checkout_page_id ' + CHECKOUT_PAGE_ID); + }); - const [popup] = await Promise.all([ - page.waitForEvent('popup'), - page.frameLocator('.component-frame').locator('[data-funding-source="paypal"]').click(), - ]); + test('PayPal button place order from Product page', async ({page}) => { + await page.goto(PRODUCT_URL); - await popup.waitForLoadState(); + const popup = await openPaypalPopup(page); - return popup; -} + await loginIntoPaypal(popup); -async function loginIntoPaypal(popup) { - await popup.click("text=Log in"); - await popup.fill('[name="login_email"]', CUSTOMER_EMAIL); - await popup.locator('#btnNext').click(); - await popup.fill('[name="login_password"]', CUSTOMER_PASSWORD); - await popup.locator('#btnLogin').click(); -} + await completePaypalPayment(popup); -test('PayPal button place order from Product page', async ({page}) => { + await fillCheckoutForm(page); - await page.goto(PRODUCT_URL); + await Promise.all([ + page.waitForNavigation(), + page.locator('#place_order').click(), + ]); - const popup = await openPaypalPopup(page); + await expectOrderReceivedPage(page); + }); - await loginIntoPaypal(popup); + 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 popup.locator('#payment-submit-btn').click(); + await page.goto(CHECKOUT_URL); + await fillCheckoutForm(page); - await fillCheckoutForm(page); + await page.click("text=Credit Cards"); - await Promise.all([ - page.waitForNavigation(), - page.locator('#place_order').click(), - ]); + const creditCardNumber = page.frameLocator('#braintree-hosted-field-number').locator('#credit-card-number'); + await creditCardNumber.fill(CREDIT_CARD_NUMBER); - const title = await page.locator('.entry-title'); - await expect(title).toHaveText('Order received'); + 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); - await page.locator('.single_add_to_cart_button').click(); + test('PayPal express block checkout', async ({page}) => { + await page.goto('?add-to-cart=' + PRODUCT_ID); - await page.goto('/checkout/'); - await fillCheckoutForm(page); + await page.goto(BLOCK_CHECKOUT_URL) - await page.click("text=Credit Cards"); + const popup = await openPaypalPopup(page); - const creditCardNumber = page.frameLocator('#braintree-hosted-field-number').locator('#credit-card-number'); - await creditCardNumber.fill(CREDIT_CARD_NUMBER); + await loginIntoPaypal(popup); - const expirationDate = page.frameLocator('#braintree-hosted-field-expirationDate').locator('#expiration'); - await expirationDate.fill(CREDIT_CARD_EXPIRATION); + await completePaypalPayment(popup); - const cvv = page.frameLocator('#braintree-hosted-field-cvv').locator('#cvv'); - await cvv.fill(CREDIT_CARD_CVV); + await completeBlockContinuation(page); + }); - await Promise.all([ - page.waitForNavigation(), - page.locator('.ppcp-dcc-order-button').click(), - ]); + test('PayPal express block cart', async ({page}) => { + await page.goto(BLOCK_CART_URL + '?add-to-cart=' + PRODUCT_ID) - const title = await page.locator('.entry-title'); - await expect(title).toHaveText('Order received'); + const popup = await openPaypalPopup(page); + + 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); + }); + }); }); diff --git a/tests/playwright/subscriptions-api.spec.js b/tests/playwright/subscriptions-api.spec.js new file mode 100644 index 000000000..9d5bce889 --- /dev/null +++ b/tests/playwright/subscriptions-api.spec.js @@ -0,0 +1,311 @@ +const {test, expect} = require('@playwright/test'); +const {loginAsAdmin, loginAsCustomer} = require('./utils/user'); +const {openPaypalPopup, loginIntoPaypal, completePaypalPayment} = require("./utils/paypal-popup"); +const {fillCheckoutForm, expectOrderReceivedPage} = require("./utils/checkout"); +const { + AUTHORIZATION, + SUBSCRIPTION_URL, + CHECKOUT_URL, + CART_URL, +} = process.env; + +async function purchaseSubscriptionFromCart(page) { + await loginAsCustomer(page); + await page.goto(SUBSCRIPTION_URL); + await page.click("text=Sign up now"); + await page.goto(CART_URL); + + const popup = await openPaypalPopup(page); + await loginIntoPaypal(popup); + + await popup.getByText('Continue', { exact: true }).click(); + await popup.locator('#confirmButtonTop').click(); + + await fillCheckoutForm(page); + + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Sign up now').click(), + ]); + + await expectOrderReceivedPage(page); +} + +test.describe.serial('Subscriptions Merchant', () => { + const productTitle = (Math.random() + 1).toString(36).substring(7); + const planName = (Math.random() + 1).toString(36).substring(7); + let product_id = ''; + let plan_id = ''; + + test('Create new subscription product', async ({page, request}) => { + await loginAsAdmin(page); + + await page.goto('/wp-admin/post-new.php?post_type=product'); + await page.fill('#title', productTitle); + await page.selectOption('select#product-type', 'subscription'); + await page.fill('#_subscription_price', '10'); + await page.locator('#ppcp_enable_subscription_product').check(); + await page.fill('#ppcp_subscription_plan_name', planName); + + await Promise.all([ + page.waitForNavigation(), + page.locator('#publish').click(), + ]); + + const message = await page.locator('.notice-success'); + await expect(message).toContainText('Product published.'); + + const products = await request.get('https://api.sandbox.paypal.com/v1/catalogs/products?page_size=100&page=1&total_required=true', { + headers: { + 'Authorization': AUTHORIZATION, + 'Content-Type': 'application/json' + } + }); + expect(products.ok()).toBeTruthy(); + + const productList = await products.json(); + const product = productList.products.find((p) => { + return p.name === productTitle; + }); + await expect(product.id).toBeTruthy; + + product_id = product.id; + + const plans = await request.get(`https://api.sandbox.paypal.com/v1/billing/plans?product_id=${product_id}&page_size=10&page=1&total_required=true`, { + headers: { + 'Authorization': AUTHORIZATION, + 'Content-Type': 'application/json' + } + }); + expect(plans.ok()).toBeTruthy(); + + const planList = await plans.json(); + const plan = planList.plans.find((p) => { + return p.product_id === product.id; + }); + await expect(plan.id).toBeTruthy; + + plan_id = plan.id; + }); + + test('Update subscription product', async ({page, request}) => { + await loginAsAdmin(page); + + await page.goto('/wp-admin/edit.php?post_type=product'); + await page.getByRole('link', { name: productTitle, exact: true }).click(); + + await page.fill('#title', `Updated ${productTitle}`); + await page.fill('#_subscription_price', '20'); + + await Promise.all([ + page.waitForNavigation(), + page.locator('#publish').click(), + ]); + + const message = await page.locator('.notice-success'); + await expect(message).toContainText('Product updated.'); + + const products = await request.get('https://api.sandbox.paypal.com/v1/catalogs/products?page_size=100&page=1&total_required=true', { + headers: { + 'Authorization': AUTHORIZATION, + 'Content-Type': 'application/json' + } + }); + expect(products.ok()).toBeTruthy(); + + const productList = await products.json(); + const product = productList.products.find((p) => { + return p.name === `Updated ${productTitle}`; + }); + await expect(product.id).toBeTruthy; + + const plan = await request.get(`https://api.sandbox.paypal.com/v1/billing/plans/${plan_id}`, { + headers: { + 'Authorization': AUTHORIZATION, + 'Content-Type': 'application/json' + } + }); + expect(plan.ok()).toBeTruthy(); + + const plan_content = await plan.json(); + await expect(plan_content.billing_cycles[0].pricing_scheme.fixed_price.value).toBe('20.0') + }); +}); + +test('Create new free trial subscription product', async ({page, request}) => { + const productTitle = (Math.random() + 1).toString(36).substring(7); + const planName = (Math.random() + 1).toString(36).substring(7); + await loginAsAdmin(page); + + await page.goto('/wp-admin/post-new.php?post_type=product'); + await page.fill('#title', productTitle); + await page.selectOption('select#product-type', 'subscription'); + await page.fill('#_subscription_price', '42'); + await page.fill('#_subscription_trial_length', '15'); + + await page.locator('#ppcp_enable_subscription_product').check(); + await page.fill('#ppcp_subscription_plan_name', planName); + + await Promise.all([ + page.waitForNavigation(), + page.locator('#publish').click(), + ]); + + const message = await page.locator('.notice-success'); + await expect(message).toContainText('Product published.'); + + const products = await request.get('https://api.sandbox.paypal.com/v1/catalogs/products?page_size=100&page=1&total_required=true', { + headers: { + 'Authorization': AUTHORIZATION, + 'Content-Type': 'application/json' + } + }); + expect(products.ok()).toBeTruthy(); + + const productList = await products.json(); + const product = productList.products.find((p) => { + return p.name === productTitle; + }); + await expect(product.id).toBeTruthy; + + const plans = await request.get(`https://api.sandbox.paypal.com/v1/billing/plans?product_id=${product.id}&page_size=10&page=1&total_required=true`, { + headers: { + 'Authorization': AUTHORIZATION, + 'Content-Type': 'application/json' + } + }); + expect(plans.ok()).toBeTruthy(); + + const planList = await plans.json(); + const plan = planList.plans.find((p) => { + return p.product_id === product.id; + }); + await expect(plan.id).toBeTruthy; + + const planDetail = await request.get(`https://api.sandbox.paypal.com/v1/billing/plans/${plan.id}`, { + headers: { + 'Authorization': AUTHORIZATION, + 'Content-Type': 'application/json' + } + }); + expect(planDetail.ok()).toBeTruthy(); + const planDetailContent = await planDetail.json(); + + await expect(planDetailContent.billing_cycles[0].tenure_type).toBe('TRIAL'); + await expect(planDetailContent.billing_cycles[0].pricing_scheme.fixed_price.value).toBe('0.0'); + await expect(planDetailContent.billing_cycles[1].tenure_type).toBe('REGULAR'); + await expect(planDetailContent.billing_cycles[1].pricing_scheme.fixed_price.value).toBe('42.0'); +}); + +test.describe('Subscriber purchase a Subscription', () => { + test('Purchase Subscription from Checkout Page', async ({page}) => { + await loginAsCustomer(page); + + await page.goto(SUBSCRIPTION_URL); + await page.click("text=Sign up now"); + await page.goto(CHECKOUT_URL); + await fillCheckoutForm(page); + + const popup = await openPaypalPopup(page); + await loginIntoPaypal(popup); + + await popup.getByText('Continue', { exact: true }).click(); + + await Promise.all([ + page.waitForNavigation(), + await popup.locator('text=Agree & Subscribe').click(), + ]); + + await expectOrderReceivedPage(page); + }); + + test('Purchase Subscription from Single Product Page', async ({page}) => { + await loginAsCustomer(page); + await page.goto(SUBSCRIPTION_URL); + + const popup = await openPaypalPopup(page); + await loginIntoPaypal(popup); + + await popup.getByText('Continue', { exact: true }).click(); + await popup.locator('#confirmButtonTop').click(); + + await fillCheckoutForm(page); + + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Sign up now').click(), + ]); + + await expectOrderReceivedPage(page); + }); + + test('Purchase Subscription from Cart Page', async ({page}) => { + await purchaseSubscriptionFromCart(page); + }); +}); + +test.describe('Subscriber my account actions', () => { + test('Subscriber Suspend Subscription', async ({page, request}) => { + await purchaseSubscriptionFromCart(page); + await page.goto('/my-account/subscriptions'); + await page.locator('text=View').first().click(); + + const subscriptionId = await page.locator('#ppcp-subscription-id').textContent(); + let subscription = await request.get(`https://api.sandbox.paypal.com/v1/billing/subscriptions/${subscriptionId}`, { + headers: { + 'Authorization': AUTHORIZATION, + 'Content-Type': 'application/json' + } + }); + expect(subscription.ok()).toBeTruthy(); + let details = await subscription.json(); + await expect(details.status).toBe('ACTIVE'); + + await page.locator('text=Suspend').click(); + const title = page.locator('.woocommerce-message'); + await expect(title).toHaveText('Your subscription has been cancelled.'); + + subscription = await request.get(`https://api.sandbox.paypal.com/v1/billing/subscriptions/${subscriptionId}`, { + headers: { + 'Authorization': AUTHORIZATION, + 'Content-Type': 'application/json' + } + }); + expect(subscription.ok()).toBeTruthy(); + + details = await subscription.json(); + await expect(details.status).toBe('SUSPENDED'); + }); + + test('Subscriber Cancel Subscription', async ({page, request}) => { + await purchaseSubscriptionFromCart(page); + await page.goto('/my-account/subscriptions'); + await page.locator('text=View').first().click(); + + const subscriptionId = await page.locator('#ppcp-subscription-id').textContent(); + let subscription = await request.get(`https://api.sandbox.paypal.com/v1/billing/subscriptions/${subscriptionId}`, { + headers: { + 'Authorization': AUTHORIZATION, + 'Content-Type': 'application/json' + } + }); + expect(subscription.ok()).toBeTruthy(); + let details = await subscription.json(); + await expect(details.status).toBe('ACTIVE'); + + await page.locator('text=Cancel').click(); + const title = page.locator('.woocommerce-message'); + await expect(title).toHaveText('Your subscription has been cancelled.'); + + subscription = await request.get(`https://api.sandbox.paypal.com/v1/billing/subscriptions/${subscriptionId}`, { + headers: { + 'Authorization': AUTHORIZATION, + 'Content-Type': 'application/json' + } + }); + expect(subscription.ok()).toBeTruthy(); + + details = await subscription.json(); + await expect(details.status).toBe('CANCELLED'); + }); +}) ; diff --git a/tests/playwright/utils/checkout.js b/tests/playwright/utils/checkout.js new file mode 100644 index 000000000..625c543fa --- /dev/null +++ b/tests/playwright/utils/checkout.js @@ -0,0 +1,38 @@ +import {expect} from "@playwright/test"; + +const { + CUSTOMER_EMAIL, + CUSTOMER_FIRST_NAME, + CUSTOMER_LAST_NAME, + CUSTOMER_COUNTRY, + CUSTOMER_ADDRESS, + CUSTOMER_POSTCODE, + CUSTOMER_CITY, + CUSTOMER_PHONE, +} = process.env; + +export const fillCheckoutForm = async (page) => { + await page.fill('#billing_first_name', CUSTOMER_FIRST_NAME); + await page.fill('#billing_last_name', CUSTOMER_LAST_NAME); + await page.selectOption('select#billing_country', CUSTOMER_COUNTRY); + await page.fill('#billing_address_1', CUSTOMER_ADDRESS); + await page.fill('#billing_postcode', CUSTOMER_POSTCODE); + await page.fill('#billing_city', CUSTOMER_CITY); + await page.fill('#billing_phone', CUSTOMER_PHONE); + await page.fill('#billing_email', CUSTOMER_EMAIL); + + const differentShippingLocator = page.locator('[name="ship_to_different_address"]'); + if (await differentShippingLocator.count() > 0) { + await differentShippingLocator.uncheck(); + } + + const termsLocator = page.locator('[name="terms"]'); + if (await termsLocator.count() > 0) { + await termsLocator.check(); + } +} + +export const expectOrderReceivedPage = async (page) => { + const title = await page.locator('.entry-title'); + await expect(title).toHaveText('Order received'); +} diff --git a/tests/playwright/utils/paypal-popup.js b/tests/playwright/utils/paypal-popup.js new file mode 100644 index 000000000..a39e06759 --- /dev/null +++ b/tests/playwright/utils/paypal-popup.js @@ -0,0 +1,91 @@ +import {expect} from "@playwright/test"; + +const { + CUSTOMER_EMAIL, + CUSTOMER_PASSWORD, +} = process.env; + +/** + * Opens the PayPal popup by pressing the button, and returns the popup object. + * @param page + * @param {boolean} retry Retries the button click if the popup did not appear after timeout. + * @param {int} timeout + */ +export const openPaypalPopup = async (page, retry = true, timeout = 5000) => { + try { + await page.locator('.component-frame').scrollIntoViewIfNeeded(); + + const [popup] = await Promise.all([ + page.waitForEvent('popup', {timeout}), + page.frameLocator('.component-frame').locator('[data-funding-source="paypal"]').click(), + ]); + + await popup.waitForLoadState(); + + return popup; + } catch (err) { + try { + for (const f of page.mainFrame().childFrames()) { + if (f.name().startsWith('__paypal_checkout')) { + for (const f2 of f.childFrames()) { + if (f.name().includes('__paypal_checkout')) { + await f2.waitForLoadState(); + await expect(await f2.locator('#main')).toBeVisible(); + return f2; + } + } + } + } + } catch (frameErr) { + console.log(frameErr) + } + + if (retry) { + return openPaypalPopup(page, false); + } + throw err; + } +} + +export const loginIntoPaypal = async (popup, retry = true) => { + await Promise.any([ + popup.locator('[name="login_email"]'), + popup.click("text=Log in"), + ]); + + await popup.fill('[name="login_email"]', CUSTOMER_EMAIL); + + const nextButtonLocator = popup.locator('#btnNext'); + // Sometimes we get a popup with email and password fields at the same screen + if (await nextButtonLocator.count() > 0) { + await nextButtonLocator.click(); + } + + try { + await popup.fill('[name="login_password"]', CUSTOMER_PASSWORD, {timeout: 5000}); + } catch (err) { + console.log('Failed to fill password, possibly need to enter email again, retrying') + if (retry) { + return loginIntoPaypal(popup, false); + } + throw err; + } + + await popup.locator('#btnLogin').click(); +} + +/** + * Waits up to 15 sec for the shipping methods list to load. + * @param popup + * @returns {Promise} + */ +export const waitForPaypalShippingList = async (popup) => { + await expect(popup.locator('#shippingMethodsDropdown')).toBeVisible({timeout: 15000}); +} + +export const completePaypalPayment = async (popup) => { + await Promise.all([ + popup.waitForEvent('close', {timeout: 20000}), + popup.click('#payment-submit-btn'), + ]); +} diff --git a/tests/playwright/utils/server.js b/tests/playwright/utils/server.js new file mode 100644 index 000000000..351f7dfb8 --- /dev/null +++ b/tests/playwright/utils/server.js @@ -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); + } + })) +} diff --git a/tests/playwright/utils/user.js b/tests/playwright/utils/user.js new file mode 100644 index 000000000..2d793af4b --- /dev/null +++ b/tests/playwright/utils/user.js @@ -0,0 +1,26 @@ +const { + WP_MERCHANT_USER, + WP_MERCHANT_PASSWORD, + WP_CUSTOMER_USER, + WP_CUSTOMER_PASSWORD, +} = process.env; + +export const loginAsAdmin = async (page) => { + await page.goto('/wp-admin'); + await page.locator('input[name="log"]').fill(WP_MERCHANT_USER); + await page.locator('input[name="pwd"]').fill(WP_MERCHANT_PASSWORD); + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Log In').click() + ]); +} + +export const loginAsCustomer = async (page) => { + await page.goto('/wp-admin'); + await page.locator('input[name="log"]').fill(WP_CUSTOMER_USER); + await page.locator('input[name="pwd"]').fill(WP_CUSTOMER_PASSWORD); + await Promise.all([ + page.waitForNavigation(), + page.locator('text=Log In').click() + ]); +} diff --git a/woocommerce-paypal-payments.php b/woocommerce-paypal-payments.php index 92160fd68..4fad31bcd 100644 --- a/woocommerce-paypal-payments.php +++ b/woocommerce-paypal-payments.php @@ -3,13 +3,13 @@ * Plugin Name: WooCommerce PayPal Payments * Plugin URI: https://woocommerce.com/products/woocommerce-paypal-payments/ * Description: PayPal's latest complete payments processing solution. Accept PayPal, Pay Later, credit/debit cards, alternative digital wallets local payment types and bank accounts. Turn on only PayPal options or process a full suite of payment methods. Enable global transaction with extensive currency and country coverage. - * Version: 2.0.4 + * Version: 2.1.0 * Author: WooCommerce * Author URI: https://woocommerce.com/ * License: GPL-2.0 * Requires PHP: 7.2 * WC requires at least: 3.9 - * WC tested up to: 7.5 + * WC tested up to: 7.7 * Text Domain: woocommerce-paypal-payments * * @package WooCommerce\PayPalCommerce @@ -23,9 +23,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings; define( 'PAYPAL_API_URL', 'https://api.paypal.com' ); define( 'PAYPAL_SANDBOX_API_URL', 'https://api.sandbox.paypal.com' ); -define( 'PAYPAL_INTEGRATION_DATE', '2023-03-20' ); - -define( 'PPCP_FLAG_SUBSCRIPTION', true ); +define( 'PAYPAL_INTEGRATION_DATE', '2023-06-02' ); ! defined( 'CONNECT_WOO_CLIENT_ID' ) && define( 'CONNECT_WOO_CLIENT_ID', 'AcCAsWta_JTL__OfpjspNyH7c1GGHH332fLwonA5CwX4Y10mhybRZmHLA0GdRbwKwjQIhpDQy0pluX_P' ); ! defined( 'CONNECT_WOO_SANDBOX_CLIENT_ID' ) && define( 'CONNECT_WOO_SANDBOX_CLIENT_ID', 'AYmOHbt1VHg-OZ_oihPdzKEVbU3qg0qXonBcAztuzniQRaKE0w1Hr762cSFwd4n8wxOl-TCWohEa0XM_' ); @@ -44,6 +42,8 @@ define( 'PPCP_FLAG_SUBSCRIPTION', true ); * Initialize the plugin and its modules. */ function init(): void { + define( 'PPCP_FLAG_SUBSCRIPTIONS_API', apply_filters( 'ppcp_flag_subscriptions_api', getenv( 'PPCP_FLAG_SUBSCRIPTIONS_API' ) === '1' ) ); + $root_dir = __DIR__; if ( ! is_woocommerce_activated() ) { diff --git a/wp-cli.yml b/wp-cli.yml new file mode 100644 index 000000000..d07f2be00 --- /dev/null +++ b/wp-cli.yml @@ -0,0 +1 @@ +path: .ddev/wordpress/