🔀 Merge branch 'trunk'

This commit is contained in:
Philipp Stracker 2024-09-20 16:23:22 +02:00
commit 44a4830480
No known key found for this signature in database
243 changed files with 9347 additions and 1704 deletions

View file

@ -1,7 +1,7 @@
name: woocommerce-paypal-payments
type: php
docroot: .ddev/wordpress
php_version: "7.2"
php_version: "7.4"
webserver_type: apache-fpm
router_http_port: "80"
router_https_port: "443"
@ -18,7 +18,7 @@ hooks:
pre-start:
- exec-host: "mkdir -p .ddev/wordpress/wp-content/plugins/${DDEV_PROJECT}"
web_environment:
- WP_VERSION=6.2.2
- WP_VERSION=6.3.3
- WP_LOCALE=en_US
- WP_TITLE=WooCommerce PayPal Payments
- WP_MULTISITE=true

34
.distignore Normal file
View file

@ -0,0 +1,34 @@
.distignore
.editorconfig
.gitattributes
.gitignore
.eslintrc
.ddev
.git*
.phpstorm*
.idea
*.env*
.psalm*
tests
*.xml*
.phpunit.result.cache
babel.config.json
node_modules
modules/*/resources/css
modules/*/resources/js/**/*.js
*.lock
webpack.config.js
wp-cli.yml
ngrok.yml
composer.json
package.json
package-lock.json
.composer_compiled_assets
assets-compiler.json
patchwork.json
.babelrc
README.md
wordpress_org_assets
.DS_Store
auth.json
*.log

View file

@ -7,8 +7,8 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.3', '7.4', '8.2']
wc-versions: ['5.9.5', '7.7.2']
php-versions: ['7.4', '8.2']
wc-versions: ['6.9.4', '7.7.2']
name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }}
steps:

37
.github/workflows/package-new.yml vendored Normal file
View file

@ -0,0 +1,37 @@
name: Build package (New)
on:
workflow_dispatch:
inputs:
packageVersion:
description: 'Package version'
required: false
type: string
jobs:
check_version:
runs-on: ubuntu-latest
env:
PACKAGE_VERSION: ${{ github.event.inputs.packageVersion }}
outputs:
version: ${{ env.PACKAGE_VERSION }}
name: Check version
steps:
- uses: actions/checkout@v4
- name: Fix plugin version input # Add the version number if only suffix entered
run: echo "PACKAGE_VERSION=$(sed -nE '/Version:/s/.* ([0-9.]+).*/\1/p' woocommerce-paypal-payments.php)-$PACKAGE_VERSION" >> $GITHUB_ENV
if: env.PACKAGE_VERSION && !contains(env.PACKAGE_VERSION, '.')
- name: Fill plugin version # If the version number was not set, retrieve it from the file
run: echo "PACKAGE_VERSION=$(sed -nE '/Version:/s/.* ([0-9.]+).*/\1/p' woocommerce-paypal-payments.php)" >> $GITHUB_ENV
if: "!env.PACKAGE_VERSION"
create_archive:
needs: check_version
uses: inpsyde/reusable-workflows/.github/workflows/build-plugin-archive.yml@main
with:
PHP_VERSION: 7.4
PLUGIN_MAIN_FILE: ./woocommerce-paypal-payments.php
PLUGIN_VERSION: ${{ needs.check_version.outputs.version }}
PLUGIN_FOLDER_NAME: woocommerce-paypal-payments
ARCHIVE_NAME: woocommerce-paypal-payments-${{ needs.check_version.outputs.version }}
COMPILE_ASSETS_ARGS: '-vv --env=root'

View file

@ -27,7 +27,7 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.2
php-version: 7.4
- name: Fix plugin version input # Add the version number if only suffix entered
run: echo "PACKAGE_VERSION=$(sed -nE '/Version:/s/.* ([0-9.]+).*/\1/p' woocommerce-paypal-payments.php)-$PACKAGE_VERSION" >> $GITHUB_ENV

View file

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php-versions: ['7.2', '7.3', '7.4', '8.0', '8.1', '8.2', '8.3']
php-versions: ['7.4', '8.0', '8.1', '8.2', '8.3']
name: PHP ${{ matrix.php-versions }}
steps:

1
.gitignore vendored
View file

@ -13,3 +13,4 @@ modules/*/assets/*
auth.json
.DS_Store
tests/.DS_Store
.composer_compiled_assets

View file

@ -1 +0,0 @@
-s:*

View file

@ -4,9 +4,9 @@ PayPal's latest complete payments processing solution. Accept PayPal, Pay Later,
## Dependencies
* PHP >= 7.2
* WordPress >=5.3
* WooCommerce >=4.5
* PHP >= 7.4
* WordPress >= 6.3
* WooCommerce >= 6.9
## Development

4
assets-compiler.json Normal file
View file

@ -0,0 +1,4 @@
{
"dependencies": "install",
"commands": "yarn"
}

View file

@ -5,13 +5,8 @@
* @package WooCommerce\PayPalCommerce
*/
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\CachingContainer;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\CompositeCachingServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\CompositeContainer;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\DelegatingContainer;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ProxyContainer;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Package;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties\PluginProperties;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return function (
@ -34,38 +29,15 @@ return function (
*/
$modules = apply_filters( 'woocommerce_paypal_payments_modules', $modules );
$providers = array_map(
function ( ModuleInterface $module ): ServiceProviderInterface {
return $module->setup();
},
$modules
);
$provider = new CompositeCachingServiceProvider( $providers );
$proxy_container = new ProxyContainer();
// TODO: caching does not work currently,
// may want to consider fixing it later (pass proxy as parent to DelegatingContainer)
// for now not fixed since we were using this behavior for long time and fixing it now may break things.
$container = new DelegatingContainer( $provider );
/**
* Skip iterable vs array check.
*
* @psalm-suppress PossiblyInvalidArgument
*/
$app_container = new CachingContainer(
new CompositeContainer(
array_merge(
$additional_containers,
array( $container )
)
)
);
$proxy_container->setInnerContainer( $app_container );
// Initialize plugin.
$properties = PluginProperties::new( __FILE__ );
$bootstrap = Package::new( $properties );
foreach ( $modules as $module ) {
/* @var $module ModuleInterface module */
$module->run( $app_container );
$bootstrap->addModule( $module );
}
return $app_container;
$bootstrap->boot();
return $bootstrap->container();
};

View file

@ -1,5 +1,43 @@
*** Changelog ***
= 2.9.1 - xxxx-xx-xx =
* Fix - Improve card fields hiding #2574
* Fix - Google Pay: Shipping callback not calculating totals correctly on Single Product page #2513
* Fix - Fix shipping callback condition in status report #2578
* Fix - Can't Disconnect Account #2539
* Fix - Google Pay billing data without shipping callback #2525
* Fix - Standard payment tab - Google Pay and Apple Pay button - Shape from one location is applied to all until saving changes #2419
* Enhancement - Allow to override the list of Pay Later supported countries #2563
* Enhancement - Add more feature statuses into system report #2550
* Enhancement - Use SVG for APM gateway icons #2509
* Enhancement - Add inline notice to inform users about ACDC block Checkout support if the store uses a Classic Checkout setup #2422
* Enhancement - Remove leftover console.log #2589
* Enhancement - Require PHP 7.4+, WP 6.3+, WC 6.9+ #2556
* Enhancement - Modularity module migration #1944
* Enhancement - Keep only 5 tags in readme.txt #2562
* Enhancement - Select ACDC by default during onboarding for China store locations #2619
* Enhancement - Add title, description and gatewayId to the express payment method #2566
= 2.9.0 - 2024-09-02 =
* Fix - Fatal error in Block Editor when using WooCommerce blocks #2534
* Fix - Can't pay from block pages when the shipping callback is enabled and no shipping methods defined #2429
* Fix - Various Google Pay button fixes #2496
* Fix - Buying a free trial subscription with ACDC results in a $1 charge in the API call #2465
* Fix - Problem with Google Pay and Apple Pay button placement on Pay for Order page #2542
* Fix - When there isn't any shipping option for the address the order is still created from classic cart #2437
* Fix - Patch the order with no shipping methods, instead of throwing an error #2435
* Enhancement - Separate Apple Pay button for Classic Checkout #2457
* Enhancement - Remove AMEX support for ACDC when store location is set to China #2526
* Enhancement - Inform users of Pay Later messaging configuration when Pay Later wasn't recently enabled #2529
* Enhancement - Update ACDC signup URLs #2475
* Enhancement - Implement country based APMs via Orders API #2511
* Enhancement - Update PaymentsStatusHandlingTrait.php (author @callmeahmedr) #2523
* Enhancement - Disable PayPal Shipping callback by default #2527
* Enhancement - Change Apple Pay and Google Pay default button labels to plain #2476
* Enhancement - Add Package Tracking compatibility with DHL Shipping plugin #2463
* Enhancement - Add support for WC Bookings when skipping checkout confirmation #2452
* Enhancement - Remove currencies from country-currency matrix in card fields module #2441
= 2.8.3 - 2024-08-12 =
* Fix - Google Pay: Prevent field validation from being triggered on checkout page load #2474
* Fix - Do not add tax info into order meta during order creation #2471

View file

@ -4,7 +4,7 @@
"description": "PayPal Commerce Platform for WooCommerce",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"ext-json": "*",
"psr/log": "^1.1",
"ralouphie/getallheaders": "^3.0",
@ -14,10 +14,12 @@
"symfony/polyfill-php80": "^1.19"
},
"require-dev": {
"inpsyde/composer-assets-compiler": "^2.5",
"psr/container": "^1.0",
"dhii/module-interface": "^0.2 || ^0.3",
"container-interop/service-provider": "^0.4.0",
"dhii/containers": "^0.1.0-alpha1",
"inpsyde/modularity": "^1.7",
"woocommerce/woocommerce-sniffs": "^0.1.0",
"phpunit/phpunit": "^7.0 | ^8.0 | ^9.0",
"brain/monkey": "^2.4",
@ -69,6 +71,15 @@
"vendor/bin/phpcbf"
]
},
"composer-asset-compiler": {
"auto-run": false,
"packages": {
"inpsyde/*": {
"dependencies": "install",
"script": "build"
}
}
},
"mozart": {
"dep_namespace": "WooCommerce\\PayPalCommerce\\Vendor\\",
"dep_directory": "/lib/packages/",
@ -77,13 +88,18 @@
"packages": [
"psr/container",
"dhii/containers",
"dhii/module-interface"
"dhii/module-interface",
"inpsyde/modularity"
],
"delete_vendor_directories": true
}
},
"config": {
"platform": {
"php": "7.4"
},
"allow-plugins": {
"inpsyde/composer-assets-compiler": true,
"dealerdirect/phpcodesniffer-composer-installer": true,
"wikimedia/composer-merge-plugin": true
}

1358
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,153 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerExceptionInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
class ContainerConfigurator
{
/**
* @var array<string, callable(ContainerInterface $container):mixed>
*/
private $services = [];
/**
* @var array<string, bool>
*/
private $factoryIds = [];
/**
* @var array<string, array<callable(mixed $service, ContainerInterface $container):mixed>>
*/
private $extensions = [];
/**
* @var ContainerInterface[]
*/
private $containers = [];
/**
* @var null|ContainerInterface
*/
private $compiledContainer;
/**
* ContainerConfigurator constructor.
*
* @param ContainerInterface[] $containers
*/
public function __construct(array $containers = [])
{
array_map([$this, 'addContainer'], $containers);
}
/**
* Allowing to add child containers.
*
* @param ContainerInterface $container
*/
public function addContainer(ContainerInterface $container): void
{
$this->containers[] = $container;
}
/**
* @param string $id
* @param callable(ContainerInterface $container):mixed $factory
*/
public function addFactory(string $id, callable $factory): void
{
$this->addService($id, $factory);
// We're using a hash table to detect later
// via isset() if a Service as a Factory.
$this->factoryIds[$id] = true;
}
/**
* @param string $id
* @param callable(ContainerInterface $container):mixed $service
*
* @return void
*/
public function addService(string $id, callable $service): void
{
/*
* We are being intentionally permissive here,
* allowing a simple workflow for *intentional* overrides
* while accepting the (small?) risk of *accidental* overrides
* that could be hard to notice and debug.
*
* Clear a factory flag in case it was a factory.
* If needs be, it will get re-added after this function completes.
*/
unset($this->factoryIds[$id]);
$this->services[$id] = $service;
}
/**
* @param string $id
*
* @return bool
*/
public function hasService(string $id): bool
{
if (array_key_exists($id, $this->services)) {
return true;
}
foreach ($this->containers as $container) {
if ($container->has($id)) {
return true;
}
}
return false;
}
/**
* @param string $id
* @param callable(mixed $service, ContainerInterface $container):mixed $extender
*
* @return void
*/
public function addExtension(string $id, callable $extender): void
{
if (!isset($this->extensions[$id])) {
$this->extensions[$id] = [];
}
$this->extensions[$id][] = $extender;
}
/**
* @param string $id
*
* @return bool
*/
public function hasExtension(string $id): bool
{
return isset($this->extensions[$id]);
}
/**
* Returns a read only version of this Container.
*
* @return ContainerInterface
*/
public function createReadOnlyContainer(): ContainerInterface
{
if (!$this->compiledContainer) {
$this->compiledContainer = new ReadOnlyContainer(
$this->services,
$this->factoryIds,
$this->extensions,
$this->containers
);
}
return $this->compiledContainer;
}
}

View file

@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Package;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerExceptionInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
class PackageProxyContainer implements ContainerInterface
{
/**
* @var Package
*/
private $package;
/**
* @var ContainerInterface|null
*/
private $container;
/**
* @param Package $package
*/
public function __construct(Package $package)
{
$this->package = $package;
}
/**
* @param string $id
* @return mixed
*
* @throws \Exception
*/
public function get(string $id)
{
$this->assertPackageBooted($id);
return $this->container->get($id);
}
/**
* @param string $id
* @return bool
*
* @throws \Exception
*/
public function has(string $id): bool
{
return $this->tryContainer() && $this->container->has($id);
}
/**
* @return bool
*
* @throws \Exception
* @psalm-assert-if-true ContainerInterface $this->container
*/
private function tryContainer(): bool
{
if ($this->container) {
return true;
}
/** TODO: We need a better way to deal with status checking besides equality */
if (
$this->package->statusIs(Package::STATUS_READY)
|| $this->package->statusIs(Package::STATUS_BOOTED)
) {
$this->container = $this->package->container();
}
return (bool)$this->container;
}
/**
* @param string $id
* @return void
*
* @throws \Exception
*
* @psalm-assert ContainerInterface $this->container
*/
private function assertPackageBooted(string $id): void
{
if ($this->tryContainer()) {
return;
}
$name = $this->package->name();
$status = $this->package->statusIs(Package::STATUS_FAILED)
? 'is errored'
: 'is not ready yet';
throw new class ("Error retrieving service {$id} because package {$name} {$status}.")
extends \Exception
implements ContainerExceptionInterface {
};
}
}

View file

@ -0,0 +1,138 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\NotFoundExceptionInterface;
class ReadOnlyContainer implements ContainerInterface
{
/**
* @var array<string, callable(\WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface $container):mixed>
*/
private $services;
/**
* @var array<string, bool>
*/
private $factoryIds;
/**
* @var array<string, array<callable(mixed, ContainerInterface $container):mixed>>
*/
private $extensions;
/**
* Resolved factories.
*
* @var array<string, mixed>
*/
private $resolvedServices = [];
/**
* @var ContainerInterface[]
*/
private $containers;
/**
* ReadOnlyContainer constructor.
*
* @param array<string, callable(ContainerInterface $container):mixed> $services
* @param array<string, bool> $factoryIds
* @param array<string, array<callable(mixed, ContainerInterface $container):mixed>> $extensions
* @param ContainerInterface[] $containers
*/
public function __construct(
array $services,
array $factoryIds,
array $extensions,
array $containers
) {
$this->services = $services;
$this->factoryIds = $factoryIds;
$this->extensions = $extensions;
$this->containers = $containers;
}
/**
* @param string $id
*
* @return mixed
*/
public function get(string $id)
{
if (array_key_exists($id, $this->resolvedServices)) {
return $this->resolvedServices[$id];
}
if (array_key_exists($id, $this->services)) {
$service = $this->services[$id]($this);
$resolved = $this->resolveExtensions($id, $service);
if (!isset($this->factoryIds[$id])) {
$this->resolvedServices[$id] = $resolved;
unset($this->services[$id]);
}
return $resolved;
}
foreach ($this->containers as $container) {
if ($container->has($id)) {
$service = $container->get($id);
return $this->resolveExtensions($id, $service);
}
}
throw new class ("Service with ID {$id} not found.")
extends \Exception
implements NotFoundExceptionInterface {
};
}
/**
* @param string $id
*
* @return bool
*/
public function has(string $id): bool
{
if (array_key_exists($id, $this->services)) {
return true;
}
if (array_key_exists($id, $this->resolvedServices)) {
return true;
}
foreach ($this->containers as $container) {
if ($container->has($id)) {
return true;
}
}
return false;
}
/**
* @param string $id
* @param mixed $service
*
* @return mixed
*/
private function resolveExtensions(string $id, $service)
{
if (!isset($this->extensions[$id])) {
return $service;
}
foreach ($this->extensions[$id] as $extender) {
$service = $extender($service, $this);
}
return $service;
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
interface ExecutableModule extends Module
{
/**
* Perform actions with objects retrieved from the container. Usually, adding WordPress hooks.
* Return true to signal a success, false to signal a failure.
*
* @param ContainerInterface $container
*
* @return bool true when successfully booted, otherwise false.
*/
public function run(ContainerInterface $container): bool;
}

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
interface ExtendingModule extends Module
{
/**
* Return application services' extensions.
*
* Array keys will be services' IDs in the container, array values are callback that
* accepts as parameters the original service and a PSR-11 container and return an instance of
* the extended service.
*
* It is possible to explicitly extend extensions made by other modules.
* That is done by using as ID (array key in the `extensions` method) the target module ID
* and the service ID.
*
* @return array<string, callable(mixed $service, \WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface $container):mixed>
*/
public function extensions(): array;
}

View file

@ -0,0 +1,18 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
interface FactoryModule extends Module
{
/**
* Return application factories.
*
* Similar to `services`, but object created by given factories are not "cached", but a *new*
* instance is returned everytime `get()` is called in the container.
*
* @return array<string, callable(\WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface $container):mixed>
*/
public function factories(): array;
}

View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
/**
* @package WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module
*/
interface Module
{
/**
* Unique identifier for your Module.
*
* @return string
*/
public function id(): string;
}

View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
/**
* Trait ModuleClassNameIdTrait
*
* @package WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module
*/
trait ModuleClassNameIdTrait
{
/**
* @return string
* @see Module::id()
*/
public function id(): string
{
return __CLASS__;
}
}

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
interface ServiceModule extends Module
{
/**
* Return application services' factories.
*
* Array keys will be services' IDs in the container, array values are callback that
* accepts a PSR-11 container as parameter and return an instance of the service.
* Services are "cached", so the given factory is called once the first time `get()` is called
* in the container, and on subsequent `get()` the same instance is returned again and again.
*
* @return array<string, callable(\WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface $container):mixed>
*/
public function services(): array;
}

View file

@ -0,0 +1,727 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container\ContainerConfigurator;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container\PackageProxyContainer;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\FactoryModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\Module;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties\Properties;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
class Package
{
/**
* All the hooks fired in this class use this prefix.
* @var string
*/
private const HOOK_PREFIX = 'inpsyde.modularity.';
/**
* Identifier to access Properties in Container.
*
* @example
* <code>
* $package = Package::new();
* $package->boot();
*
* $container = $package->container();
* $container->has(Package::PROPERTIES);
* $container->get(Package::PROPERTIES);
* </code>
*
* @var string
*/
public const PROPERTIES = 'properties';
/**
* Custom action to be used to add Modules to the package.
* It might also be used to access package properties.
*
* @example
* <code>
* $package = Package::new();
*
* add_action(
* $package->hookName(Package::ACTION_INIT),
* $callback
* );
* </code>
*/
public const ACTION_INIT = 'init';
/**
* Custom action which is triggered after the application
* is booted to access container and properties.
*
* @example
* <code>
* $package = Package::new();
*
* add_action(
* $package->hookName(Package::ACTION_READY),
* $callback
* );
* </code>
*/
public const ACTION_READY = 'ready';
/**
* Custom action which is triggered when a failure happens during the building stage.
*
* @example
* <code>
* $package = Package::new();
*
* add_action(
* $package->hookName(Package::ACTION_FAILED_BUILD),
* $callback
* );
* </code>
*/
public const ACTION_FAILED_BUILD = 'failed-build';
/**
* Custom action which is triggered when a failure happens during the booting stage.
*
* @example
* <code>
* $package = Package::new();
*
* add_action(
* $package->hookName(Package::ACTION_FAILED_BOOT),
* $callback
* );
* </code>
*/
public const ACTION_FAILED_BOOT = 'failed-boot';
/**
* Custom action which is triggered when a package is connected.
*/
public const ACTION_PACKAGE_CONNECTED = 'package-connected';
/**
* Custom action which is triggered when a package cannot be connected.
*/
public const ACTION_FAILED_CONNECTION = 'failed-connection';
/**
* Module states can be used to get information about your module.
*
* @example
* <code>
* $package = Package::new();
* $package->moduleIs(SomeModule::class, Package::MODULE_ADDED); // false
* $package->boot(new SomeModule());
* $package->moduleIs(SomeModule::class, Package::MODULE_ADDED); // true
* </code>
*/
public const MODULE_ADDED = 'added';
public const MODULE_NOT_ADDED = 'not-added';
public const MODULE_REGISTERED = 'registered';
public const MODULE_REGISTERED_FACTORIES = 'registered-factories';
public const MODULE_EXTENDED = 'extended';
public const MODULE_EXECUTED = 'executed';
public const MODULE_EXECUTION_FAILED = 'executed-failed';
public const MODULES_ALL = '*';
/**
* Custom states for the class.
*
* @example
* <code>
* $package = Package::new();
* $package->statusIs(Package::IDLE); // true
* $package->boot();
* $package->statusIs(Package::BOOTED); // true
* </code>
*/
public const STATUS_IDLE = 2;
public const STATUS_INITIALIZED = 4;
public const STATUS_MODULES_ADDED = 5;
public const STATUS_READY = 7;
public const STATUS_BOOTED = 8;
public const STATUS_FAILED = -8;
/**
* Current state of the application.
*
* @see Package::STATUS_*
*
* @var int
*/
private $status = self::STATUS_IDLE;
/**
* Contains the progress of all modules.
*
* @see Package::moduleProgress()
*
* @var array<string, list<string>>
*/
private $moduleStatus = [self::MODULES_ALL => []];
/**
* Hashmap of where keys are names of connected packages, and values are boolean, true
* if connection was successful.
*
* @see Package::connect()
*
* @var array<string, bool>
*/
private $connectedPackages = [];
/**
* @var list<ExecutableModule>
*/
private $executables = [];
/**
* @var Properties
*/
private $properties;
/**
* @var ContainerConfigurator
*/
private $containerConfigurator;
/**
* @var bool
*/
private $built = false;
/**
* @var bool
*/
private $hasContainer = false;
/**
* @var \Throwable|null
*/
private $lastError = null;
/**
* @param Properties $properties
* @param ContainerInterface[] $containers
*
* @return Package
*/
public static function new(Properties $properties, ContainerInterface ...$containers): Package
{
return new self($properties, ...$containers);
}
/**
* @param Properties $properties
* @param ContainerInterface[] $containers
*/
private function __construct(Properties $properties, ContainerInterface ...$containers)
{
$this->properties = $properties;
$this->containerConfigurator = new ContainerConfigurator($containers);
$this->containerConfigurator->addService(
self::PROPERTIES,
static function () use ($properties) {
return $properties;
}
);
}
/**
* @param Module $module
*
* @return static
* @throws \Exception
*/
public function addModule(Module $module): Package
{
try {
$this->assertStatus(self::STATUS_IDLE, sprintf('add module %s', $module->id()));
$registeredServices = $this->addModuleServices(
$module,
self::MODULE_REGISTERED
);
$registeredFactories = $this->addModuleServices(
$module,
self::MODULE_REGISTERED_FACTORIES
);
$extended = $this->addModuleServices(
$module,
self::MODULE_EXTENDED
);
$isExecutable = $module instanceof ExecutableModule;
// ExecutableModules are collected and executed on Package::boot()
// when the Container is being compiled.
if ($isExecutable) {
/** @var ExecutableModule $module */
$this->executables[] = $module;
}
$added = $registeredServices || $registeredFactories || $extended || $isExecutable;
$status = $added ? self::MODULE_ADDED : self::MODULE_NOT_ADDED;
$this->moduleProgress($module->id(), $status);
} catch (\Throwable $throwable) {
$this->handleFailure($throwable, self::ACTION_FAILED_BUILD);
}
return $this;
}
/**
* @param Package $package
* @return bool
* @throws \Exception
*/
public function connect(Package $package): bool
{
try {
if ($package === $this) {
return false;
}
$packageName = $package->name();
$errorData = ['package' => $packageName, 'status' => $this->status];
$errorMessage = "Failed connecting package {$packageName}";
// Don't connect, if already connected
if (array_key_exists($packageName, $this->connectedPackages)) {
$error = "{$errorMessage} because it was already connected.";
do_action(
$this->hookName(self::ACTION_FAILED_CONNECTION),
$packageName,
new \WP_Error('already_connected', $error, $errorData)
);
throw new \Exception($error, 0, $this->lastError);
}
// Don't connect, if already booted or boot failed
$failed = $this->statusIs(self::STATUS_FAILED);
if ($failed || $this->statusIs(self::STATUS_BOOTED)) {
$status = $failed ? 'errored' : 'booted';
$error = "{$errorMessage} to a {$status} package.";
do_action(
$this->hookName(self::ACTION_FAILED_CONNECTION),
$packageName,
new \WP_Error("no_connect_on_{$status}", $error, $errorData)
);
throw new \Exception($error, 0, $this->lastError);
}
$this->connectedPackages[$packageName] = true;
// We put connected package's properties in this package's container, so that in modules
// "run" method we can access them if we need to.
$this->containerConfigurator->addService(
sprintf('%s.%s', $package->name(), self::PROPERTIES),
static function () use ($package): Properties {
return $package->properties();
}
);
// If the other package is booted, we can obtain a container, otherwise
// we build a proxy container
$container = $package->statusIs(self::STATUS_BOOTED)
? $package->container()
: new PackageProxyContainer($package);
$this->containerConfigurator->addContainer($container);
do_action(
$this->hookName(self::ACTION_PACKAGE_CONNECTED),
$packageName,
$this->status,
$container instanceof PackageProxyContainer
);
return true;
} catch (\Throwable $throwable) {
if (isset($packageName)) {
$this->connectedPackages[$packageName] = false;
}
$this->handleFailure($throwable, self::ACTION_FAILED_BUILD);
return false;
}
}
/**
* @return static
*/
public function build(): Package
{
try {
// Don't allow building the application multiple times.
$this->assertStatus(self::STATUS_IDLE, 'build package');
do_action(
$this->hookName(self::ACTION_INIT),
$this
);
// Changing the status here ensures we can not call this method again, and also we can not
// add new modules, because both this and `addModule()` methods check for idle status.
// For backward compatibility, adding new modules via `boot()` will still be possible, even
// if deprecated, at the condition that the container was not yet accessed at that point.
$this->progress(self::STATUS_INITIALIZED);
} catch (\Throwable $throwable) {
$this->handleFailure($throwable, self::ACTION_FAILED_BUILD);
} finally {
$this->built = true;
}
return $this;
}
/**
* @param Module ...$defaultModules Deprecated, use `addModule()` to add default modules.
* @return bool
*
* @throws \Throwable
*/
public function boot(Module ...$defaultModules): bool
{
try {
// Call build() if not called yet, and ensure any new module passed here is added
// as well, throwing if the container was already built.
$this->doBuild(...$defaultModules);
// Don't allow booting the application multiple times.
$this->assertStatus(self::STATUS_MODULES_ADDED, 'boot application', '<');
$this->assertStatus(self::STATUS_FAILED, 'boot application', '!=');
$this->progress(self::STATUS_MODULES_ADDED);
$this->doExecute();
$this->progress(self::STATUS_READY);
do_action(
$this->hookName(self::ACTION_READY),
$this
);
} catch (\Throwable $throwable) {
$this->handleFailure($throwable, self::ACTION_FAILED_BOOT);
return false;
}
$this->progress(self::STATUS_BOOTED);
return true;
}
/**
* @param Module ...$defaultModules
* @return void
*/
private function doBuild(Module ...$defaultModules): void
{
if ($defaultModules) {
$this->deprecatedArgument(
sprintf(
'Passing default modules to %1$s::boot() is deprecated since version 1.7.0.'
. ' Please add modules via %1$s::addModule().',
__CLASS__
),
__METHOD__,
'1.7.0'
);
}
if (!$this->built) {
array_map([$this, 'addModule'], $defaultModules);
$this->build();
return;
}
if (
!$defaultModules
|| ($this->status >= self::STATUS_MODULES_ADDED)
|| ($this->statusIs(self::STATUS_FAILED))
) {
// if we don't have default modules, there's nothing to do, and if the status is beyond
// "modules added" or is failed, we do nothing as well and let `boot()` throw.
return;
}
$backup = $this->status;
try {
// simulate idle status to prevent `addModule()` from throwing
// only if we don't have a container yet
$this->hasContainer or $this->status = self::STATUS_IDLE;
foreach ($defaultModules as $defaultModule) {
// If a module was added by `build()` or `addModule()` we can skip it, a
// deprecation was trigger to make it noticeable without breakage
if (!$this->moduleIs($defaultModule->id(), self::MODULE_ADDED)) {
$this->addModule($defaultModule);
}
}
} finally {
$this->status = $backup;
}
}
/**
* @param Module $module
* @param string $status
* @return bool
*/
private function addModuleServices(Module $module, string $status): bool
{
$services = null;
$addCallback = null;
switch ($status) {
case self::MODULE_REGISTERED:
$services = $module instanceof ServiceModule ? $module->services() : null;
$addCallback = [$this->containerConfigurator, 'addService'];
break;
case self::MODULE_REGISTERED_FACTORIES:
$services = $module instanceof FactoryModule ? $module->factories() : null;
$addCallback = [$this->containerConfigurator, 'addFactory'];
break;
case self::MODULE_EXTENDED:
$services = $module instanceof ExtendingModule ? $module->extensions() : null;
$addCallback = [$this->containerConfigurator, 'addExtension'];
break;
}
if (!$services) {
return false;
}
$ids = [];
array_walk(
$services,
static function (callable $service, string $id) use ($addCallback, &$ids) {
/** @var callable(string, callable) $addCallback */
$addCallback($id, $service);
/** @var list<string> $ids */
$ids[] = $id;
}
);
/** @var list<string> $ids */
$this->moduleProgress($module->id(), $status, $ids);
return true;
}
/**
* @return void
*
* @throws \Throwable
*/
private function doExecute(): void
{
foreach ($this->executables as $executable) {
$success = $executable->run($this->container());
$this->moduleProgress(
$executable->id(),
$success
? self::MODULE_EXECUTED
: self::MODULE_EXECUTION_FAILED
);
}
}
/**
* @param string $moduleId
* @param string $status
* @param list<string>|null $serviceIds
*
* @return void
*/
private function moduleProgress(string $moduleId, string $status, ?array $serviceIds = null)
{
isset($this->moduleStatus[$status]) or $this->moduleStatus[$status] = [];
$this->moduleStatus[$status][] = $moduleId;
if (!$serviceIds || !$this->properties->isDebug()) {
$this->moduleStatus[self::MODULES_ALL][] = "{$moduleId} {$status}";
return;
}
$description = sprintf('%s %s (%s)', $moduleId, $status, implode(', ', $serviceIds));
$this->moduleStatus[self::MODULES_ALL][] = $description;
}
/**
* @return array<string, list<string>>
*/
public function modulesStatus(): array
{
return $this->moduleStatus;
}
/**
* @return array<string, bool>
*/
public function connectedPackages(): array
{
return $this->connectedPackages;
}
/**
* @param string $packageName
* @return bool
*/
public function isPackageConnected(string $packageName): bool
{
return $this->connectedPackages[$packageName] ?? false;
}
/**
* @param string $moduleId
* @param string $status
*
* @return bool
*/
public function moduleIs(string $moduleId, string $status): bool
{
return in_array($moduleId, $this->moduleStatus[$status] ?? [], true);
}
/**
* Return the filter name to be used to extend modules of the plugin.
*
* If the plugin is single file `my-plugin.php` in plugins folder the filter name will be:
* `inpsyde.modularity.my-plugin`.
*
* If the plugin is in a sub-folder e.g. `my-plugin/index.php` the filter name will be:
* `inpsyde.modularity.my-plugin` anyway, so the file name is not relevant.
*
* @param string $suffix
*
* @return string
* @see Package::name()
*
*/
public function hookName(string $suffix = ''): string
{
$filter = self::HOOK_PREFIX . $this->properties->baseName();
if ($suffix) {
$filter .= '.' . $suffix;
}
return $filter;
}
/**
* @return Properties
*/
public function properties(): Properties
{
return $this->properties;
}
/**
* @return ContainerInterface
*
* @throws \Exception
*/
public function container(): ContainerInterface
{
$this->assertStatus(self::STATUS_INITIALIZED, 'obtain the container instance', '>=');
$this->hasContainer = true;
return $this->containerConfigurator->createReadOnlyContainer();
}
/**
* @return string
*/
public function name(): string
{
return $this->properties->baseName();
}
/**
* @param int $status
*/
private function progress(int $status): void
{
$this->status = $status;
}
/**
* @param int $status
*
* @return bool
*/
public function statusIs(int $status): bool
{
return $this->status === $status;
}
/**
* @param \Throwable $throwable
* @param Package::ACTION_FAILED_* $action
* @return void
* @throws \Throwable
*/
private function handleFailure(\Throwable $throwable, string $action): void
{
$this->progress(self::STATUS_FAILED);
$hook = $this->hookName($action);
did_action($hook) or do_action($hook, $throwable);
if ($this->properties->isDebug()) {
throw $throwable;
}
$this->lastError = $throwable;
}
/**
* @param int $status
* @param string $action
* @param string $operator
*
* @throws \Exception
* @psalm-suppress ArgumentTypeCoercion
*/
private function assertStatus(int $status, string $action, string $operator = '=='): void
{
if (!version_compare((string) $this->status, (string) $status, $operator)) {
throw new \Exception(
sprintf("Can't %s at this point of application.", $action),
0,
$this->lastError
);
}
}
/**
* Similar to WP's `_deprecated_argument()`, but executes regardless of WP_DEBUG and without
* translated message (so without attempting loading translation files).
*
* @param string $message
* @param string $function
* @param string $version
*
* @return void
*/
private function deprecatedArgument(string $message, string $function, string $version): void
{
do_action('deprecated_argument_run', $function, $message, $version);
if (apply_filters('deprecated_argument_trigger_error', true)) {
trigger_error($message, \E_USER_DEPRECATED);
}
}
}

View file

@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties;
class BaseProperties implements Properties
{
/**
* @var null|bool
*/
protected $isDebug = null;
/**
* @var string
*/
protected $baseName;
/**
* @var string
*/
protected $basePath;
/**
* @var string|null
*/
protected $baseUrl;
/**
* @var array
*/
protected $properties;
/**
* @param string $baseName
* @param string $basePath
* @param string|null $baseUrl
* @param array $properties
*/
protected function __construct(
string $baseName,
string $basePath,
string $baseUrl = null,
array $properties = []
) {
$baseName = $this->sanitizeBaseName($baseName);
$basePath = (string) trailingslashit($basePath);
if ($baseUrl) {
$baseUrl = (string) trailingslashit($baseUrl);
}
$this->baseName = $baseName;
$this->basePath = $basePath;
$this->baseUrl = $baseUrl;
$this->properties = array_replace(Properties::DEFAULT_PROPERTIES, $properties);
}
/**
* @param string $name
*
* @return string
*/
protected function sanitizeBaseName(string $name): string
{
substr_count($name, '/') and $name = dirname($name);
return strtolower(pathinfo($name, PATHINFO_FILENAME));
}
/**
* @return string
*/
public function baseName(): string
{
return $this->baseName;
}
/**
* @return string
*/
public function basePath(): string
{
return $this->basePath;
}
/**
* @return string|null
*/
public function baseUrl(): ?string
{
return $this->baseUrl;
}
/**
* @return string
*/
public function author(): string
{
return (string) $this->get(self::PROP_AUTHOR);
}
/**
* @return string
*/
public function authorUri(): string
{
return (string) $this->get(self::PROP_AUTHOR_URI);
}
/**
* @return string
*/
public function description(): string
{
return (string) $this->get(self::PROP_DESCRIPTION);
}
/**
* @return string
*/
public function textDomain(): string
{
return (string) $this->get(self::PROP_TEXTDOMAIN);
}
/**
* @return string
*/
public function domainPath(): string
{
return (string) $this->get(self::PROP_DOMAIN_PATH);
}
/**
* @return string
*/
public function name(): string
{
return (string) $this->get(self::PROP_NAME);
}
/**
* @return string
*/
public function uri(): string
{
return (string) $this->get(self::PROP_URI);
}
/**
* @return string
*/
public function version(): string
{
return (string) $this->get(self::PROP_VERSION);
}
/**
* @return string|null
*/
public function requiresWp(): ?string
{
$value = $this->get(self::PROP_REQUIRES_WP);
return $value && is_string($value) ? $value : null;
}
/**
* @return string|null
*/
public function requiresPhp(): ?string
{
$value = $this->get(self::PROP_REQUIRES_PHP);
return $value && is_string($value) ? $value : null;
}
/**
* @return array
*/
public function tags(): array
{
return (array) $this->get(self::PROP_TAGS);
}
/**
* @param string $key
* @param null $default
* @return mixed
*/
public function get(string $key, $default = null)
{
return $this->properties[$key] ?? $default;
}
/**
* @param string $key
* @return bool
*/
public function has(string $key): bool
{
return isset($this->properties[$key]);
}
/**
* @return bool
* @see Properties::isDebug()
*/
public function isDebug(): bool
{
if ($this->isDebug === null) {
$this->isDebug = defined('WP_DEBUG') && WP_DEBUG;
}
return $this->isDebug;
}
}

View file

@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties;
/**
* Class LibraryProperties
*
* @package WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties
*/
class LibraryProperties extends BaseProperties
{
/**
* Allowed configuration in composer.json "extra.modularity".
*
* @var array
*/
public const EXTRA_KEYS = [
self::PROP_DOMAIN_PATH,
self::PROP_NAME,
self::PROP_TEXTDOMAIN,
self::PROP_URI,
self::PROP_VERSION,
self::PROP_REQUIRES_WP,
];
/**
* @param string $composerJsonFile
* @param string|null $baseUrl
*
* @return LibraryProperties
*
* @throws \Exception
* @psalm-suppress MixedArrayAccess
*/
public static function new(string $composerJsonFile, ?string $baseUrl = null): LibraryProperties
{
if (!\is_file($composerJsonFile) || !\is_readable($composerJsonFile)) {
throw new \Exception("File {$composerJsonFile} does not exist or is not readable.");
}
$content = (string) file_get_contents($composerJsonFile);
/** @var array $composerJsonData */
$composerJsonData = json_decode($content, true);
$properties = Properties::DEFAULT_PROPERTIES;
$properties[self::PROP_DESCRIPTION] = $composerJsonData['description'] ?? '';
$properties[self::PROP_TAGS] = $composerJsonData['keywords'] ?? [];
$authors = $composerJsonData['authors'] ?? [];
$names = [];
foreach ((array) $authors as $author) {
$name = $author['name'] ?? null;
if ($name && is_string($name)) {
$names[] = $name;
}
$url = $author['homepage'] ?? null;
if ($url && !$properties['authorUri'] && is_string($url)) {
$properties[self::PROP_AUTHOR_URI] = $url;
}
}
if (count($names) > 0) {
$properties[self::PROP_AUTHOR] = implode(', ', $names);
}
// Custom settings which can be stored in composer.json "extra.modularity"
$extra = $composerJsonData['extra']['modularity'] ?? [];
foreach (self::EXTRA_KEYS as $key) {
$properties[$key] = $extra[$key] ?? '';
}
// PHP requirement in composer.json "require" or "require-dev"
$properties[self::PROP_REQUIRES_PHP] = self::extractPhpVersion($composerJsonData);
// composer.json might have "version" in root
$version = $composerJsonData['version'] ?? null;
if ($version && is_string($version)) {
$properties[self::PROP_VERSION] = $version;
}
[$baseName, $name] = static::buildNames($composerJsonData);
$basePath = dirname($composerJsonFile);
if (empty($properties[self::PROP_NAME])) {
$properties[self::PROP_NAME] = $name;
}
return new self(
$baseName,
$basePath,
$baseUrl,
$properties
);
}
/**
* @param array $composerJsonData
*
* @return array{string, string}
*/
private static function buildNames(array $composerJsonData): array
{
$composerName = (string) ($composerJsonData['name'] ?? '');
$packageNamePieces = explode('/', $composerName, 2);
$basename = implode('-', $packageNamePieces);
// "inpsyde/foo-bar-baz" => "Inpsyde Foo Bar Baz"
$name = mb_convert_case(
str_replace(['-', '_', '.'], ' ', implode(' ', $packageNamePieces)),
MB_CASE_TITLE
);
return [$basename, $name];
}
/**
* Check PHP version in require, require-dev.
*
* Attempt to parse requirements to find the _minimum_ accepted version (consistent with WP).
* Composer requirements are parsed in a way that, for example:
* `>=7.2` returns `7.2`
* `^7.3` returns `7.3`
* `5.6 || >= 7.1` returns `5.6`
* `>= 7.1 < 8` returns `7.1`
*
* @param array $composerData
* @param string $key
*
* @return string|null
*/
private static function extractPhpVersion(array $composerData, string $key = 'require'): ?string
{
$nextKey = ($key === 'require')
? 'require-dev'
: null;
$base = (array) ($composerData[$key] ?? []);
$requirement = $base['php'] ?? null;
$version = ($requirement && is_string($requirement))
? trim($requirement)
: null;
if (!$version) {
return $nextKey
? static::extractPhpVersion($composerData, $nextKey)
: null;
}
static $matcher;
$matcher or $matcher = static function (string $version): ?string {
$version = trim($version);
if (!$version) {
return null;
}
// versions range like `>= 7.2.4 < 8`
if (preg_match('{>=?([\s0-9\.]+)<}', $version, $matches)) {
return trim($matches[1], " \t\n\r\0\x0B.");
}
// aliases like `dev-src#abcde as 7.4`
if (preg_match('{as\s*([\s0-9\.]+)}', $version, $matches)) {
return trim($matches[1], " \t\n\r\0\x0B.");
}
// Basic requirements like 7.2, >=7.2, ^7.2, ~7.2
if (preg_match('{^(?:[>=\s~\^]+)?([0-9\.]+)}', $version, $matches)) {
return trim($matches[1], " \t\n\r\0\x0B.");
}
return null;
};
// support for simpler requirements like `7.3`, `>=7.4` or alternative like `5.6 || >=7`
$alternatives = explode('||', $version);
$found = null;
foreach ($alternatives as $alternative) {
/** @var callable(string):?string $matcher */
$itemFound = $matcher($alternative);
if ($itemFound && (!$found || version_compare($itemFound, $found, '<'))) {
$found = $itemFound;
}
}
if ($found) {
return $found;
}
return $nextKey
? static::extractPhpVersion($composerData, $nextKey)
: null;
}
/**
* @param string $url
*
* @return static
*
* @throws \Exception
*/
public function withBaseUrl(string $url): LibraryProperties
{
if ($this->baseUrl !== null) {
throw new \Exception(sprintf('%s::$baseUrl property is not overridable.', __CLASS__));
}
$this->baseUrl = trailingslashit($url);
return $this;
}
}

View file

@ -0,0 +1,176 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties;
/**
* Class PluginProperties
*
* @package WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties
*
* @psalm-suppress PossiblyFalseArgument, InvalidArgument
*/
class PluginProperties extends BaseProperties
{
/**
* Custom properties for Plugins.
*/
public const PROP_NETWORK = 'network';
/**
* Available methods of Properties::__call()
* from plugin headers.
*
* @link https://developer.wordpress.org/reference/functions/get_plugin_data/
*/
protected const HEADERS = [
self::PROP_AUTHOR => 'Author',
self::PROP_AUTHOR_URI => 'AuthorURI',
self::PROP_DESCRIPTION => 'Description',
self::PROP_DOMAIN_PATH => 'DomainPath',
self::PROP_NAME => 'Name',
self::PROP_TEXTDOMAIN => 'TextDomain',
self::PROP_URI => 'PluginURI',
self::PROP_VERSION => 'Version',
self::PROP_REQUIRES_WP => 'RequiresWP',
self::PROP_REQUIRES_PHP => 'RequiresPHP',
// additional headers
self::PROP_NETWORK => 'Network',
];
/**
* @var string
*/
private $pluginMainFile;
/**
* @var string
*/
private $pluginBaseName;
/**
* @var bool|null
*/
protected $isMu;
/**
* @var bool|null
*/
protected $isActive;
/**
* @var bool|null
*/
protected $isNetworkActive;
/**
* @param string $pluginMainFile
*
* @return PluginProperties
*/
public static function new(string $pluginMainFile): PluginProperties
{
return new self($pluginMainFile);
}
/**
* PluginProperties constructor.
*
* @param string $pluginMainFile
*/
protected function __construct(string $pluginMainFile)
{
if (!function_exists('get_plugin_data')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$pluginData = get_plugin_data($pluginMainFile);
$properties = Properties::DEFAULT_PROPERTIES;
// Map pluginData to internal structure.
foreach (self::HEADERS as $key => $pluginDataKey) {
$properties[$key] = $pluginData[$pluginDataKey] ?? '';
unset($pluginData[$pluginDataKey]);
}
$properties = array_merge($properties, $pluginData);
$this->pluginMainFile = wp_normalize_path($pluginMainFile);
$this->pluginBaseName = plugin_basename($pluginMainFile);
$basePath = plugin_dir_path($pluginMainFile);
$baseUrl = plugins_url('/', $pluginMainFile);
parent::__construct(
$this->pluginBaseName,
$basePath,
$baseUrl,
$properties
);
}
/**
* @return string
*/
public function pluginMainFile(): string
{
return $this->pluginMainFile;
}
/**
* @return bool
*
* @psalm-suppress PossiblyFalseArgument
*/
public function network(): bool
{
return (bool) $this->get(self::PROP_NETWORK, false);
}
/**
* @return bool
*/
public function isActive(): bool
{
if ($this->isActive === null) {
if (!function_exists('is_plugin_active')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$this->isActive = is_plugin_active($this->pluginBaseName);
}
return $this->isActive;
}
/**
* @return bool
*/
public function isNetworkActive(): bool
{
if ($this->isNetworkActive === null) {
if (!function_exists('is_plugin_active_for_network')) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$this->isNetworkActive = is_plugin_active_for_network($this->pluginBaseName);
}
return $this->isNetworkActive;
}
/**
* @return bool
*/
public function isMuPlugin(): bool
{
if ($this->isMu === null) {
/**
* @psalm-suppress UndefinedConstant
* @psalm-suppress MixedArgument
*/
$muPluginDir = wp_normalize_path(WPMU_PLUGIN_DIR);
$this->isMu = strpos($this->pluginMainFile, $muPluginDir) === 0;
}
return $this->isMu;
}
}

View file

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties;
interface Properties
{
public const PROP_AUTHOR = 'author';
public const PROP_AUTHOR_URI = 'authorUri';
public const PROP_DESCRIPTION = 'description';
public const PROP_DOMAIN_PATH = 'domainPath';
public const PROP_NAME = 'name';
public const PROP_TEXTDOMAIN = 'textDomain';
public const PROP_URI = 'uri';
public const PROP_VERSION = 'version';
public const PROP_REQUIRES_WP = 'requiresWp';
public const PROP_REQUIRES_PHP = 'requiresPhp';
public const PROP_TAGS = 'tags';
/**
* @var array
*/
public const DEFAULT_PROPERTIES = [
self::PROP_AUTHOR => '',
self::PROP_AUTHOR_URI => '',
self::PROP_DESCRIPTION => '',
self::PROP_DOMAIN_PATH => '',
self::PROP_NAME => '',
self::PROP_TEXTDOMAIN => '',
self::PROP_URI => '',
self::PROP_VERSION => '',
self::PROP_REQUIRES_WP => null,
self::PROP_REQUIRES_PHP => null,
self::PROP_TAGS => [],
];
/**
* @param string $key
* @param null $default
*
* @return mixed
*/
public function get(string $key, $default = null);
/**
* @param string $key
*
* @return bool
*/
public function has(string $key): bool;
/**
* @return bool
*/
public function isDebug(): bool;
/**
* @return string
*/
public function baseName(): string;
/**
* @return string
*/
public function basePath(): string;
/**
* @return string|null
*/
public function baseUrl(): ?string;
/**
* @return string
*/
public function author(): string;
/**
* @return string
*/
public function authorUri(): string;
/**
* @return string
*/
public function description(): string;
/**
* @return string
*/
public function textDomain(): string;
/**
* @return string
*/
public function domainPath(): string;
/**
* The name of the plugin, theme or library.
*
* @return string
*/
public function name(): string;
/**
* The home page of the plugin, theme or library.
* @return string
*/
public function uri(): string;
/**
* @return string
*/
public function version(): string;
/**
* Optional. Specify the minimum required WordPress version.
*
* @return string|null
*/
public function requiresWp(): ?string;
/**
* Optional. Specify the minimum required PHP version.
*
* @return string
*/
public function requiresPhp(): ?string;
/**
* Optional. Currently, only available for Theme and Library.
* Plugins do not have support for "tags"/"keywords" in header.
*
* @link https://developer.wordpress.org/reference/classes/wp_theme/#properties
* @link https://getcomposer.org/doc/04-schema.md#keywords
*
* @return array
*/
public function tags(): array;
}

View file

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties;
/**
* Class ThemeProperties
*
* @package WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties
*
* @psalm-suppress PossiblyFalseArgument, InvalidArgument
*/
class ThemeProperties extends BaseProperties
{
/**
* Additional properties specific for themes.
*/
public const PROP_STATUS = 'status';
public const PROP_TEMPLATE = 'template';
/**
* Available methods of Properties::__call()
* from theme headers.
*
* @link https://developer.wordpress.org/reference/classes/wp_theme/
*/
protected const HEADERS = [
self::PROP_AUTHOR => 'Author',
self::PROP_AUTHOR_URI => 'AuthorURI',
self::PROP_DESCRIPTION => 'Description',
self::PROP_DOMAIN_PATH => 'DomainPath',
self::PROP_NAME => 'Name',
self::PROP_TEXTDOMAIN => 'TextDomain',
self::PROP_URI => 'ThemeURI',
self::PROP_VERSION => 'Version',
self::PROP_REQUIRES_WP => 'RequiresWP',
self::PROP_REQUIRES_PHP => 'RequiresPHP',
// additional headers
self::PROP_STATUS => 'Status',
self::PROP_TAGS => 'Tags',
self::PROP_TEMPLATE => 'Template',
];
/**
* @param string $themeDirectory
*
* @return ThemeProperties
*/
public static function new(string $themeDirectory): ThemeProperties
{
return new self($themeDirectory);
}
/**
* ThemeProperties constructor.
*
* @param string $themeDirectory
*/
protected function __construct(string $themeDirectory)
{
if (!function_exists('wp_get_theme')) {
require_once ABSPATH . 'wp-includes/theme.php';
}
$theme = wp_get_theme($themeDirectory);
$properties = Properties::DEFAULT_PROPERTIES;
foreach (self::HEADERS as $key => $themeKey) {
/** @psalm-suppress DocblockTypeContradiction */
$properties[$key] = $theme->get($themeKey) ?? '';
}
$baseName = $theme->get_stylesheet();
$basePath = $theme->get_stylesheet_directory();
$baseUrl = (string) trailingslashit($theme->get_stylesheet_directory_uri());
parent::__construct(
$baseName,
$basePath,
$baseUrl,
$properties
);
}
/**
* If the theme is published.
*
* @return string
*/
public function status(): string
{
return (string) $this->get(self::PROP_STATUS);
}
public function template(): string
{
return (string) $this->get(self::PROP_TEMPLATE);
}
/**
* @return bool
*/
public function isChildTheme(): bool
{
return (bool) $this->template();
}
/**
* @return bool
*/
public function isCurrentTheme(): bool
{
return get_stylesheet() === $this->baseName();
}
/**
* @return ThemeProperties|null
*/
public function parentThemeProperties(): ?ThemeProperties
{
$template = $this->template();
if (!$template) {
return null;
}
$parent = wp_get_theme($template, get_theme_root($template));
return static::new($parent->get_template_directory());
}
}

View file

@ -1,7 +1,4 @@
<?php
/**
* @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
*/
namespace WooCommerce\PayPalCommerce\Vendor\Psr\Container;

View file

@ -1,7 +1,6 @@
<?php
/**
* @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
*/
declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Vendor\Psr\Container;
@ -20,7 +19,7 @@ interface ContainerInterface
*
* @return mixed Entry.
*/
public function get($id);
public function get(string $id);
/**
* Returns true if the container can return an entry for the given identifier.
@ -33,5 +32,5 @@ interface ContainerInterface
*
* @return bool
*/
public function has($id);
public function has(string $id);
}

View file

@ -1,7 +1,4 @@
<?php
/**
* @license http://www.opensource.org/licenses/mit-license.php MIT (see the LICENSE file)
*/
namespace WooCommerce\PayPalCommerce\Vendor\Psr\Container;

View file

@ -31,6 +31,7 @@ return function ( string $root_dir ): iterable {
( require "$modules_dir/ppcp-uninstall/module.php" )(),
( require "$modules_dir/ppcp-blocks/module.php" )(),
( require "$modules_dir/ppcp-paypal-subscriptions/module.php" )(),
( require "$modules_dir/ppcp-local-alternative-payment-methods/module.php" )(),
);
// phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
@ -88,12 +89,5 @@ return function ( string $root_dir ): iterable {
$modules[] = ( require "$modules_dir/ppcp-axo/module.php" )();
}
if ( apply_filters(
'woocommerce.feature-flags.woocommerce_paypal_payments.local_apms_enabled',
getenv( 'PCP_LOCAL_APMS_ENABLED' ) === '1'
) ) {
$modules[] = ( require "$modules_dir/ppcp-local-alternative-payment-methods/module.php" )();
}
return $modules;
};

View file

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

View file

@ -4,7 +4,7 @@
"description": "Admin notices module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\AdminNotices;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): AdminNotices {
return new AdminNotices();
};

View file

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

View file

@ -0,0 +1,10 @@
.notice.is-dismissible {
.spinner.doing-ajax {
position: absolute;
z-index: 1;
right: 0;
top: 0;
margin: 9px;
pointer-events: none;
}
}

View file

@ -0,0 +1,137 @@
export default class DismissibleMessage {
#notice = null;
#muteConfig = {};
#closeButton = null;
#msgId = '';
constructor( noticeElement, muteConfig ) {
this.#notice = noticeElement;
this.#muteConfig = muteConfig;
this.#msgId = this.#notice.dataset.ppcpMsgId;
// Quick sanitation.
if ( ! this.#muteConfig?.endpoint || ! this.#muteConfig?.nonce ) {
console.error( 'Ajax config (Mute):', this.#muteConfig );
throw new Error(
'Invalid ajax configuration for DismissibleMessage. Nonce/Endpoint missing'
);
}
if ( ! this.#msgId ) {
console.error( 'Notice Element:', this.#notice );
throw new Error(
'Invalid notice element passed to DismissibleMessage. No MsgId defined'
);
}
this.onDismissClickProxy = this.onDismissClickProxy.bind( this );
this.enableCloseButtons = this.enableCloseButtons.bind( this );
this.disableCloseButtons = this.disableCloseButtons.bind( this );
this.dismiss = this.dismiss.bind( this );
this.addEventListeners();
}
get id() {
return this.#msgId;
}
get closeButton() {
if ( ! this.#closeButton ) {
this.#closeButton = this.#notice.querySelector(
'button.notice-dismiss'
);
}
return this.#closeButton;
}
addEventListeners() {
this.#notice.addEventListener(
'click',
this.onDismissClickProxy,
true
);
}
removeEventListeners() {
this.#notice.removeEventListener(
'click',
this.onDismissClickProxy,
true
);
}
onDismissClickProxy( event ) {
if ( ! event.target?.matches( 'button.notice-dismiss' ) ) {
return;
}
this.disableCloseButtons();
this.muteMessage();
event.preventDefault();
event.stopPropagation();
return false;
}
disableCloseButtons() {
this.closeButton.setAttribute( 'disabled', 'disabled' );
this.closeButton.style.pointerEvents = 'none';
this.closeButton.style.opacity = 0;
}
enableCloseButtons() {
this.closeButton.removeAttribute( 'disabled', 'disabled' );
this.closeButton.style.pointerEvents = '';
this.closeButton.style.opacity = '';
}
showSpinner() {
const spinner = document.createElement( 'span' );
spinner.classList.add( 'spinner', 'is-active', 'doing-ajax' );
this.#notice.appendChild( spinner );
}
/**
* Mute the message (on server side) and dismiss it (in browser).
*/
muteMessage() {
this.#ajaxMuteMessage().then( this.dismiss );
}
/**
* Start an ajax request that marks the message as "muted" on server side.
*
* @return {Promise<any>} Resolves after the ajax request is completed.
*/
#ajaxMuteMessage() {
this.showSpinner();
const ajaxData = {
id: this.id,
nonce: this.#muteConfig.nonce,
};
return fetch( this.#muteConfig.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify( ajaxData ),
} ).then( ( response ) => response.json() );
}
/**
* Proxy to the original dismiss logic provided by WP core JS.
*/
dismiss() {
this.removeEventListeners();
this.enableCloseButtons();
this.closeButton.dispatchEvent( new Event( 'click' ) );
}
}

View file

@ -0,0 +1,27 @@
import DismissibleMessage from './DismissibleMessage';
class AdminMessageHandler {
#config = {};
constructor( config ) {
this.#config = config;
this.setupDismissibleMessages();
}
/**
* Finds all mutable admin messages in the DOM and initializes them.
*/
setupDismissibleMessages() {
const muteConfig = this.#config?.ajax?.mute_message;
const addDismissibleMessage = ( element ) => {
new DismissibleMessage( element, muteConfig );
};
document
.querySelectorAll( '.notice[data-ppcp-msg-id]' )
.forEach( addDismissibleMessage );
}
}
new AdminMessageHandler( window.wc_admin_notices );

View file

@ -14,15 +14,33 @@ use WooCommerce\PayPalCommerce\AdminNotices\Renderer\Renderer;
use WooCommerce\PayPalCommerce\AdminNotices\Renderer\RendererInterface;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\RepositoryInterface;
use WooCommerce\PayPalCommerce\AdminNotices\Endpoint\MuteMessageEndpoint;
return array(
'admin-notices.renderer' => static function ( ContainerInterface $container ): RendererInterface {
$repository = $container->get( 'admin-notices.repository' );
return new Renderer( $repository );
'admin-notices.url' => static function ( ContainerInterface $container ): string {
$path = realpath( __FILE__ );
if ( false === $path ) {
return '';
}
return plugins_url(
'/modules/ppcp-admin-notices/',
dirname( $path, 3 ) . '/woocommerce-paypal-payments.php'
);
},
'admin-notices.repository' => static function ( ContainerInterface $container ): RepositoryInterface {
'admin-notices.renderer' => static function ( ContainerInterface $container ): RendererInterface {
return new Renderer(
$container->get( 'admin-notices.repository' ),
$container->get( 'admin-notices.url' ),
$container->get( 'ppcp.asset-version' )
);
},
'admin-notices.repository' => static function ( ContainerInterface $container ): RepositoryInterface {
return new Repository();
},
'admin-notices.mute-message-endpoint' => static function ( ContainerInterface $container ): MuteMessageEndpoint {
return new MuteMessageEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'admin-notices.repository' )
);
},
);

View file

@ -9,36 +9,46 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\AdminNotices;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\AdminNotices\Endpoint\MuteMessageEndpoint;
use WooCommerce\PayPalCommerce\AdminNotices\Renderer\RendererInterface;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\PersistentMessage;
/**
* Class AdminNotices
*/
class AdminNotices implements ModuleInterface {
class AdminNotices implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
$renderer = $c->get( 'admin-notices.renderer' );
assert( $renderer instanceof RendererInterface );
add_action(
'admin_notices',
function() use ( $c ) {
$renderer = $c->get( 'admin-notices.renderer' );
function() use ( $renderer ) {
$renderer->render();
}
);
@ -70,13 +80,36 @@ class AdminNotices implements ModuleInterface {
return $notices;
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
/**
* Since admin notices are rendered after the initial `admin_enqueue_scripts`
* action fires, we use the `admin_footer` hook to enqueue the optional assets
* for admin-notices in the page footer.
*/
add_action(
'admin_footer',
static function () use ( $renderer ) {
$renderer->enqueue_admin();
}
);
add_action(
'wp_ajax_' . MuteMessageEndpoint::ENDPOINT,
static function () use ( $c ) {
$endpoint = $c->get( 'admin-notices.mute-message-endpoint' );
assert( $endpoint instanceof MuteMessageEndpoint );
$endpoint->handle_request();
}
);
add_action(
'woocommerce_paypal_payments_uninstall',
static function () {
PersistentMessage::clear_all();
}
);
return true;
}
}

View file

@ -0,0 +1,89 @@
<?php
/**
* Permanently mutes an admin notification for the current user.
*
* @package WooCommerce\PayPalCommerce\AdminNotices\Endpoint
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AdminNotices\Endpoint;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\Repository;
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
use WooCommerce\PayPalCommerce\Button\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\PersistentMessage;
/**
* Class MuteMessageEndpoint
*/
class MuteMessageEndpoint {
const ENDPOINT = 'ppc-mute-message';
/**
* The request data helper.
*
* @var RequestData
*/
private $request_data;
/**
* Message repository to retrieve the message object to mute.
*
* @var Repository
*/
private $message_repository;
/**
* UpdateShippingEndpoint constructor.
*
* @param RequestData $request_data The Request Data Helper.
* @param Repository $message_repository Message repository, to access messages.
*/
public function __construct(
RequestData $request_data,
Repository $message_repository
) {
$this->request_data = $request_data;
$this->message_repository = $message_repository;
}
/**
* Returns the nonce.
*
* @return string
*/
public static function nonce() : string {
return self::ENDPOINT;
}
/**
* Handles the request.
*
* @return void
*/
public function handle_request() : void {
try {
$data = $this->request_data->read_request( $this->nonce() );
} catch ( RuntimeException $ex ) {
wp_send_json_error();
}
$id = $data['id'] ?? '';
if ( ! $id || ! is_string( $id ) ) {
wp_send_json_error();
}
/**
* Create a dummy message with the provided ID and mark it as muted.
*
* This helps to keep code cleaner and make the mute-endpoint more reliable,
* as other modules do not need to register the PersistentMessage on every
* ajax request.
*/
$message = new PersistentMessage( $id, '', '', '' );
$message->mute();
wp_send_json_success();
}
}

View file

@ -5,7 +5,7 @@
* @package WooCommerce\PayPalCommerce\AdminNotices\Entity
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AdminNotices\Entity;
@ -15,7 +15,7 @@ namespace WooCommerce\PayPalCommerce\AdminNotices\Entity;
class Message {
/**
* The messagte text.
* The message text.
*
* @var string
*/
@ -29,11 +29,11 @@ class Message {
private $type;
/**
* Whether the message is dismissable.
* Whether the message is dismissible.
*
* @var bool
*/
private $dismissable;
private $dismissible;
/**
* The wrapper selector that will contain the notice.
@ -45,15 +45,15 @@ class Message {
/**
* Message constructor.
*
* @param string $message The message text.
* @param string $type The message type.
* @param bool $dismissable Whether the message is dismissable.
* @param string $wrapper The wrapper selector that will contain the notice.
* @param string $message The message text.
* @param string $type The message type.
* @param bool $dismissible Whether the message is dismissible.
* @param string $wrapper The wrapper selector that will contain the notice.
*/
public function __construct( string $message, string $type, bool $dismissable = true, string $wrapper = '' ) {
public function __construct( string $message, string $type, bool $dismissible = true, string $wrapper = '' ) {
$this->type = $type;
$this->message = $message;
$this->dismissable = $dismissable;
$this->dismissible = $dismissible;
$this->wrapper = $wrapper;
}
@ -62,7 +62,7 @@ class Message {
*
* @return string
*/
public function message(): string {
public function message() : string {
return $this->message;
}
@ -71,17 +71,17 @@ class Message {
*
* @return string
*/
public function type(): string {
public function type() : string {
return $this->type;
}
/**
* Returns whether the message is dismissable.
* Returns whether the message is dismissible.
*
* @return bool
*/
public function is_dismissable(): bool {
return $this->dismissable;
public function is_dismissible() : bool {
return $this->dismissible;
}
/**
@ -89,21 +89,37 @@ class Message {
*
* @return string
*/
public function wrapper(): string {
public function wrapper() : string {
return $this->wrapper;
}
/**
* Returns the object as array.
* Returns the object as array, for serialization.
*
* @return array
*/
public function to_array(): array {
public function to_array() : array {
return array(
'type' => $this->type,
'message' => $this->message,
'dismissable' => $this->dismissable,
'dismissible' => $this->dismissible,
'wrapper' => $this->wrapper,
);
}
/**
* Converts a plain array to a full Message instance, during deserialization.
*
* @param array $data Data generated by `Message::to_array()`.
*
* @return Message
*/
public static function from_array( array $data ) : Message {
return new Message(
(string) ( $data['message'] ?? '' ),
(string) ( $data['type'] ?? '' ),
(bool) ( $data['dismissible'] ?? true ),
(string) ( $data['wrapper'] ?? '' )
);
}
}

View file

@ -0,0 +1,125 @@
<?php
/**
* Extends the Message class to permanently dismiss notices for single users.
*
* @package WooCommerce\PayPalCommerce\AdminNotices\Entity
*/
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AdminNotices\Entity;
/**
* Class PersistentMessage
*/
class PersistentMessage extends Message {
/**
* Prefix for DB keys to store IDs of permanently muted notices.
*/
public const USER_META_PREFIX = '_ppcp_notice_';
/**
* An internal ID to permanently dismiss the persistent message.
*
* @var string
*/
private $message_id;
/**
* Message constructor.
*
* @param string $id ID of this message, to allow permanent dismissal.
* @param string $message The message text.
* @param string $type The message type.
* @param string $wrapper The wrapper selector that will contain the notice.
*/
public function __construct( string $id, string $message, string $type, string $wrapper = '' ) {
parent::__construct( $message, $type, true, $wrapper );
$this->message_id = sanitize_key( $id );
}
/**
* Returns the sanitized ID that identifies a permanently dismissible message.
*
* @param bool $with_db_prefix Whether to add the user-meta prefix.
*
* @return string
*/
public function id( bool $with_db_prefix = false ) : string {
if ( ! $this->message_id ) {
return '';
}
return $with_db_prefix ? self::USER_META_PREFIX . $this->message_id : $this->message_id;
}
/**
* {@inheritDoc}
*/
public function to_array() : array {
$data = parent::to_array();
$data['id'] = $this->message_id;
return $data;
}
/**
* {@inheritDoc}
*
* @return PersistentMessage
*/
public static function from_array( array $data ) : Message {
return new PersistentMessage(
(string) ( $data['id'] ?? '' ),
(string) ( $data['message'] ?? '' ),
(string) ( $data['type'] ?? '' ),
(string) ( $data['wrapper'] ?? '' )
);
}
/**
* Whether the message was permanently muted by the current user.
*
* @return bool
*/
public function is_muted() : bool {
$user_id = get_current_user_id();
if ( ! $this->message_id || ! $user_id ) {
return false;
}
return 0 < (int) get_user_meta( $user_id, $this->id( true ), true );
}
/**
* Mark the message as permanently muted by the current user.
*
* @return void
*/
public function mute() : void {
$user_id = get_current_user_id();
if ( $this->message_id && $user_id && ! $this->is_muted() ) {
update_user_meta( $user_id, $this->id( true ), time() );
}
}
/**
* Removes all user-meta flags for muted messages.
*
* @return void
*/
public static function clear_all() : void {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM $wpdb->usermeta WHERE meta_key LIKE %s",
$wpdb->esc_like( self::USER_META_PREFIX ) . '%'
)
);
}
}

View file

@ -10,6 +10,8 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\AdminNotices\Renderer;
use WooCommerce\PayPalCommerce\AdminNotices\Repository\RepositoryInterface;
use WooCommerce\PayPalCommerce\AdminNotices\Endpoint\MuteMessageEndpoint;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\PersistentMessage;
/**
* Class Renderer
@ -23,32 +25,123 @@ class Renderer implements RendererInterface {
*/
private $repository;
/**
* Used to enqueue assets.
*
* @var string
*/
private $module_url;
/**
* Used to enqueue assets.
*
* @var string
*/
private $version;
/**
* Whether the current page contains at least one message that can be muted.
*
* @var bool
*/
private $can_mute_message = false;
/**
* Renderer constructor.
*
* @param RepositoryInterface $repository The message repository.
* @param string $module_url The module URL.
* @param string $version The module version.
*/
public function __construct( RepositoryInterface $repository ) {
public function __construct(
RepositoryInterface $repository,
string $module_url,
string $version
) {
$this->repository = $repository;
$this->module_url = untrailingslashit( $module_url );
$this->version = $version;
}
/**
* Renders the current messages.
*
* @return bool
* {@inheritDoc}
*/
public function render(): bool {
$messages = $this->repository->current_message();
foreach ( $messages as $message ) {
$mute_message_id = '';
if ( $message instanceof PersistentMessage ) {
$this->can_mute_message = true;
$mute_message_id = $message->id();
}
printf(
'<div class="notice notice-%s %s" %s><p>%s</p></div>',
'<div class="notice notice-%s %s" %s%s><p>%s</p></div>',
$message->type(),
( $message->is_dismissable() ) ? 'is-dismissible' : '',
( $message->is_dismissible() ) ? 'is-dismissible' : '',
( $message->wrapper() ? sprintf( 'data-ppcp-wrapper="%s"', esc_attr( $message->wrapper() ) ) : '' ),
// Use `empty()` in condition, to avoid false phpcs warning.
( empty( $mute_message_id ) ? '' : sprintf( 'data-ppcp-msg-id="%s"', esc_attr( $mute_message_id ) ) ),
wp_kses_post( $message->message() )
);
}
return (bool) count( $messages );
}
/**
* {@inheritDoc}
*/
public function enqueue_admin() : void {
if ( ! $this->can_mute_message ) {
return;
}
wp_register_style(
'wc-ppcp-admin-notice',
$this->module_url . '/assets/css/styles.css',
array(),
$this->version
);
wp_register_script(
'wc-ppcp-admin-notice',
$this->module_url . '/assets/js/boot-admin.js',
array(),
$this->version,
true
);
wp_localize_script(
'wc-ppcp-admin-notice',
'wc_admin_notices',
$this->script_data_for_admin()
);
wp_enqueue_style( 'wc-ppcp-admin-notice' );
wp_enqueue_script( 'wc-ppcp-admin-notice' );
}
/**
* Data to inject into the current admin page, which is required by JS assets.
*
* @return array
*/
protected function script_data_for_admin() : array {
$ajax_url = admin_url( 'admin-ajax.php' );
return array(
'ajax' => array(
'mute_message' => array(
'endpoint' => add_query_arg(
array( 'action' => MuteMessageEndpoint::ENDPOINT ),
$ajax_url
),
'nonce' => wp_create_nonce( MuteMessageEndpoint::nonce() ),
),
),
);
}
}

View file

@ -20,4 +20,11 @@ interface RendererInterface {
* @return bool
*/
public function render(): bool;
/**
* Enqueues common assets required for the admin notice behavior.
*
* @return void
*/
public function enqueue_admin() : void;
}

View file

@ -5,11 +5,12 @@
* @package WooCommerce\PayPalCommerce\AdminNotices\Repository
*/
declare(strict_types=1);
declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\AdminNotices\Repository;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\Message;
use WooCommerce\PayPalCommerce\AdminNotices\Entity\PersistentMessage;
/**
* Class Repository
@ -20,11 +21,11 @@ class Repository implements RepositoryInterface {
const PERSISTED_NOTICES_OPTION = 'woocommerce_ppcp-admin-notices';
/**
* Returns the current messages.
* Returns current messages to display, which excludes muted messages.
*
* @return Message[]
*/
public function current_message(): array {
public function current_message() : array {
return array_filter(
/**
* Returns the list of admin messages.
@ -33,7 +34,11 @@ class Repository implements RepositoryInterface {
self::NOTICES_FILTER,
array()
),
function( $element ) : bool {
function ( $element ) : bool {
if ( $element instanceof PersistentMessage ) {
return ! $element->is_muted();
}
return is_a( $element, Message::class );
}
);
@ -43,9 +48,10 @@ class Repository implements RepositoryInterface {
* Adds a message to persist between page reloads.
*
* @param Message $message The message.
*
* @return void
*/
public function persist( Message $message ): void {
public function persist( Message $message ) : void {
$persisted_notices = get_option( self::PERSISTED_NOTICES_OPTION ) ?: array();
$persisted_notices[] = $message->to_array();
@ -58,20 +64,18 @@ class Repository implements RepositoryInterface {
*
* @return array|Message[]
*/
public function get_persisted_and_clear(): array {
public function get_persisted_and_clear() : array {
$notices = array();
$persisted_data = get_option( self::PERSISTED_NOTICES_OPTION ) ?: array();
foreach ( $persisted_data as $notice_data ) {
$notices[] = new Message(
(string) ( $notice_data['message'] ?? '' ),
(string) ( $notice_data['type'] ?? '' ),
(bool) ( $notice_data['dismissable'] ?? true ),
(string) ( $notice_data['wrapper'] ?? '' )
);
if ( is_array( $notice_data ) ) {
$notices[] = Message::from_array( $notice_data );
}
}
update_option( self::PERSISTED_NOTICES_OPTION, array(), true );
return $notices;
}
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@
"description": "API client module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\ApiClient;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return function (): ModuleInterface {
return function (): ApiModule {
return new ApiModule();
};

View file

@ -1642,6 +1642,21 @@ return array(
'SE',
);
},
'api.paylater-countries' => static function ( ContainerInterface $container ) : array {
return apply_filters(
'woocommerce_paypal_payments_supported_paylater_countries',
array(
'US',
'DE',
'GB',
'FR',
'AU',
'IT',
'ES',
)
);
},
'api.order-helper' => static function( ContainerInterface $container ): OrderHelper {
return new OrderHelper();
},

View file

@ -14,31 +14,37 @@ use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\ApiClient\Entity\Order;
/**
* Class ApiModule
*/
class ApiModule implements ModuleInterface {
class ApiModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
add_action(
'woocommerce_after_calculate_totals',
function ( \WC_Cart $cart ) {
@ -96,13 +102,7 @@ class ApiModule implements ModuleInterface {
10,
2
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
return true;
}
}

View file

@ -96,9 +96,107 @@ class Orders {
}
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( $status_code !== 200 ) {
if ( ! in_array( $status_code, array( 200, 201 ), true ) ) {
$body = json_decode( $response['body'] );
$message = $body->details[0]->description ?? '';
if ( $message ) {
throw new RuntimeException( $message );
}
throw new PayPalApiException(
json_decode( $response['body'] ),
$body,
$status_code
);
}
return $response;
}
/**
* Confirms the given order.
*
* @link https://developer.paypal.com/docs/api/orders/v2/#orders_confirm
*
* @param array $request_body The request body.
* @param string $id PayPal order ID.
* @return array
* @throws RuntimeException If something went wrong with the request.
* @throws PayPalApiException If something went wrong with the PayPal API request.
*/
public function confirm_payment_source( array $request_body, string $id ): array {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $id . '/confirm-payment-source';
$args = array(
'method' => 'POST',
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
'body' => wp_json_encode( $request_body ),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( $status_code !== 200 ) {
$body = json_decode( $response['body'] );
$message = $body->details[0]->description ?? '';
if ( $message ) {
throw new RuntimeException( $message );
}
throw new PayPalApiException(
$body,
$status_code
);
}
return $response;
}
/**
* Get PayPal order by id.
*
* @param string $id PayPal order ID.
* @return array
* @throws RuntimeException If something went wrong with the request.
* @throws PayPalApiException If something went wrong with the PayPal API request.
*/
public function order( string $id ): array {
$bearer = $this->bearer->bearer();
$url = trailingslashit( $this->host ) . 'v2/checkout/orders/' . $id;
$args = array(
'headers' => array(
'Authorization' => 'Bearer ' . $bearer->token(),
'Content-Type' => 'application/json',
'PayPal-Request-Id' => uniqid( 'ppcp-', true ),
),
);
$response = $this->request( $url, $args );
if ( $response instanceof WP_Error ) {
throw new RuntimeException( $response->get_error_message() );
}
$status_code = (int) wp_remote_retrieve_response_code( $response );
if ( $status_code !== 200 ) {
$body = json_decode( $response['body'] );
$message = $body->details[0]->description ?? '';
if ( $message ) {
throw new RuntimeException( $message );
}
throw new PayPalApiException(
$body,
$status_code
);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="78.102px" height="50px" viewBox="-5 -5 175.52107 115.9651" enable-background="new 0 0 165.52107 105.9651"
xml:space="preserve">
<g>
<path id="XMLID_4_" d="M150.69807,0H14.82318c-0.5659,0-1.1328,0-1.69769,0.0033c-0.47751,0.0034-0.95391,0.0087-1.43031,0.0217
c-1.039,0.0281-2.0869,0.0894-3.1129,0.2738c-1.0424,0.1876-2.0124,0.4936-2.9587,0.9754
c-0.9303,0.4731-1.782,1.0919-2.52009,1.8303c-0.73841,0.7384-1.35721,1.5887-1.83021,2.52
c-0.4819,0.9463-0.7881,1.9166-0.9744,2.9598c-0.18539,1.0263-0.2471,2.074-0.2751,3.1119
c-0.0128,0.4764-0.01829,0.9528-0.0214,1.4291c-0.0033,0.5661-0.0022,1.1318-0.0022,1.6989V91.142
c0,0.5671-0.0011,1.13181,0.0022,1.69901c0.00311,0.4763,0.0086,0.9527,0.0214,1.4291
c0.028,1.03699,0.08971,2.08469,0.2751,3.11069c0.1863,1.0436,0.4925,2.0135,0.9744,2.9599
c0.473,0.9313,1.0918,1.7827,1.83021,2.52c0.73809,0.7396,1.58979,1.3583,2.52009,1.8302
c0.9463,0.4831,1.9163,0.7892,2.9587,0.9767c1.026,0.1832,2.0739,0.2456,3.1129,0.2737c0.4764,0.0108,0.9528,0.0172,1.43031,0.0194
c0.56489,0.0044,1.13179,0.0044,1.69769,0.0044h135.87489c0.5649,0,1.13181,0,1.69659-0.0044
c0.47641-0.0022,0.95282-0.0086,1.4314-0.0194c1.0368-0.0281,2.0845-0.0905,3.11301-0.2737
c1.041-0.1875,2.0112-0.4936,2.9576-0.9767c0.9313-0.4719,1.7805-1.0906,2.52011-1.8302c0.7372-0.7373,1.35599-1.5887,1.8302-2.52
c0.48299-0.9464,0.78889-1.9163,0.97429-2.9599c0.1855-1.026,0.2457-2.0737,0.2738-3.11069
c0.013-0.4764,0.01941-0.9528,0.02161-1.4291c0.00439-0.5672,0.00439-1.1319,0.00439-1.69901V14.8242
c0-0.5671,0-1.1328-0.00439-1.6989c-0.0022-0.4763-0.00861-0.9527-0.02161-1.4291c-0.02811-1.0379-0.0883-2.0856-0.2738-3.1119
c-0.18539-1.0432-0.4913-2.0135-0.97429-2.9598c-0.47421-0.9313-1.093-1.7816-1.8302-2.52
c-0.73961-0.7384-1.58881-1.3572-2.52011-1.8303c-0.9464-0.4818-1.9166-0.7878-2.9576-0.9754
c-1.0285-0.1844-2.0762-0.2457-3.11301-0.2738c-0.47858-0.013-0.95499-0.0183-1.4314-0.0217C151.82988,0,151.26297,0,150.69807,0
L150.69807,0z"/>
<path id="XMLID_3_" fill="#FFFFFF" d="M150.69807,3.532l1.67149,0.0032c0.4528,0.0032,0.90561,0.0081,1.36092,0.0205
c0.79201,0.0214,1.71849,0.0643,2.58209,0.2191c0.7507,0.1352,1.38029,0.3408,1.9845,0.6484
c0.5965,0.3031,1.14301,0.7003,1.62019,1.1768c0.479,0.4797,0.87671,1.0271,1.18381,1.6302
c0.30589,0.5995,0.51019,1.2261,0.64459,1.9823c0.1544,0.8542,0.1971,1.7832,0.21881,2.5801
c0.01219,0.4498,0.01819,0.8996,0.0204,1.3601c0.00429,0.5569,0.0042,1.1135,0.0042,1.6715V91.142
c0,0.558,0.00009,1.1136-0.0043,1.6824c-0.00211,0.4497-0.0081,0.8995-0.0204,1.3501c-0.02161,0.7957-0.0643,1.7242-0.2206,2.5885
c-0.13251,0.7458-0.3367,1.3725-0.64429,1.975c-0.30621,0.6016-0.70331,1.1484-1.18022,1.6251
c-0.47989,0.48-1.0246,0.876-1.62819,1.1819c-0.5997,0.3061-1.22821,0.51151-1.97151,0.6453
c-0.88109,0.157-1.84639,0.2002-2.57339,0.2199c-0.4574,0.0103-0.9126,0.01649-1.37889,0.0187
c-0.55571,0.0043-1.1134,0.0042-1.6692,0.0042H14.82318c-0.0074,0-0.0146,0-0.0221,0c-0.5494,0-1.0999,0-1.6593-0.0043
c-0.4561-0.00211-0.9112-0.0082-1.3512-0.0182c-0.7436-0.0201-1.7095-0.0632-2.5834-0.2193
c-0.74969-0.1348-1.3782-0.3402-1.9858-0.6503c-0.59789-0.3032-1.1422-0.6988-1.6223-1.1797
c-0.4764-0.4756-0.8723-1.0207-1.1784-1.6232c-0.3064-0.6019-0.5114-1.2305-0.64619-1.9852
c-0.15581-0.8626-0.19861-1.7874-0.22-2.5777c-0.01221-0.4525-0.01731-0.9049-0.02021-1.3547l-0.0022-1.3279l0.0001-0.3506V14.8242
l-0.0001-0.3506l0.0021-1.3251c0.003-0.4525,0.0081-0.9049,0.02031-1.357c0.02139-0.7911,0.06419-1.7163,0.22129-2.5861
c0.1336-0.7479,0.3385-1.3765,0.6465-1.9814c0.3037-0.5979,0.7003-1.1437,1.17921-1.6225
c0.477-0.4772,1.02309-0.8739,1.62479-1.1799c0.6011-0.3061,1.2308-0.5116,1.9805-0.6465c0.8638-0.1552,1.7909-0.198,2.5849-0.2195
c0.4526-0.0123,0.9052-0.0172,1.3544-0.0203l1.6771-0.0033H150.69807"/>
<g>
<g>
<path d="M45.1862,35.64053c1.41724-1.77266,2.37897-4.15282,2.12532-6.58506c-2.07464,0.10316-4.60634,1.36871-6.07207,3.14276
c-1.31607,1.5192-2.4809,3.99902-2.17723,6.3293C41.39111,38.72954,43.71785,37.36345,45.1862,35.64053"/>
<path d="M47.28506,38.98252c-3.38211-0.20146-6.25773,1.91951-7.87286,1.91951c-1.61602,0-4.08931-1.81799-6.76438-1.76899
c-3.48177,0.05114-6.71245,2.01976-8.4793,5.15079c-3.63411,6.2636-0.95904,15.55471,2.57494,20.65606
c1.71618,2.5238,3.78447,5.30269,6.50976,5.20287c2.57494-0.10104,3.58421-1.66732,6.71416-1.66732
c3.12765,0,4.03679,1.66732,6.76252,1.61681c2.82665-0.05054,4.59381-2.52506,6.30997-5.05132
c1.96878-2.877,2.77473-5.65498,2.82542-5.80748c-0.0507-0.05051-5.45058-2.12204-5.50065-8.33358
c-0.05098-5.20101,4.23951-7.6749,4.44144-7.82832C52.3832,39.4881,48.5975,39.08404,47.28506,38.98252"/>
</g>
<g>
<path d="M76.73385,31.94381c7.35096,0,12.4697,5.06708,12.4697,12.44437c0,7.40363-5.22407,12.49704-12.65403,12.49704h-8.13892
v12.94318h-5.88037v-37.8846H76.73385z M68.41059,51.9493h6.74732c5.11975,0,8.0336-2.75636,8.0336-7.53479
c0-4.77792-2.91385-7.50845-8.00727-7.50845h-6.77365V51.9493z"/>
<path d="M90.73997,61.97864c0-4.8311,3.70182-7.79761,10.26583-8.16526l7.56061-0.44614v-2.12639
c0-3.07185-2.07423-4.90959-5.53905-4.90959c-3.28251,0-5.33041,1.57492-5.82871,4.04313h-5.35574
c0.31499-4.98859,4.56777-8.66407,11.3941-8.66407c6.69466,0,10.97377,3.54432,10.97377,9.08388v19.03421h-5.43472v-4.54194
h-0.13065c-1.60125,3.07185-5.09341,5.01441-8.71623,5.01441C94.52078,70.30088,90.73997,66.94038,90.73997,61.97864z
M108.56641,59.4846v-2.17905l-6.8,0.41981c-3.38683,0.23649-5.30306,1.73291-5.30306,4.09579
c0,2.41504,1.99523,3.99046,5.04075,3.99046C105.46823,65.81161,108.56641,63.08108,108.56641,59.4846z"/>
<path d="M119.34167,79.9889v-4.5946c0.4193,0.10483,1.36425,0.10483,1.83723,0.10483c2.6252,0,4.04313-1.10245,4.90908-3.9378
c0-0.05267,0.49931-1.68025,0.49931-1.70658l-9.97616-27.64562h6.14268l6.98432,22.47371h0.10432l6.98433-22.47371h5.9857
l-10.34483,29.06304c-2.36186,6.69517-5.0924,8.84789-10.81577,8.84789C121.17891,80.12006,119.76098,80.06739,119.34167,79.9889
z"/>
</g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View file

@ -4,7 +4,7 @@
"description": "Applepay module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -18,7 +18,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
'wcgateway.settings.fields' => function ( array $fields, ContainerInterface $container ): array {
// Used in various places to mark fields for the preview button.
$apm_name = 'ApplePay';
@ -101,7 +101,7 @@ return array(
'applepay_button_enabled' => array(
'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
'<img src="%sassets/images/applepay.png" alt="%s" style="max-width: 150px; max-height: 45px;" />',
'<img src="%sassets/images/applepay.svg" alt="%s" style="max-width: 150px; max-height: 45px;" />',
$module_url,
__( 'Apple Pay', 'woocommerce-paypal-payments' )
),
@ -155,7 +155,7 @@ return array(
'applepay_button_enabled' => array(
'title' => __( 'Apple Pay Button', 'woocommerce-paypal-payments' ),
'title_html' => sprintf(
'<img src="%sassets/images/applepay.png" alt="%s" style="max-width: 150px; max-height: 45px;" />',
'<img src="%sassets/images/applepay.svg" alt="%s" style="max-width: 150px; max-height: 45px;" />',
$module_url,
__( 'Apple Pay', 'woocommerce-paypal-payments' )
),

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Applepay;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): ApplepayModule {
return new ApplepayModule();
};

View file

@ -23,6 +23,11 @@
&.ppcp-button-minicart {
--apple-pay-button-display: block;
}
&.ppcp-preview-button.ppcp-button-dummy {
/* URL must specify the correct module-folder! */
--apm-button-dummy-background: url(../../../ppcp-applepay/assets/images/applepay.png);
}
}
.wp-block-woocommerce-checkout, .wp-block-woocommerce-cart {

View file

@ -57,6 +57,8 @@ const CONTEXT = {
* On a single page, multiple Apple Pay buttons can be displayed, which also means multiple
* ApplePayButton instances exist. A typical case is on the product page, where one Apple Pay button
* is located inside the minicart-popup, and another pay-now button is in the product context.
*
* TODO - extend from PaymentButton (same as we do in GooglepayButton.js)
*/
class ApplePayButton {
/**
@ -339,6 +341,12 @@ class ApplePayButton {
this.#isInitialized = true;
this.applePayConfig = config;
if ( this.isSeparateGateway ) {
document
.querySelectorAll( '#ppc-button-applepay-container' )
.forEach( ( el ) => el.remove() );
}
if ( ! this.isEligible ) {
this.hide();
} else {
@ -484,6 +492,7 @@ class ApplePayButton {
const ppcpStyle = this.ppcpStyle;
wrapper.innerHTML = `<apple-pay-button id='${ id }' buttonstyle='${ style.color }' type='${ style.type }' locale='${ style.lang }' />`;
wrapper.classList.remove( 'ppcp-button-rect', 'ppcp-button-pill' );
wrapper.classList.add(
`ppcp-button-${ ppcpStyle.shape }`,
'ppcp-button-apm',

View file

@ -0,0 +1,54 @@
import ApplepayButton from '../ApplepayButton';
import PreviewButton from '../../../../ppcp-button/resources/js/modules/Preview/PreviewButton';
/**
* A single Apple Pay preview button instance.
*/
export default class ApplePayPreviewButton extends PreviewButton {
constructor( args ) {
super( args );
this.selector = `${ args.selector }ApplePay`;
this.defaultAttributes = {
button: {
type: 'pay',
color: 'black',
lang: 'en',
},
};
}
createButton( buttonConfig ) {
const button = new ApplepayButton(
'preview',
null,
buttonConfig,
this.ppcpConfig
);
button.init( this.apiConfig );
}
/**
* Merge form details into the config object for preview.
* Mutates the previewConfig object; no return value.
* @param buttonConfig
* @param ppcpConfig
*/
dynamicPreviewConfig( buttonConfig, ppcpConfig ) {
// The Apple Pay button expects the "wrapper" to be an ID without `#` prefix!
buttonConfig.button.wrapper = buttonConfig.button.wrapper.replace(
/^#/,
''
);
// Merge the current form-values into the preview-button configuration.
if ( ppcpConfig.button ) {
buttonConfig.button.type = ppcpConfig.button.style.type;
buttonConfig.button.color = ppcpConfig.button.style.color;
buttonConfig.button.lang =
ppcpConfig.button.style?.lang ||
ppcpConfig.button.style.language;
}
}
}

View file

@ -0,0 +1,50 @@
import PreviewButtonManager from '../../../../ppcp-button/resources/js/modules/Preview/PreviewButtonManager';
import ApplePayPreviewButton from './ApplePayPreviewButton';
/**
* Manages all Apple Pay preview buttons on this page.
*/
export default class ApplePayPreviewButtonManager extends PreviewButtonManager {
constructor() {
const args = {
methodName: 'ApplePay',
buttonConfig: window.wc_ppcp_applepay_admin,
};
super( args );
}
/**
* Responsible for fetching and returning the PayPal configuration object for this payment
* method.
*
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>}
*/
async fetchConfig( payPal ) {
const apiMethod = payPal?.Applepay()?.config;
if ( ! apiMethod ) {
this.error(
'configuration object cannot be retrieved from PayPal'
);
return {};
}
return await apiMethod();
}
/**
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {ApplePayPreviewButton}
*/
createButtonInstance( wrapperId ) {
return new ApplePayPreviewButton( {
selector: wrapperId,
apiConfig: this.apiConfig,
methodName: this.methodName,
} );
}
}

View file

@ -1,6 +1,4 @@
import ApplePayButton from './ApplepayButton';
import PreviewButton from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButton';
import PreviewButtonManager from '../../../ppcp-button/resources/js/modules/Renderer/PreviewButtonManager';
import ApplePayPreviewButtonManager from './Preview/ApplePayPreviewButtonManager';
/**
* Accessor that creates and returns a single PreviewButtonManager instance.
@ -14,111 +12,5 @@ const buttonManager = () => {
return ApplePayPreviewButtonManager.instance;
};
/**
* Manages all Apple Pay preview buttons on this page.
*/
class ApplePayPreviewButtonManager extends PreviewButtonManager {
constructor() {
const args = {
methodName: 'ApplePay',
buttonConfig: window.wc_ppcp_applepay_admin,
};
super( args );
}
/**
* Responsible for fetching and returning the PayPal configuration object for this payment
* method.
*
* @param {{}} payPal - The PayPal SDK object provided by WidgetBuilder.
* @return {Promise<{}>}
*/
async fetchConfig( payPal ) {
const apiMethod = payPal?.Applepay()?.config;
if ( ! apiMethod ) {
this.error(
'configuration object cannot be retrieved from PayPal'
);
return {};
}
return await apiMethod();
}
/**
* This method is responsible for creating a new PreviewButton instance and returning it.
*
* @param {string} wrapperId - CSS ID of the wrapper element.
* @return {ApplePayPreviewButton}
*/
createButtonInstance( wrapperId ) {
return new ApplePayPreviewButton( {
selector: wrapperId,
apiConfig: this.apiConfig,
} );
}
}
/**
* A single Apple Pay preview button instance.
*/
class ApplePayPreviewButton extends PreviewButton {
constructor( args ) {
super( args );
this.selector = `${ args.selector }ApplePay`;
this.defaultAttributes = {
button: {
type: 'pay',
color: 'black',
lang: 'en',
},
};
}
createNewWrapper() {
const element = super.createNewWrapper();
element.addClass( 'ppcp-button-applepay' );
return element;
}
createButton( buttonConfig ) {
const button = new ApplePayButton(
'preview',
null,
buttonConfig,
this.ppcpConfig
);
button.init( this.apiConfig );
}
/**
* Merge form details into the config object for preview.
* Mutates the previewConfig object; no return value.
* @param buttonConfig
* @param ppcpConfig
*/
dynamicPreviewConfig( buttonConfig, ppcpConfig ) {
// The Apple Pay button expects the "wrapper" to be an ID without `#` prefix!
buttonConfig.button.wrapper = buttonConfig.button.wrapper.replace(
/^#/,
''
);
// Merge the current form-values into the preview-button configuration.
if ( ppcpConfig.button ) {
buttonConfig.button.type = ppcpConfig.button.style.type;
buttonConfig.button.color = ppcpConfig.button.style.color;
buttonConfig.button.lang =
ppcpConfig.button.style?.lang ||
ppcpConfig.button.style.language;
}
}
}
// Initialize the preview button manager.
buttonManager();

View file

@ -299,14 +299,15 @@ return array(
esc_html( $button_text )
);
},
'applepay.wc-gateway' => static function ( ContainerInterface $container ): ApplePayGateway {
'applepay.wc-gateway' => static function ( ContainerInterface $container ): ApplePayGateway {
return new ApplePayGateway(
$container->get( 'wcgateway.order-processor' ),
$container->get( 'api.factory.paypal-checkout-url' ),
$container->get( 'wcgateway.processor.refunds' ),
$container->get( 'wcgateway.transaction-url-provider' ),
$container->get( 'session.handler' ),
$container->get( 'applepay.url' )
$container->get( 'applepay.url' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},

View file

@ -10,6 +10,7 @@ declare( strict_types = 1 );
namespace WooCommerce\PayPalCommerce\Applepay;
use Exception;
use Psr\Log\LoggerInterface;
use WC_Order;
use WC_Payment_Gateway;
use WooCommerce\PayPalCommerce\Session\SessionHandler;
@ -72,6 +73,13 @@ class ApplePayGateway extends WC_Payment_Gateway {
*/
private $module_url;
/**
* The logger.
*
* @var LoggerInterface
*/
private $logger;
/**
* ApplePayGateway constructor.
*
@ -84,6 +92,7 @@ class ApplePayGateway extends WC_Payment_Gateway {
* view URL based on order.
* @param SessionHandler $session_handler The Session Handler.
* @param string $module_url The URL to the module.
* @param LoggerInterface $logger The logger.
*/
public function __construct(
OrderProcessor $order_processor,
@ -91,7 +100,8 @@ class ApplePayGateway extends WC_Payment_Gateway {
RefundProcessor $refund_processor,
TransactionUrlProvider $transaction_url_provider,
SessionHandler $session_handler,
string $module_url
string $module_url,
LoggerInterface $logger
) {
$this->id = self::ID;
@ -102,7 +112,7 @@ class ApplePayGateway extends WC_Payment_Gateway {
$this->description = $this->get_option( 'description', '' );
$this->module_url = $module_url;
$this->icon = esc_url( $this->module_url ) . 'assets/images/applepay.png';
$this->icon = esc_url( $this->module_url ) . 'assets/images/applepay.svg';
$this->init_form_fields();
$this->init_settings();
@ -111,6 +121,7 @@ class ApplePayGateway extends WC_Payment_Gateway {
$this->refund_processor = $refund_processor;
$this->transaction_url_provider = $transaction_url_provider;
$this->session_handler = $session_handler;
$this->logger = $logger;
add_action(
'woocommerce_update_options_payment_gateways_' . $this->id,

View file

@ -18,30 +18,37 @@ use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice;
use WooCommerce\PayPalCommerce\Onboarding\Environment;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class ApplepayModule
*/
class ApplepayModule implements ModuleInterface {
class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
$module = $this;
// Clears product status when appropriate.
@ -160,14 +167,8 @@ class ApplepayModule implements ModuleInterface {
echo '<div id="ppc-button-' . esc_attr( ApplePayGateway::ID ) . '"></div>';
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
return true;
}
/**

View file

@ -4,7 +4,7 @@
"description": "Axo module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -17,7 +17,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\DisplayManager;
return array(
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
'wcgateway.settings.fields' => function ( array $fields, ContainerInterface $container ): array {
$insert_after = function( array $array, string $key, array $new ): array {
$keys = array_keys( $array );
@ -120,7 +120,6 @@ return array(
'',
array(
$container->get( 'axo.settings-conflict-notice' ),
$container->get( 'axo.shipping-config-notice' ),
$container->get( 'axo.checkout-config-notice' ),
$container->get( 'axo.incompatible-plugins-notice' ),
)

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Axo;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): AxoModule {
return new AxoModule();
};

View file

@ -11,17 +11,54 @@ import {
} from '../../../ppcp-button/resources/js/modules/Helper/ButtonDisabler';
import { getCurrentPaymentMethod } from '../../../ppcp-button/resources/js/modules/Helper/CheckoutMethodState';
/**
* Internal customer details.
*
* @typedef {Object} CustomerDetails
* @property {null|string} email - Customer email.
* @property {null|string} phone - Fastlane phone number.
* @property {null|Object} billing - Billing details object.
* @property {null|Object} shipping - Shipping details object.
* @property {null|Object} card - Payment details object.
*/
class AxoManager {
axoConfig = null;
ppcpConfig = null;
$ = null;
fastlane = null;
/**
* @type {FastlaneCardComponent}
*/
cardComponent = null;
initialized = false;
hideGatewaySelection = false;
phoneNumber = null;
/**
* @type {CustomerDetails}
*/
data = {};
status = {};
styles = {};
locale = 'en_us';
el = null;
emailInput = null;
phoneInput = null;
shippingView = null;
billingView = null;
cardView = null;
constructor( axoConfig, ppcpConfig ) {
this.axoConfig = axoConfig;
this.ppcpConfig = ppcpConfig;
this.initialized = false;
this.fastlane = new Fastlane();
this.$ = jQuery;
this.hideGatewaySelection = false;
this.status = {
active: false,
validEmail: false,
@ -30,13 +67,9 @@ class AxoManager {
hasCard: false,
};
this.data = {
email: null,
billing: null,
shipping: null,
card: null,
};
this.clearData();
// TODO - Do we need a public `states` property for this?
this.states = this.axoConfig.woocommerce.states;
this.el = new DomElementCollection();
@ -51,8 +84,6 @@ class AxoManager {
},
};
this.locale = 'en_us';
this.registerEventHandlers();
this.shippingView = new ShippingView(
@ -101,9 +132,31 @@ class AxoManager {
}
}
this.onChangePhone = this.onChangePhone.bind( this );
this.initPhoneSyncWooToFastlane();
this.triggerGatewayChange();
}
/**
* Checks if the current flow is the "Ryan flow": Ryan is a known customer who created a
* Fastlane profile before. Ryan can leverage all benefits of the accelerated 1-click checkout.
*
* @return {boolean} True means, Fastlane could link the customer's email to an existing account.
*/
get isRyanFlow() {
return !! this.data.card;
}
/**
* CSS selector to target the Fastlane Card Component wrapper.
*
* @return {string} CSS selector.
*/
get cardFormSelector() {
return this.el.paymentContainer.selector + '-form';
}
registerEventHandlers() {
this.$( document ).on(
'change',
@ -458,6 +511,7 @@ class AxoManager {
this.initPlacements();
this.initFastlane();
this.setStatus( 'active', true );
this.readPhoneFromWoo();
log( `Attempt on activation - emailInput: ${ this.emailInput.value }` );
log(
@ -468,6 +522,8 @@ class AxoManager {
this.lastEmailCheckedIdentity !== this.emailInput.value
) {
this.onChangeEmail();
} else {
this.refreshFastlanePrefills();
}
}
@ -690,6 +746,51 @@ class AxoManager {
}
}
/**
* Locates the WooCommerce checkout "billing phone" field and adds event listeners to it.
*/
initPhoneSyncWooToFastlane() {
this.phoneInput = document.querySelector( '#billing_phone' );
this.phoneInput?.addEventListener( 'change', this.onChangePhone );
}
/**
* Strips the country prefix and non-numeric characters from the phone number. If the remaining
* phone number is valid, it's returned. Otherwise, the function returns null.
*
* @param {string} number - Phone number to sanitize.
* @return {string|null} A valid US phone number, or null if the number is invalid.
*/
sanitizePhoneNumber( number ) {
const localNumber = number.replace( /^\+1/, '' );
const cleanNumber = localNumber.replace( /\D/g, '' );
// All valid US mobile numbers have exactly 10 digits.
return cleanNumber.length === 10 ? cleanNumber : null;
}
/**
* Reads the phone number from the WooCommerce checkout form, sanitizes it, and (if valid)
* stores it in the internal customer details object.
*
* @return {boolean} True, if the internal phone number was updated.
*/
readPhoneFromWoo() {
if ( ! this.phoneInput ) {
return false;
}
const phoneNumber = this.phoneInput.value;
const validPhoneNumber = this.sanitizePhoneNumber( phoneNumber );
if ( ! validPhoneNumber ) {
return false;
}
this.data.phone = validPhoneNumber;
return true;
}
async onChangeEmail() {
this.clearData();
@ -711,8 +812,8 @@ class AxoManager {
this.emailInput.value = this.stripSpaces( this.emailInput.value );
this.$( this.el.paymentContainer.selector + '-detail' ).html( '' );
this.$( this.el.paymentContainer.selector + '-form' ).html( '' );
this.$( this.el.paymentContainer.selector + '-details' ).html( '' );
this.removeFastlaneComponent();
this.setStatus( 'validEmail', false );
this.setStatus( 'hasProfile', false );
@ -733,6 +834,8 @@ class AxoManager {
this.data.email = this.emailInput.value;
this.billingView.setData( this.data );
this.readPhoneFromWoo();
if ( ! this.fastlane.identity ) {
log( 'Not initialized.' );
return;
@ -757,6 +860,22 @@ class AxoManager {
this.enableGatewaySelection();
}
/**
* Event handler that fires when the customer changed the phone number in the WooCommerce
* checkout form. If Fastlane is active, the component is refreshed.
*
* @return {Promise<void>}
*/
async onChangePhone() {
const hasChanged = this.readPhoneFromWoo();
if ( hasChanged && this.status.active ) {
await this.refreshFastlanePrefills();
}
return Promise.resolve();
}
async lookupCustomerByEmail() {
const lookupResponse =
await this.fastlane.identity.lookupCustomerByEmail(
@ -792,11 +911,7 @@ class AxoManager {
if ( authResponse.profileData.card ) {
this.setStatus( 'hasCard', true );
} else {
this.cardComponent = (
await this.fastlane.FastlaneCardComponent(
this.cardComponentData()
)
).render( this.el.paymentContainer.selector + '-form' );
await this.initializeFastlaneComponent();
}
const cardBillingAddress =
@ -837,12 +952,7 @@ class AxoManager {
this.setStatus( 'hasProfile', false );
await this.renderWatermark( true );
this.cardComponent = (
await this.fastlane.FastlaneCardComponent(
this.cardComponentData()
)
).render( this.el.paymentContainer.selector + '-form' );
await this.initializeFastlaneComponent();
}
} else {
// No profile found with this email address.
@ -853,12 +963,7 @@ class AxoManager {
this.setStatus( 'hasProfile', false );
await this.renderWatermark( true );
this.cardComponent = (
await this.fastlane.FastlaneCardComponent(
this.cardComponentData()
)
).render( this.el.paymentContainer.selector + '-form' );
await this.initializeFastlaneComponent();
}
}
@ -873,6 +978,7 @@ class AxoManager {
clearData() {
this.data = {
email: null,
phone: null,
billing: null,
shipping: null,
card: null,
@ -897,7 +1003,7 @@ class AxoManager {
onClickSubmitButton() {
// TODO: validate data.
if ( this.data.card ) {
if ( this.isRyanFlow ) {
// Ryan flow
log( 'Starting Ryan flow.' );
@ -935,7 +1041,7 @@ class AxoManager {
}
cardComponentData() {
return {
const config = {
fields: {
cardholderName: {
enabled: this.axoConfig.name_on_card === '1',
@ -945,6 +1051,68 @@ class AxoManager {
this.axoConfig.style_options
),
};
// Ryan is a known customer, we do not need his phone number.
if ( this.data.phone && ! this.isRyanFlow ) {
config.fields.phoneNumber = {
prefill: this.data.phone,
};
}
return config;
}
/**
* Initializes the Fastlane UI component, using configuration provided by the
* `cardComponentData()` method. If the UI component was already initialized, nothing happens.
*
* @return {Promise<*>} Resolves when the component was rendered.
*/
async initializeFastlaneComponent() {
if ( ! this.status.active || this.cardComponent ) {
return Promise.resolve();
}
const elem = this.cardFormSelector;
const config = this.cardComponentData();
this.cardComponent =
await this.fastlane.FastlaneCardComponent( config );
return this.cardComponent.render( elem );
}
/**
* Reverts the changes made by `initializeFastlaneComponent()`.
*
* Calling this method will lose any input that the user made inside the
* Fastlane Card Component.
*/
removeFastlaneComponent() {
document.querySelector( this.cardFormSelector ).innerHTML = '';
this.cardComponent = null;
}
/**
* Updates the prefill-values in the UI component. This method only updates empty fields.
*
* @return {Promise<*>} Resolves when the component was refreshed.
*/
async refreshFastlanePrefills() {
if ( ! this.cardComponent ) {
return Promise.resolve();
}
const { fields } = this.cardComponentData();
const prefills = Object.keys( fields ).reduce( ( result, key ) => {
if ( fields[ key ].hasOwnProperty( 'prefill' ) ) {
result[ key ] = fields[ key ].prefill;
}
return result;
}, {} );
return this.cardComponent.updatePrefills( prefills );
}
tokenizeData() {
@ -1081,8 +1249,8 @@ class AxoManager {
ensureBillingPhoneNumber( data ) {
if ( data.billing_phone === '' ) {
let phone = '';
const cc = this.data?.shipping?.phoneNumber?.countryCode;
const number = this.data?.shipping?.phoneNumber?.nationalNumber;
const cc = this.data.shipping?.phoneNumber?.countryCode;
const number = this.data.shipping?.phoneNumber?.nationalNumber;
if ( cc ) {
phone = `+${ cc } `;

View file

@ -21,14 +21,14 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
return array(
// If AXO can be configured.
'axo.eligible' => static function ( ContainerInterface $container ): bool {
'axo.eligible' => static function ( ContainerInterface $container ): bool {
$apm_applies = $container->get( 'axo.helpers.apm-applies' );
assert( $apm_applies instanceof ApmApplies );
return $apm_applies->for_country_currency();
},
'axo.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
'axo.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
return new ApmApplies(
$container->get( 'axo.supported-country-currency-matrix' ),
$container->get( 'api.shop.currency' ),
@ -36,16 +36,16 @@ return array(
);
},
'axo.helpers.settings-notice-generator' => static function ( ContainerInterface $container ) : SettingsNoticeGenerator {
return new SettingsNoticeGenerator();
'axo.helpers.settings-notice-generator' => static function ( ContainerInterface $container ) : SettingsNoticeGenerator {
return new SettingsNoticeGenerator( $container->get( 'axo.fastlane-incompatible-plugin-names' ) );
},
// If AXO is configured and onboarded.
'axo.available' => static function ( ContainerInterface $container ): bool {
'axo.available' => static function ( ContainerInterface $container ): bool {
return true;
},
'axo.url' => static function ( ContainerInterface $container ): string {
'axo.url' => static function ( ContainerInterface $container ): string {
$path = realpath( __FILE__ );
if ( false === $path ) {
return '';
@ -56,7 +56,7 @@ return array(
);
},
'axo.manager' => static function ( ContainerInterface $container ): AxoManager {
'axo.manager' => static function ( ContainerInterface $container ): AxoManager {
return new AxoManager(
$container->get( 'axo.url' ),
$container->get( 'ppcp.asset-version' ),
@ -70,7 +70,7 @@ return array(
);
},
'axo.gateway' => static function ( ContainerInterface $container ): AxoGateway {
'axo.gateway' => static function ( ContainerInterface $container ): AxoGateway {
return new AxoGateway(
$container->get( 'wcgateway.settings.render' ),
$container->get( 'wcgateway.settings' ),
@ -87,7 +87,7 @@ return array(
);
},
'axo.card_icons' => static function ( ContainerInterface $container ): array {
'axo.card_icons' => static function ( ContainerInterface $container ): array {
return array(
array(
'title' => 'Visa',
@ -108,7 +108,7 @@ return array(
);
},
'axo.card_icons.axo' => static function ( ContainerInterface $container ): array {
'axo.card_icons.axo' => static function ( ContainerInterface $container ): array {
return array(
array(
'title' => 'Visa',
@ -144,7 +144,7 @@ return array(
/**
* The matrix which countries and currency combinations can be used for AXO.
*/
'axo.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
'axo.supported-country-currency-matrix' => static function ( ContainerInterface $container ) : array {
/**
* Returns which countries and currency combinations can be used for AXO.
*/
@ -163,7 +163,7 @@ return array(
);
},
'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string {
'axo.settings-conflict-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
@ -173,28 +173,21 @@ return array(
return $settings_notice_generator->generate_settings_conflict_notice( $settings );
},
'axo.checkout-config-notice' => static function ( ContainerInterface $container ) : string {
'axo.checkout-config-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
return $settings_notice_generator->generate_checkout_notice();
},
'axo.shipping-config-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
return $settings_notice_generator->generate_shipping_notice();
},
'axo.incompatible-plugins-notice' => static function ( ContainerInterface $container ) : string {
'axo.incompatible-plugins-notice' => static function ( ContainerInterface $container ) : string {
$settings_notice_generator = $container->get( 'axo.helpers.settings-notice-generator' );
assert( $settings_notice_generator instanceof SettingsNoticeGenerator );
return $settings_notice_generator->generate_incompatible_plugins_notice();
},
'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string {
'axo.smart-button-location-notice' => static function ( ContainerInterface $container ) : string {
$settings = $container->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
@ -222,10 +215,92 @@ return array(
return '<div class="ppcp-notice ppcp-notice-warning"><p>' . $notice_content . '</p></div>';
},
'axo.endpoint.frontend-logger' => static function ( ContainerInterface $container ): FrontendLoggerEndpoint {
'axo.endpoint.frontend-logger' => static function ( ContainerInterface $container ): FrontendLoggerEndpoint {
return new FrontendLoggerEndpoint(
$container->get( 'button.request-data' ),
$container->get( 'woocommerce.logger.woocommerce' )
);
},
/**
* The list of Fastlane incompatible plugins.
*
* @returns array<array{name: string, is_active: bool}>
*/
'axo.fastlane-incompatible-plugins' => static function () : array {
/**
* Filters the list of Fastlane incompatible plugins.
*/
return apply_filters(
'woocommerce_paypal_payments_fastlane_incompatible_plugins',
array(
array(
'name' => 'Elementor',
'is_active' => did_action( 'elementor/loaded' ),
),
array(
'name' => 'CheckoutWC',
'is_active' => defined( 'CFW_NAME' ),
),
array(
'name' => 'Direct Checkout for WooCommerce',
'is_active' => defined( 'QLWCDC_PLUGIN_NAME' ),
),
array(
'name' => 'Multi-Step Checkout for WooCommerce',
'is_active' => class_exists( 'WPMultiStepCheckout' ),
),
array(
'name' => 'Fluid Checkout for WooCommerce',
'is_active' => class_exists( 'FluidCheckout' ),
),
array(
'name' => 'MultiStep Checkout for WooCommerce',
'is_active' => class_exists( 'THWMSCF_Multistep_Checkout' ),
),
array(
'name' => 'WooCommerce Subscriptions',
'is_active' => class_exists( 'WC_Subscriptions' ),
),
array(
'name' => 'CartFlows',
'is_active' => class_exists( 'Cartflows_Loader' ),
),
array(
'name' => 'FunnelKit Funnel Builder',
'is_active' => class_exists( 'WFFN_Core' ),
),
array(
'name' => 'WooCommerce One Page Checkout',
'is_active' => class_exists( 'PP_One_Page_Checkout' ),
),
array(
'name' => 'All Products for Woo Subscriptions',
'is_active' => class_exists( 'WCS_ATT' ),
),
)
);
},
'axo.fastlane-incompatible-plugin-names' => static function ( ContainerInterface $container ) : array {
$incompatible_plugins = $container->get( 'axo.fastlane-incompatible-plugins' );
$active_plugins_list = array_filter(
$incompatible_plugins,
function( array $plugin ): bool {
return (bool) $plugin['is_active'];
}
);
if ( empty( $active_plugins_list ) ) {
return array();
}
return array_map(
function ( array $plugin ): string {
return "<li>{$plugin['name']}</li>";
},
$active_plugins_list
);
},
);

View file

@ -16,10 +16,12 @@ use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager;
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -29,21 +31,28 @@ use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
/**
* Class AxoModule
*/
class AxoModule implements ModuleInterface {
class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
use ContextTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
add_filter(
'woocommerce_payment_gateways',
@ -66,7 +75,9 @@ class AxoModule implements ModuleInterface {
// Add the gateway in admin area.
if ( is_admin() ) {
// $methods[] = $gateway; - Temporarily remove Fastlane from the payment gateway list in admin area.
if ( ! $this->is_wc_settings_payments_tab() ) {
$methods[] = $gateway;
}
return $methods;
}
@ -78,7 +89,7 @@ class AxoModule implements ModuleInterface {
assert( $settings instanceof Settings );
$is_paypal_enabled = $settings->has( 'enabled' ) && $settings->get( 'enabled' ) ?? false;
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false;
$is_dcc_enabled = $settings->has( 'dcc_enabled' ) && $settings->get( 'dcc_enabled' ) ?? false;
if ( ! $is_paypal_enabled || ! $is_dcc_enabled ) {
return $methods;
@ -88,10 +99,6 @@ class AxoModule implements ModuleInterface {
return $methods;
}
if ( ! $this->is_compatible_shipping_config() ) {
return $methods;
}
$methods[] = $gateway;
return $methods;
},
@ -162,7 +169,7 @@ class AxoModule implements ModuleInterface {
|| ! $c->get( 'axo.eligible' )
|| 'continuation' === $c->get( 'button.context' )
|| $subscription_helper->cart_contains_subscription()
|| ! $this->is_compatible_shipping_config() ) {
) {
return;
}
@ -284,6 +291,7 @@ class AxoModule implements ModuleInterface {
$endpoint->handle_request();
}
);
return true;
}
/**
@ -317,14 +325,6 @@ class AxoModule implements ModuleInterface {
return $localized_script_data;
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
}
/**
* Condition to evaluate if Credit Card gateway should be hidden.
*
@ -353,7 +353,6 @@ class AxoModule implements ModuleInterface {
return ! is_user_logged_in()
&& CartCheckoutDetector::has_classic_checkout()
&& $this->is_compatible_shipping_config()
&& $is_axo_enabled
&& $is_dcc_enabled
&& ! $this->is_excluded_endpoint();
@ -410,15 +409,6 @@ class AxoModule implements ModuleInterface {
return is_wc_endpoint_url( 'order-pay' );
}
/**
* Condition to evaluate if the shipping configuration is compatible.
*
* @return bool
*/
private function is_compatible_shipping_config(): bool {
return ! wc_shipping_enabled() || ( wc_shipping_enabled() && ! wc_ship_to_billing_address_only() );
}
/**
* Outputs a meta tag to allow feature detection on certain pages.
*

View file

@ -18,6 +18,22 @@ use WooCommerce\PayPalCommerce\WcGateway\Exception\NotFoundException;
* Class SettingsNoticeGenerator
*/
class SettingsNoticeGenerator {
/**
* The list of Fastlane incompatible plugin names.
*
* @var string[]
*/
protected $incompatible_plugin_names;
/**
* SettingsNoticeGenerator constructor.
*
* @param string[] $incompatible_plugin_names The list of Fastlane incompatible plugin names.
*/
public function __construct( array $incompatible_plugin_names ) {
$this->incompatible_plugin_names = $incompatible_plugin_names;
}
/**
* Generates the full HTML of the notification.
*
@ -87,54 +103,16 @@ class SettingsNoticeGenerator {
return $notice_content ? '<div class="ppcp-notice ppcp-notice-error"><p>' . $notice_content . '</p></div>' : '';
}
/**
* Generates the shipping notice.
*
* @return string
*/
public function generate_shipping_notice(): string {
$shipping_settings_link = admin_url( 'admin.php?page=wc-settings&tab=shipping&section=options' );
$notice_content = '';
if ( wc_shipping_enabled() && wc_ship_to_billing_address_only() ) {
$notice_content = sprintf(
/* translators: %1$s: URL to the Shipping destination settings page. */
__(
'<span class="highlight">Warning:</span> The <a href="%1$s">Shipping destination</a> of your store is currently configured to <code>Force shipping to the customer billing address</code>. To enable Fastlane and accelerate payments, the shipping destination must be configured either to <code>Default to customer shipping address</code> or <code>Default to customer billing address</code> so buyers can set separate billing and shipping details.',
'woocommerce-paypal-payments'
),
esc_url( $shipping_settings_link )
);
}
return $notice_content ? '<div class="ppcp-notice ppcp-notice-error"><p>' . $notice_content . '</p></div>' : '';
}
/**
* Generates the incompatible plugins notice.
*
* @return string
*/
public function generate_incompatible_plugins_notice(): string {
$incompatible_plugins = array(
'Elementor' => did_action( 'elementor/loaded' ),
'CheckoutWC' => defined( 'CFW_NAME' ),
);
$active_plugins_list = array_filter( $incompatible_plugins );
if ( empty( $active_plugins_list ) ) {
if ( empty( $this->incompatible_plugin_names ) ) {
return '';
}
$incompatible_plugin_items = array_map(
function ( $plugin ) {
return "<li>{$plugin}</li>";
},
array_keys( $active_plugins_list )
);
$plugins_settings_link = esc_url( admin_url( 'plugins.php' ) );
$notice_content = sprintf(
/* translators: %1$s: URL to the plugins settings page. %2$s: List of incompatible plugins. */
@ -143,7 +121,7 @@ class SettingsNoticeGenerator {
'woocommerce-paypal-payments'
),
$plugins_settings_link,
implode( '', $incompatible_plugin_items )
implode( '', $this->incompatible_plugin_names )
);
return '<div class="ppcp-notice"><p>' . $notice_content . '</p></div>';

View file

@ -4,7 +4,7 @@
"description": "Blocks module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -13,7 +13,7 @@ use WooCommerce\PayPalCommerce\Onboarding\State;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
return array(
'wcgateway.button.locations' => function ( ContainerInterface $container, array $locations ): array {
'wcgateway.button.locations' => function ( array $locations, ContainerInterface $container ): array {
return array_merge(
$locations,
array(
@ -22,13 +22,13 @@ return array(
)
);
},
'wcgateway.settings.pay-later.messaging-locations' => function ( ContainerInterface $container, array $locations ): array {
'wcgateway.settings.pay-later.messaging-locations' => function ( array $locations, ContainerInterface $container ): array {
unset( $locations['checkout-block-express'] );
unset( $locations['cart-block'] );
return $locations;
},
'wcgateway.settings.fields' => function ( ContainerInterface $container, array $fields ): array {
'wcgateway.settings.fields' => function ( array $fields, ContainerInterface $container ): array {
$insert_after = function( array $array, string $key, array $new ): array {
$keys = array_keys( $array );
$index = array_search( $key, $keys, true );
@ -72,7 +72,7 @@ return array(
);
},
'button.pay-now-contexts' => function ( ContainerInterface $container, array $contexts ): array {
'button.pay-now-contexts' => function ( array $contexts, ContainerInterface $container ): array {
if ( ! $container->get( 'blocks.settings.final_review_enabled' ) ) {
$contexts[] = 'checkout-block';
$contexts[] = 'cart-block';
@ -81,7 +81,7 @@ return array(
return $contexts;
},
'button.handle-shipping-in-paypal' => function ( ContainerInterface $container ): bool {
'button.handle-shipping-in-paypal' => function ( bool $previous, ContainerInterface $container ): bool {
return ! $container->get( 'blocks.settings.final_review_enabled' );
},
);

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Blocks;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): BlocksModule {
return new BlocksModule();
};

View file

@ -19,6 +19,7 @@
"@babel/core": "^7.19",
"@babel/preset-env": "^7.19",
"@babel/preset-react": "^7.18.6",
"@wordpress/i18n": "^5.6.0",
"@woocommerce/dependency-extraction-webpack-plugin": "2.2.0",
"babel-loader": "^8.2",
"cross-env": "^7.0.3",

View file

@ -3,6 +3,7 @@ import {
registerExpressPaymentMethod,
registerPaymentMethod,
} from '@woocommerce/blocks-registry';
import { __ } from '@wordpress/i18n';
import {
mergeWcAddress,
paypalAddressToWc,
@ -774,6 +775,12 @@ if ( block_enabled && config.enabled ) {
] ) {
registerExpressPaymentMethod( {
name: `${ config.id }-${ fundingSource }`,
title: 'PayPal',
description: __(
'Eligible users will see the PayPal button.',
'woocommerce-paypal-payments'
),
gatewayId: 'ppcp-gateway',
paymentMethodId: config.id,
label: (
<div dangerouslySetInnerHTML={ { __html: config.title } } />

View file

@ -12,29 +12,36 @@ namespace WooCommerce\PayPalCommerce\Blocks;
use Automattic\WooCommerce\Blocks\Payments\PaymentMethodRegistry;
use WooCommerce\PayPalCommerce\Blocks\Endpoint\UpdateShippingEndpoint;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* Class BlocksModule
*/
class BlocksModule implements ModuleInterface {
class BlocksModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
if (
! class_exists( 'Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType' )
|| ! function_exists( 'woocommerce_store_api_register_payment_requirements' )
@ -54,7 +61,7 @@ class BlocksModule implements ModuleInterface {
}
);
return;
return true;
}
add_action(
@ -118,13 +125,6 @@ class BlocksModule implements ModuleInterface {
return $components;
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
return true;
}
}

View file

@ -4,7 +4,7 @@
"description": "Button module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Button;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): ButtonModule {
return new ButtonModule();
};

View file

@ -1,11 +1,10 @@
#payment ul.payment_methods li img.ppcp-card-icon {
padding: 0 0 3px 3px;
max-height: 25px;
display: inline-block;
#payment ul.payment_methods [class*="payment_method_ppcp-"] label img {
max-height: 25px;
display: inline-block;
}
.payments-sdk-contingency-handler {
z-index: 1000 !important;
z-index: 1000 !important;
}
.ppcp-credit-card-gateway-form-field-disabled {

View file

@ -0,0 +1,52 @@
import PreviewButton from './PreviewButton';
/**
* Dummy preview button, to use in case an APM button cannot be rendered
*/
export default class DummyPreviewButton extends PreviewButton {
#innerEl;
constructor( args ) {
super( args );
this.selector = `${ args.selector }Dummy`;
this.label = args.label || 'Not Available';
}
createNewWrapper() {
const wrapper = super.createNewWrapper();
wrapper.classList.add( 'ppcp-button-apm', 'ppcp-button-dummy' );
return wrapper;
}
createButton( buttonConfig ) {
this.#innerEl?.remove();
this.#innerEl = document.createElement( 'div' );
this.#innerEl.innerHTML = `<div class="reason">${ this.label }</div>`;
this._applyStyles( this.ppcpConfig?.button?.style );
this.domWrapper.appendChild( this.#innerEl );
}
/**
* Applies the button shape (rect/pill) to the dummy button
*
* @param {{shape: string, height: number|null}} style
* @private
*/
_applyStyles( style ) {
this.domWrapper.classList.remove(
'ppcp-button-pill',
'ppcp-button-rect'
);
this.domWrapper.classList.add( `ppcp-button-${ style.shape }` );
if ( style.height ) {
this.domWrapper.style.height = `${ style.height }px`;
}
}
}

View file

@ -5,16 +5,21 @@ import merge from 'deepmerge';
*/
class PreviewButton {
/**
* @param {string} selector - CSS ID of the wrapper, including the `#`
* @param {Object} apiConfig - PayPal configuration object; retrieved via a
* widgetBuilder API method
* @param {string} selector - CSS ID of the wrapper, including the `#`
* @param {Object} apiConfig - PayPal configuration object; retrieved via a
* widgetBuilder API method
* @param {string} methodName - Name of the payment method, e.g. "Google Pay"
*/
constructor( { selector, apiConfig } ) {
constructor( { selector, apiConfig, methodName = '' } ) {
this.apiConfig = apiConfig;
this.defaultAttributes = {};
this.buttonConfig = {};
this.ppcpConfig = {};
this.isDynamic = true;
this.methodName = methodName;
this.methodSlug = this.methodName
.toLowerCase()
.replace( /[^a-z]+/g, '' );
// The selector is usually overwritten in constructor of derived class.
this.selector = selector;
@ -26,13 +31,16 @@ class PreviewButton {
/**
* Creates a new DOM node to contain the preview button.
*
* @return {jQuery} Always a single jQuery element with the new DOM node.
* @return {HTMLElement} Always a single jQuery element with the new DOM node.
*/
createNewWrapper() {
const wrapper = document.createElement( 'div' );
const previewId = this.selector.replace( '#', '' );
const previewClass = 'ppcp-button-apm';
const previewClass = `ppcp-preview-button ppcp-button-apm ppcp-button-${ this.methodSlug }`;
return jQuery( `<div id='${ previewId }' class='${ previewClass }'>` );
wrapper.setAttribute( 'id', previewId );
wrapper.setAttribute( 'class', previewClass );
return wrapper;
}
/**
@ -109,10 +117,12 @@ class PreviewButton {
console.error( 'Skip render, button is not configured yet' );
return;
}
this.domWrapper = this.createNewWrapper();
this.domWrapper.insertAfter( this.wrapper );
this._insertWrapper();
} else {
this.domWrapper.empty().show();
this._emptyWrapper();
this._showWrapper();
}
this.isVisible = true;
@ -151,16 +161,38 @@ class PreviewButton {
* Using a timeout here will make the button visible again at the end of the current
* event queue.
*/
setTimeout( () => this.domWrapper.show() );
setTimeout( () => this._showWrapper() );
}
remove() {
this.isVisible = false;
if ( this.domWrapper ) {
this.domWrapper.hide().empty();
this._hideWrapper();
this._emptyWrapper();
}
}
_showWrapper() {
this.domWrapper.style.display = '';
}
_hideWrapper() {
this.domWrapper.style.display = 'none';
}
_emptyWrapper() {
this.domWrapper.innerHTML = '';
}
_insertWrapper() {
const wrapperElement = document.querySelector( this.wrapper );
wrapperElement.parentNode.insertBefore(
this.domWrapper,
wrapperElement.nextSibling
);
}
}
export default PreviewButton;

View file

@ -1,7 +1,8 @@
import { loadCustomScript } from '@paypal/paypal-js';
import widgetBuilder from './WidgetBuilder';
import widgetBuilder from '../Renderer/WidgetBuilder';
import { debounce } from '../../../../../ppcp-blocks/resources/js/Helper/debounce';
import ConsoleLogger from '../../../../../ppcp-wc-gateway/resources/js/helper/ConsoleLogger';
import DummyPreviewButton from './DummyPreviewButton';
/**
* Manages all PreviewButton instances of a certain payment method on the page.
@ -26,6 +27,13 @@ class PreviewButtonManager {
*/
#onInit;
/**
* Initialize the new PreviewButtonManager.
*
* @param {string} methodName - Name of the payment method, e.g. "Google Pay"
* @param {Object} buttonConfig
* @param {Object} defaultAttributes
*/
constructor( { methodName, buttonConfig, defaultAttributes } ) {
// Define the payment method name in the derived class.
this.methodName = methodName;
@ -102,27 +110,15 @@ class PreviewButtonManager {
*
* This dummy is only visible on the admin side, and not rendered on the front-end.
*
* @todo Consider refactoring this into a new class that extends the PreviewButton class.
* @param {string} wrapperId
* @return {any}
*/
createDummy( wrapperId ) {
const elButton = document.createElement( 'div' );
elButton.classList.add( 'ppcp-button-apm', 'ppcp-button-dummy' );
elButton.innerHTML = `<span>${
this.apiError ?? 'Not Available'
}</span>`;
document.querySelector( wrapperId ).appendChild( elButton );
const instDummy = {
setDynamic: () => instDummy,
setPpcpConfig: () => instDummy,
render: () => {},
remove: () => {},
};
return instDummy;
createDummyButtonInstance( wrapperId ) {
return new DummyPreviewButton( {
selector: wrapperId,
label: this.apiError,
methodName: this.methodName,
} );
}
registerEventListeners() {
@ -319,6 +315,17 @@ class PreviewButtonManager {
this.log( 'configureAllButtons', ppcpConfig );
Object.entries( this.buttons ).forEach( ( [ id, button ] ) => {
const limitWrapper = ppcpConfig.button?.wrapper;
/**
* When the ppcpConfig object specifies a button wrapper, then ensure to limit preview
* changes to this individual wrapper. If no button wrapper is defined, the
* configuration is relevant for all buttons on the page.
*/
if ( limitWrapper && button.wrapper !== limitWrapper ) {
return;
}
this._configureButton( id, {
...ppcpConfig,
button: {
@ -349,12 +356,11 @@ class PreviewButtonManager {
let newInst;
if ( this.apiConfig && 'object' === typeof this.apiConfig ) {
newInst = this.createButtonInstance( id ).setButtonConfig(
this.buttonConfig
);
newInst = this.createButtonInstance( id );
} else {
newInst = this.createDummy( id );
newInst = this.createDummyButtonInstance( id );
}
newInst.setButtonConfig( this.buttonConfig );
this.buttons[ id ] = newInst;
}

View file

@ -113,6 +113,13 @@ export default class PaymentButton {
*/
#isInitialized = false;
/**
* Whether the one-time initialization of the payment gateway is complete.
*
* @type {boolean}
*/
#gatewayInitialized = false;
/**
* The button's context.
*
@ -437,6 +444,17 @@ export default class PaymentButton {
return this.wrappers.Default;
}
/**
* Whether the button is placed inside a classic gateway context.
*
* Classic gateway contexts are: Classic checkout, Pay for Order page.
*
* @return {boolean} True indicates, the button is located inside a classic gateway.
*/
get isInsideClassicGateway() {
return PaymentContext.Gateways.includes( this.context );
}
/**
* Determines if the current payment button should be rendered as a stand-alone gateway.
* The return value `false` usually means, that the payment button is bundled with all available
@ -449,20 +467,21 @@ export default class PaymentButton {
get isSeparateGateway() {
return (
this.#buttonConfig.is_wc_gateway_enabled &&
PaymentContext.Gateways.includes( this.context )
this.isInsideClassicGateway
);
}
/**
* Whether the currently selected payment gateway is set to the payment method.
*
* Only relevant on checkout pages, when `this.isSeparateGateway` is true.
* Only relevant on checkout pages where "classic" payment gateways are rendered.
*
* @return {boolean} True means that this payment method is selected as current gateway.
*/
get isCurrentGateway() {
if ( ! this.isSeparateGateway ) {
return false;
if ( ! this.isInsideClassicGateway ) {
// This means, the button's visibility is managed by another script.
return true;
}
/*
@ -470,7 +489,14 @@ export default class PaymentButton {
* module fires the "ButtonEvents.RENDER" event before any PaymentButton instances are
* created. I.e. we cannot observe the initial gateway selection event.
*/
return this.methodId === getCurrentPaymentMethod();
const currentMethod = getCurrentPaymentMethod();
if ( this.isSeparateGateway ) {
return this.methodId === currentMethod;
}
// Button is rendered inside the Smart Buttons block.
return PaymentMethods.PAYPAL === currentMethod;
}
/**
@ -682,7 +708,7 @@ export default class PaymentButton {
} );
// Events relevant for buttons inside a payment gateway.
if ( PaymentContext.Gateways.includes( this.context ) ) {
if ( this.isInsideClassicGateway ) {
const parentMethod = this.isSeparateGateway
? this.methodId
: PaymentMethods.PAYPAL;
@ -712,7 +738,7 @@ export default class PaymentButton {
this.applyWrapperStyles();
if ( this.isEligible && this.isPresent && this.isVisible ) {
if ( this.isEligible && this.isCurrentGateway && this.isVisible ) {
if ( ! this.isButtonAttached ) {
this.log( 'refresh.addButton' );
this.addButton();
@ -721,25 +747,33 @@ export default class PaymentButton {
}
/**
* Makes the custom payment gateway visible by removing initial inline styles from the DOM.
* Makes the payment gateway visible by removing initial inline styles from the DOM.
* Also, removes the button-placeholder container from the smart button block.
*
* Only relevant on the checkout page, i.e., when `this.isSeparateGateway` is `true`
*/
showPaymentGateway() {
if ( ! this.isSeparateGateway || ! this.isEligible ) {
if (
this.#gatewayInitialized ||
! this.isSeparateGateway ||
! this.isEligible
) {
return;
}
const styleSelectors = `style[data-hide-gateway="${ this.methodId }"]`;
const styleSelector = `style[data-hide-gateway="${ this.methodId }"]`;
const wrapperSelector = `#${ this.wrappers.Default }`;
const styles = document.querySelectorAll( styleSelectors );
document
.querySelectorAll( styleSelector )
.forEach( ( el ) => el.remove() );
document
.querySelectorAll( wrapperSelector )
.forEach( ( el ) => el.remove() );
if ( ! styles.length ) {
return;
}
this.log( 'Show gateway' );
styles.forEach( ( el ) => el.remove() );
this.#gatewayInitialized = true;
// This code runs only once, during button initialization, and fixes the initial visibility.
this.isVisible = this.isCurrentGateway;

View file

@ -228,7 +228,6 @@ class Renderer {
};
shouldEnableShippingCallback = () => {
console.log(this.defaultSettings.context, this.defaultSettings)
let needShipping = this.defaultSettings.needShipping || this.defaultSettings.context === 'product'
return this.defaultSettings.should_handle_shipping_in_paypal && needShipping
};

View file

@ -320,6 +320,7 @@ return array(
},
'button.helper.messages-apply' => static function ( ContainerInterface $container ): MessagesApply {
return new MessagesApply(
$container->get( 'api.paylater-countries' ),
$container->get( 'api.shop.country' )
);
},

View file

@ -14,8 +14,6 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\CartScriptParamsEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SaveCheckoutFormEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\SimulateCartEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ValidateCheckoutEndpoint;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
use WooCommerce\PayPalCommerce\Button\Endpoint\ApproveOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\ChangeCartEndpoint;
@ -23,29 +21,36 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\CreateOrderEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\DataClientIdEndpoint;
use WooCommerce\PayPalCommerce\Button\Endpoint\StartPayPalVaultingEndpoint;
use WooCommerce\PayPalCommerce\Button\Helper\EarlyOrderHandler;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
/**
* Class ButtonModule
*/
class ButtonModule implements ModuleInterface {
class ButtonModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
add_action(
'wp',
@ -91,6 +96,8 @@ class ButtonModule implements ModuleInterface {
);
$this->register_ajax_endpoints( $c );
return true;
}
/**
@ -211,12 +218,4 @@ class ButtonModule implements ModuleInterface {
}
);
}
/**
* Returns the key for the module.
*
* @return string|void
*/
public function getKey() {
}
}

View file

@ -85,7 +85,8 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
$this->add_products( $products );
$this->cart->calculate_totals();
$total = (float) $this->cart->get_total( 'numeric' );
$total = (float) $this->cart->get_total( 'numeric' );
$shipping_fee = (float) $this->cart->get_shipping_total();
$this->restore_real_cart();
@ -113,7 +114,7 @@ class SimulateCartEndpoint extends AbstractCartEndpoint {
wp_send_json_success(
array(
'total' => $total,
'total_str' => ( new Money( $total, $currency_code ) )->value_str(),
'shipping_fee' => $shipping_fee,
'currency_code' => $currency_code,
'country_code' => $shop_country_code,
'funding' => array(

View file

@ -243,4 +243,20 @@ trait ContextTrait {
$screen = get_current_screen();
return $screen && $screen->is_block_editor();
}
/**
* Checks if is WooCommerce Settings Payments tab screen (/wp-admin/admin.php?page=wc-settings&tab=checkout).
*
* @return bool
*/
protected function is_wc_settings_payments_tab(): bool {
if ( ! is_admin() || isset( $_GET['section'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification
return false;
}
$page = wc_clean( wp_unslash( $_GET['page'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification
$tab = wc_clean( wp_unslash( $_GET['tab'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification
return $page === 'wc-settings' && $tab === 'checkout';
}
}

View file

@ -18,17 +18,9 @@ class MessagesApply {
/**
* In which countries credit messaging is available.
*
* @var array
* @var string[]
*/
private $countries = array(
'US',
'DE',
'GB',
'FR',
'AU',
'IT',
'ES',
);
private $allowed_countries;
/**
* 2-letter country code of the shop.
@ -40,10 +32,12 @@ class MessagesApply {
/**
* MessagesApply constructor.
*
* @param string $country 2-letter country code of the shop.
* @param string[] $allowed_countries In which countries credit messaging is available.
* @param string $country 2-letter country code of the shop.
*/
public function __construct( string $country ) {
$this->country = $country;
public function __construct( array $allowed_countries, string $country ) {
$this->allowed_countries = $allowed_countries;
$this->country = $country;
}
/**
@ -52,6 +46,6 @@ class MessagesApply {
* @return bool
*/
public function for_country(): bool {
return in_array( $this->country, $this->countries, true );
return in_array( $this->country, $this->allowed_countries, true );
}
}

View file

@ -4,7 +4,7 @@
"description": "Advanced Checkout Card Fields module",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\CardFields;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): CardFieldsModule {
return new CardFieldsModule();
};

View file

@ -1,47 +1,35 @@
import { cardFieldStyles } from './CardFieldsHelper';
import { hide } from '../../../ppcp-button/resources/js/modules/Helper/Hiding';
function renderField( cardField, inputField ) {
if ( ! inputField || inputField.hidden || ! cardField ) {
return;
}
// Insert the PayPal card field after the original input field.
const styles = cardFieldStyles( inputField );
cardField( { style: { input: styles } } ).render( inputField.parentNode );
// Hide the original input field.
hide( inputField, true );
inputField.hidden = true;
}
export function renderFields( cardFields ) {
const nameField = document.getElementById(
'ppcp-credit-card-gateway-card-name'
renderField(
cardFields.NameField,
document.getElementById( 'ppcp-credit-card-gateway-card-name' )
);
if ( nameField && nameField.hidden !== true ) {
const styles = cardFieldStyles( nameField );
cardFields
.NameField( { style: { input: styles } } )
.render( nameField.parentNode );
nameField.hidden = true;
}
const numberField = document.getElementById(
'ppcp-credit-card-gateway-card-number'
renderField(
cardFields.NumberField,
document.getElementById( 'ppcp-credit-card-gateway-card-number' )
);
if ( numberField && numberField.hidden !== true ) {
const styles = cardFieldStyles( numberField );
cardFields
.NumberField( { style: { input: styles } } )
.render( numberField.parentNode );
numberField.hidden = true;
}
const expiryField = document.getElementById(
'ppcp-credit-card-gateway-card-expiry'
renderField(
cardFields.ExpiryField,
document.getElementById( 'ppcp-credit-card-gateway-card-expiry' )
);
if ( expiryField && expiryField.hidden !== true ) {
const styles = cardFieldStyles( expiryField );
cardFields
.ExpiryField( { style: { input: styles } } )
.render( expiryField.parentNode );
expiryField.hidden = true;
}
const cvvField = document.getElementById(
'ppcp-credit-card-gateway-card-cvc'
renderField(
cardFields.CVVField,
document.getElementById( 'ppcp-credit-card-gateway-card-cvc' )
);
if ( cvvField && cvvField.hidden !== true ) {
const styles = cardFieldStyles( cvvField );
cardFields
.CVVField( { style: { input: styles } } )
.render( cvvField.parentNode );
cvvField.hidden = true;
}
}

View file

@ -9,9 +9,10 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\CardFields;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\ServiceProvider;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule;
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
@ -19,30 +20,35 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
/**
* Class CardFieldsModule
*/
class CardFieldsModule implements ModuleInterface {
class CardFieldsModule implements ServiceModule, ExtendingModule, ExecutableModule {
use ModuleClassNameIdTrait;
/**
* {@inheritDoc}
*/
public function setup(): ServiceProviderInterface {
return new ServiceProvider(
require __DIR__ . '/../services.php',
require __DIR__ . '/../extensions.php'
);
public function services(): array {
return require __DIR__ . '/../services.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): void {
public function extensions(): array {
return require __DIR__ . '/../extensions.php';
}
/**
* {@inheritDoc}
*/
public function run( ContainerInterface $c ): bool {
if ( ! $c->get( 'card-fields.eligible' ) ) {
return;
return true;
}
$settings = $c->get( 'wcgateway.settings' );
assert( $settings instanceof Settings );
if ( ! $settings->has( 'dcc_enabled' ) || ! $settings->get( 'dcc_enabled' ) ) {
return;
return true;
}
/**
@ -137,5 +143,7 @@ class CardFieldsModule implements ModuleInterface {
10,
2
);
return true;
}
}

View file

@ -4,7 +4,7 @@
"description": "Compatibility module for PPCP",
"license": "GPL-2.0",
"require": {
"php": "^7.2 | ^8.0",
"php": "^7.4 | ^8.0",
"dhii/module-interface": "^0.3.0-alpha1"
},
"autoload": {

View file

@ -9,8 +9,6 @@ declare(strict_types=1);
namespace WooCommerce\PayPalCommerce\Compat;
use WooCommerce\PayPalCommerce\Vendor\Dhii\Modular\Module\ModuleInterface;
return static function (): ModuleInterface {
return static function (): CompatModule {
return new CompatModule();
};

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