mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-08-30 05:00:51 +08:00
Merge branch 'trunk' into PCP-4974-contact-module-virtual-product-from-block-pages-and-classic-checkout-always-return-default-contact-information-from-pay-pal-window
This commit is contained in:
commit
38f7355db1
111 changed files with 3354 additions and 1170 deletions
|
@ -32,7 +32,7 @@ return function (
|
|||
$modules = apply_filters( 'woocommerce_paypal_payments_modules', $modules );
|
||||
|
||||
// Initialize plugin.
|
||||
$properties = PluginProperties::new( __FILE__ );
|
||||
$properties = PluginProperties::new( "$root_dir/woocommerce-paypal-payments.php" );
|
||||
$bootstrap = Package::new( $properties );
|
||||
|
||||
foreach ( $modules as $module ) {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
*** Changelog ***
|
||||
|
||||
= 3.0.8 - XXXX-XX-XX =
|
||||
= 3.0.9 - 2025-07-31 =
|
||||
* Fix - Payment via "Proceed to PayPal" may result in a redirect loop #3570
|
||||
|
||||
= 3.0.8 - 2025-07-28 =
|
||||
* Enhancement - Migration from Legacy Settings to New Settings as opt-in via banner & button #3491
|
||||
* Enhancement - Replace call to `billing-agreements/agreement-tokens` with checking the capabilities for Reference Transactions #3495
|
||||
* Enhancement - Add Fastlane 3D Secure support #3493
|
||||
|
@ -16,7 +19,7 @@
|
|||
|
||||
= 3.0.7 - 2025-07-01 =
|
||||
* Enhancement - Remove `application_context` in favor of `experience_context` object #3431
|
||||
**NOTE**: If you were modifying the `application_context` object programmatically, you may need to update your code to utilize `experience_context` for your customizations.
|
||||
**NOTE**: If you were modifying the `application_context` object programmatically, you may need to update your code to utilize `experience_context` for your customizations.
|
||||
* Enhancement - Add Contact Module feature
|
||||
* Enhancement - Add WooCommerce Tracks integration
|
||||
* Enhancement - Onboarding notification for Firefox browser #3433
|
||||
|
|
|
@ -26,7 +26,8 @@
|
|||
"php-stubs/wordpress-stubs": "^5.0@stable",
|
||||
"php-stubs/woocommerce-stubs": "^8.0@stable",
|
||||
"vimeo/psalm": "^4.0",
|
||||
"vlucas/phpdotenv": "^5"
|
||||
"vlucas/phpdotenv": "^5",
|
||||
"coenjacobs/mozart": "^0.7.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
|
285
composer.lock
generated
285
composer.lock
generated
|
@ -4,7 +4,7 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "2fa610ed883c0868838d3008b7127cbf",
|
||||
"content-hash": "6ca9c2c7864d2649617db6d3850382c5",
|
||||
"packages": [
|
||||
{
|
||||
"name": "container-interop/service-provider",
|
||||
|
@ -904,6 +904,64 @@
|
|||
},
|
||||
"time": "2024-08-29T20:15:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "coenjacobs/mozart",
|
||||
"version": "0.7.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/coenjacobs/mozart.git",
|
||||
"reference": "dbcdeb992d20d9c8914eef090f9a0d684bb1102c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/coenjacobs/mozart/zipball/dbcdeb992d20d9c8914eef090f9a0d684bb1102c",
|
||||
"reference": "dbcdeb992d20d9c8914eef090f9a0d684bb1102c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"league/flysystem": "^1.0",
|
||||
"php": "^7.3|^8.0",
|
||||
"symfony/console": "^4|^5",
|
||||
"symfony/finder": "^4|^5"
|
||||
},
|
||||
"require-dev": {
|
||||
"mheap/phpunit-github-actions-printer": "^1.4",
|
||||
"phpunit/phpunit": "^8.5",
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"vimeo/psalm": "^4.4"
|
||||
},
|
||||
"bin": [
|
||||
"bin/mozart"
|
||||
],
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"CoenJacobs\\Mozart\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Coen Jacobs",
|
||||
"email": "coenjacobs@gmail.com"
|
||||
}
|
||||
],
|
||||
"description": "Composes all dependencies as a package inside a WordPress plugin",
|
||||
"support": {
|
||||
"issues": "https://github.com/coenjacobs/mozart/issues",
|
||||
"source": "https://github.com/coenjacobs/mozart/tree/0.7.1"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/coenjacobs",
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2021-02-02T21:37:03+00:00"
|
||||
},
|
||||
{
|
||||
"name": "composer/package-versions-deprecated",
|
||||
"version": "1.11.99.5",
|
||||
|
@ -1888,6 +1946,156 @@
|
|||
},
|
||||
"time": "2025-05-09T12:13:17+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/flysystem",
|
||||
"version": "1.1.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/flysystem.git",
|
||||
"reference": "3239285c825c152bcc315fe0e87d6b55f5972ed1"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/3239285c825c152bcc315fe0e87d6b55f5972ed1",
|
||||
"reference": "3239285c825c152bcc315fe0e87d6b55f5972ed1",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-fileinfo": "*",
|
||||
"league/mime-type-detection": "^1.3",
|
||||
"php": "^7.2.5 || ^8.0"
|
||||
},
|
||||
"conflict": {
|
||||
"league/flysystem-sftp": "<1.0.6"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpspec/prophecy": "^1.11.1",
|
||||
"phpunit/phpunit": "^8.5.8"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-ftp": "Allows you to use FTP server storage",
|
||||
"ext-openssl": "Allows you to use FTPS server storage",
|
||||
"league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2",
|
||||
"league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3",
|
||||
"league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
|
||||
"league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
|
||||
"league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem",
|
||||
"league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files",
|
||||
"league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
|
||||
"league/flysystem-webdav": "Allows you to use WebDAV storage",
|
||||
"league/flysystem-ziparchive": "Allows you to use ZipArchive adapter",
|
||||
"spatie/flysystem-dropbox": "Allows you to use Dropbox storage",
|
||||
"srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Flysystem\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank de Jonge",
|
||||
"email": "info@frenky.net"
|
||||
}
|
||||
],
|
||||
"description": "Filesystem abstraction: Many filesystems, one API.",
|
||||
"keywords": [
|
||||
"Cloud Files",
|
||||
"WebDAV",
|
||||
"abstraction",
|
||||
"aws",
|
||||
"cloud",
|
||||
"copy.com",
|
||||
"dropbox",
|
||||
"file systems",
|
||||
"files",
|
||||
"filesystem",
|
||||
"filesystems",
|
||||
"ftp",
|
||||
"rackspace",
|
||||
"remote",
|
||||
"s3",
|
||||
"sftp",
|
||||
"storage"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/flysystem/issues",
|
||||
"source": "https://github.com/thephpleague/flysystem/tree/1.1.10"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://offset.earth/frankdejonge",
|
||||
"type": "other"
|
||||
}
|
||||
],
|
||||
"time": "2022-10-04T09:16:37+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/mime-type-detection",
|
||||
"version": "1.16.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/mime-type-detection.git",
|
||||
"reference": "2d6702ff215bf922936ccc1ad31007edc76451b9"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9",
|
||||
"reference": "2d6702ff215bf922936ccc1ad31007edc76451b9",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-fileinfo": "*",
|
||||
"php": "^7.4 || ^8.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^3.2",
|
||||
"phpstan/phpstan": "^0.12.68",
|
||||
"phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\MimeTypeDetection\\": "src"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Frank de Jonge",
|
||||
"email": "info@frankdejonge.nl"
|
||||
}
|
||||
],
|
||||
"description": "Mime-type detection for Flysystem",
|
||||
"support": {
|
||||
"issues": "https://github.com/thephpleague/mime-type-detection/issues",
|
||||
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://github.com/frankdejonge",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/league/flysystem",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-21T08:32:55+00:00"
|
||||
},
|
||||
{
|
||||
"name": "mockery/mockery",
|
||||
"version": "1.6.12",
|
||||
|
@ -1973,16 +2181,16 @@
|
|||
},
|
||||
{
|
||||
"name": "myclabs/deep-copy",
|
||||
"version": "1.13.1",
|
||||
"version": "1.13.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c"
|
||||
"reference": "faed855a7b5f4d4637717c2b3863e277116beb36"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c",
|
||||
"reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c",
|
||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/faed855a7b5f4d4637717c2b3863e277116beb36",
|
||||
"reference": "faed855a7b5f4d4637717c2b3863e277116beb36",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
|
@ -2021,7 +2229,7 @@
|
|||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/myclabs/DeepCopy/issues",
|
||||
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.1"
|
||||
"source": "https://github.com/myclabs/DeepCopy/tree/1.13.3"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
|
@ -2029,7 +2237,7 @@
|
|||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2025-04-29T12:36:36+00:00"
|
||||
"time": "2025-07-05T12:25:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "netresearch/jsonmapper",
|
||||
|
@ -4549,6 +4757,69 @@
|
|||
],
|
||||
"time": "2024-09-25T14:11:13+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v5.4.45",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
"reference": "63741784cd7b9967975eec610b256eed3ede022b"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/finder/zipball/63741784cd7b9967975eec610b256eed3ede022b",
|
||||
"reference": "63741784cd7b9967975eec610b256eed3ede022b",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=7.2.5",
|
||||
"symfony/deprecation-contracts": "^2.1|^3",
|
||||
"symfony/polyfill-php80": "^1.16"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Symfony\\Component\\Finder\\": ""
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/Tests/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "https://symfony.com/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Finds files and directories via an intuitive fluent interface",
|
||||
"homepage": "https://symfony.com",
|
||||
"support": {
|
||||
"source": "https://github.com/symfony/finder/tree/v5.4.45"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
"url": "https://symfony.com/sponsor",
|
||||
"type": "custom"
|
||||
},
|
||||
{
|
||||
"url": "https://github.com/fabpot",
|
||||
"type": "github"
|
||||
},
|
||||
{
|
||||
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||
"type": "tidelift"
|
||||
}
|
||||
],
|
||||
"time": "2024-09-28T13:32:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/polyfill-ctype",
|
||||
"version": "v1.32.0",
|
||||
|
|
|
@ -64,6 +64,8 @@ class AliasingContainer implements ContainerInterface
|
|||
*/
|
||||
public function has($key)
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
return $this->inner->has($this->getInnerKey($key));
|
||||
}
|
||||
|
||||
|
|
|
@ -64,7 +64,7 @@ class CachingContainer implements ContainerInterface
|
|||
throw new NotFoundException($this->__('Key "%1$s" not found in inner container', [$key]), 0, $e);
|
||||
} catch (Exception $e) {
|
||||
throw new ContainerException(
|
||||
$this->__('Could not retrieve value for key "%1$s from inner container', [$key]),
|
||||
$this->__('Could not retrieve value for key "%1$s" from inner container', [$key]),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
|
|
|
@ -38,7 +38,8 @@ class CompositeCachingServiceProvider implements ServiceProviderInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @inheritDoc
|
||||
*
|
||||
* @psalm-suppress InvalidNullableReturnType
|
||||
* It isn't actually going to return null ever, because $factories will be filled during indexing.
|
||||
*/
|
||||
|
@ -56,7 +57,8 @@ class CompositeCachingServiceProvider implements ServiceProviderInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
* @inheritDoc
|
||||
*
|
||||
* @psalm-suppress InvalidNullableReturnType
|
||||
* It isn't actually going to return null ever, because $factories will be filled during indexing.
|
||||
*/
|
||||
|
@ -78,7 +80,7 @@ class CompositeCachingServiceProvider implements ServiceProviderInterface
|
|||
*
|
||||
* Caches them internally.
|
||||
*
|
||||
* @param iterable|ServiceProviderInterface[] $providers The providers to index.
|
||||
* @param iterable<ServiceProviderInterface> $providers The providers to index.
|
||||
*/
|
||||
protected function indexProviderDefinitions(iterable $providers): void
|
||||
{
|
||||
|
|
|
@ -89,6 +89,7 @@ class DelegatingContainer implements ContainerInterface
|
|||
public function has($id)
|
||||
{
|
||||
$services = $this->provider->getFactories();
|
||||
$id = (string) $id;
|
||||
|
||||
return array_key_exists($id, $services);
|
||||
}
|
||||
|
|
|
@ -84,7 +84,10 @@ class DeprefixingContainer implements ContainerInterface
|
|||
*/
|
||||
public function has($key)
|
||||
{
|
||||
return $this->inner->has($this->getInnerKey($key)) || (!$this->strict && $this->inner->has($key));
|
||||
$key = (string) $key;
|
||||
$realKey = $this->getInnerKey($key);
|
||||
|
||||
return $this->inner->has($realKey) || (!$this->strict && $this->inner->has($key));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -5,13 +5,13 @@ declare(strict_types=1);
|
|||
namespace WooCommerce\PayPalCommerce\Vendor\Dhii\Container;
|
||||
|
||||
use ArrayIterator;
|
||||
use Traversable;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Collection\WritableContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Collection\WritableMapInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\Exception\NotFoundException;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\Util\StringTranslatingTrait;
|
||||
use IteratorAggregate;
|
||||
use RangeException;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* A simple mutable dictionary, i.e. an enumerable key-value map.
|
||||
|
@ -54,6 +54,8 @@ class Dictionary implements
|
|||
*/
|
||||
public function has($key)
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
$isHas = array_key_exists($key, $this->data);
|
||||
|
||||
return $isHas;
|
||||
|
|
|
@ -61,6 +61,8 @@ class FlashContainer implements
|
|||
*/
|
||||
public function has($key)
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
return array_key_exists($key, $this->flashData);
|
||||
}
|
||||
|
||||
|
|
|
@ -88,6 +88,8 @@ class HierarchyContainer implements ContainerInterface
|
|||
*/
|
||||
public function has($key)
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
return array_key_exists($key, $this->data);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -85,6 +85,8 @@ class MappingContainer implements ContainerInterface
|
|||
*/
|
||||
public function has($key)
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
return $this->inner->has($key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,6 +80,8 @@ class MaskingContainer implements ContainerInterface
|
|||
*/
|
||||
public function has($key)
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
return $this->isExposed($key) && $this->inner->has($key);
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ use WooCommerce\PayPalCommerce\Vendor\Dhii\Collection\WritableMapInterface;
|
|||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\Exception\ContainerException;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\Exception\NotFoundException;
|
||||
use IteratorAggregate;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* A container that does nothing.
|
||||
|
@ -92,7 +93,7 @@ class NoOpContainer implements
|
|||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getIterator()
|
||||
public function getIterator(): Traversable
|
||||
{
|
||||
return new ArrayIterator([]);
|
||||
}
|
||||
|
|
|
@ -127,6 +127,8 @@ class PathContainer implements ContainerInterface
|
|||
*/
|
||||
public function has($key)
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
/**
|
||||
* @psalm-suppress InvalidCatch
|
||||
* The base interface does not extend Throwable, but in fact everything that is possible
|
||||
|
|
|
@ -5,9 +5,12 @@ declare(strict_types=1);
|
|||
namespace WooCommerce\PayPalCommerce\Vendor\Dhii\Container;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Collection\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\Exception\ContainerException;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Dhii\Container\Exception\NotFoundException;
|
||||
use Exception;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface as PsrContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\NotFoundExceptionInterface;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* A container implementation that wraps around an inner container and prefixes its keys, requiring consumers to
|
||||
|
@ -89,11 +92,18 @@ class PrefixingContainer implements ContainerInterface
|
|||
*/
|
||||
public function has($key)
|
||||
{
|
||||
$key = (string) $key;
|
||||
if (!$this->isPrefixed($key) && $this->strict) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->inner->has($this->unprefix($key)) || (!$this->strict && $this->inner->has($key));
|
||||
try {
|
||||
$realKey = $this->unprefix($key);
|
||||
} catch (Exception $e) {
|
||||
throw new ContainerException(sprintf('Could not unprefix key "%1$s"', $key), 0, $e);
|
||||
}
|
||||
|
||||
return $this->inner->has($realKey) || (!$this->strict && $this->inner->has($key));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -108,7 +118,7 @@ class PrefixingContainer implements ContainerInterface
|
|||
protected function unprefix(string $key): string
|
||||
{
|
||||
return $this->isPrefixed($key)
|
||||
? substr($key, strlen($this->prefix))
|
||||
? $this->substring($key, strlen($this->prefix))
|
||||
: $key;
|
||||
}
|
||||
|
||||
|
@ -125,4 +135,35 @@ class PrefixingContainer implements ContainerInterface
|
|||
{
|
||||
return strlen($this->prefix) > 0 && strpos($key, $this->prefix) === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a substring from the specified string.
|
||||
*
|
||||
* @see substr()
|
||||
*
|
||||
* @param string $string The string to extract from.
|
||||
* @param int $offset The char position, at which to start extraction.
|
||||
* @param int|null $length The char position, at which to end extraction; unlimited if `null`.
|
||||
*
|
||||
* @return string The extracted substring.
|
||||
*
|
||||
* @throws RuntimeException If unable to extract.
|
||||
*/
|
||||
protected function substring(string $string, int $offset = 0, ?int $length = null): string
|
||||
{
|
||||
$length = $length ?? strlen($string) - $offset;
|
||||
$substring = substr($string, $offset, $length);
|
||||
if ($substring === false) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
'Could not extract substring starting at %1$d of length %2$s from string "%3$s"',
|
||||
$offset,
|
||||
$length ?: 'null',
|
||||
$string
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $substring;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,6 +114,8 @@ class SegmentingContainer implements ContainerInterface
|
|||
*/
|
||||
public function has($key)
|
||||
{
|
||||
$key = (string) $key;
|
||||
|
||||
return $this->inner->has($key);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,22 +5,22 @@ declare(strict_types=1);
|
|||
namespace WooCommerce\PayPalCommerce\Vendor\Dhii\Container;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* A value object capable of providing services.
|
||||
*
|
||||
* @package Dhii\Di
|
||||
* @psalm-type Factory = callable(ContainerInterface): mixed
|
||||
* @psalm-type Extension = callable(ContainerInterface, mixed): mixed
|
||||
*/
|
||||
class ServiceProvider implements ServiceProviderInterface
|
||||
{
|
||||
/** @var callable[] */
|
||||
protected array $factories;
|
||||
/**
|
||||
* @var callable[]
|
||||
*/
|
||||
protected $factories;
|
||||
/**
|
||||
* @var callable[]
|
||||
*/
|
||||
protected $extensions;
|
||||
protected array $extensions;
|
||||
|
||||
/**
|
||||
* @param callable[] $factories A map of service name to service factory.
|
||||
|
|
|
@ -55,6 +55,7 @@ class SimpleCacheContainer implements
|
|||
*/
|
||||
public function has($id)
|
||||
{
|
||||
$id = (string) $id;
|
||||
$storage = $this->storage;
|
||||
|
||||
try {
|
||||
|
@ -77,7 +78,11 @@ class SimpleCacheContainer implements
|
|||
try {
|
||||
$storage->set($key, $value, $ttl);
|
||||
} catch (Exception $e) {
|
||||
throw new ContainerException(sprintf('Could not set key "%1$s" with value "%2$s"', $key, $value), 0, $e);
|
||||
throw new ContainerException(
|
||||
sprintf('Could not set key "%1$s" with value "%2$s"', $key, (string) $value),
|
||||
0,
|
||||
$e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,7 +110,7 @@ class SimpleCacheContainer implements
|
|||
try {
|
||||
$storage->clear();
|
||||
} catch (Exception $e) {
|
||||
throw new ContainerException(sprintf('Could not clear container'), 0, $e);
|
||||
throw new ContainerException('Could not clear container', 0, $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
121
lib/packages/Dhii/Container/TaggingServiceProvider.php
Normal file
121
lib/packages/Dhii/Container/TaggingServiceProvider.php
Normal file
|
@ -0,0 +1,121 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Vendor\Dhii\Container;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Vendor\Interop\Container\ServiceProviderInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use ReflectionException;
|
||||
use ReflectionFunction;
|
||||
use ReflectionObject;
|
||||
|
||||
/**
|
||||
* A service provider that detects tags in factory docBlocks, and exposes them as services.
|
||||
*
|
||||
* A service may have a docBlock. The docBlock may contain various docBlock tags, such as `@param` or `@return`.
|
||||
* This class will detect `@tag {tagname}` tags in service docBlocks. `tagname` may be anything that a service
|
||||
* key may be - they exist in the same namespace. In fact, a `tagname` corresponds to a service
|
||||
* that returns a list of tagged services. To retrieve them, just resolve the tagname as a service.
|
||||
*
|
||||
* For each unique `tagname` in factory docBlocks, this service provider will create an extension with
|
||||
* an identical name. This extension at resolution time will resolve each tagged service by key,
|
||||
* and add resulting services to the list it is extending. To ensure there's always a list to extend,
|
||||
* this service provider will also add a service with an identical name, which resolves to an empty list.
|
||||
* All such "tag" services are empty list in the beginning of their resolution, so it doesn't matter
|
||||
* if it gets overwritten by another module's identical empty list.
|
||||
*
|
||||
* @psalm-import-type Factory from ServiceProvider
|
||||
* @psalm-import-type Extension from ServiceProvider
|
||||
*/
|
||||
class TaggingServiceProvider implements ServiceProviderInterface
|
||||
{
|
||||
/** @var array<Factory> */
|
||||
protected array $factories;
|
||||
/** @var array<Extension> */
|
||||
protected array $extensions;
|
||||
|
||||
public function __construct(ServiceProviderInterface $inner)
|
||||
{
|
||||
$this->factories = $inner->getFactories();
|
||||
$this->extensions = $inner->getExtensions();
|
||||
$this->indexTags();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getFactories()
|
||||
{
|
||||
return $this->factories;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getExtensions()
|
||||
{
|
||||
return $this->extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indexes tagged factories, and creates factories and extensions for tags.
|
||||
*
|
||||
* @throws ReflectionException If problem obtaining factory reflection.
|
||||
*/
|
||||
protected function indexTags(): void
|
||||
{
|
||||
$tags = [];
|
||||
|
||||
foreach ($this->factories as $serviceName => $factory) {
|
||||
if (is_string($factory)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reflection = is_object($factory) && get_class($factory) === 'Closure'
|
||||
? new ReflectionFunction($factory)
|
||||
: new ReflectionObject($factory);
|
||||
$docBlock = $reflection->getDocComment();
|
||||
|
||||
// No docblock
|
||||
if ($docBlock === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$factoryTags = $this->getTagsFromDocBlock($docBlock);
|
||||
foreach ($factoryTags as $tag) {
|
||||
if (!isset($tags[$tag]) || !is_array($tags[$tag])) {
|
||||
$tags[$tag] = [];
|
||||
}
|
||||
$tags[$tag][] = $serviceName;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($tags as $tag => $taggedServiceNames) {
|
||||
$this->factories[$tag] = fn (): array => [];
|
||||
$this->extensions[$tag] = function (ContainerInterface $c, array $prev) use ($taggedServiceNames): array {
|
||||
return array_merge(
|
||||
$prev,
|
||||
array_map(fn (string $serviceName) => $c->get($serviceName), $taggedServiceNames)
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves tags names that are part of a docBlock.
|
||||
*
|
||||
* @link https://www.php.net/manual/en/reflectionclass.getdoccomment.php#118606
|
||||
*
|
||||
* @param string $docBlock The docBlock.
|
||||
*
|
||||
* @return array<string> A list of tag names.
|
||||
*/
|
||||
protected function getTagsFromDocBlock(string $docBlock): array
|
||||
{
|
||||
$regex = '#^\s*/?\**\s*(@tag\s*(?P<tags>[^\s]+))#m';
|
||||
preg_match_all($regex, $docBlock, $matches);
|
||||
|
||||
return $matches['tags'];
|
||||
}
|
||||
}
|
|
@ -27,6 +27,7 @@ trait StringTranslatingTrait
|
|||
*/
|
||||
protected function __(string $string, array $args = array(), $context = null): string
|
||||
{
|
||||
$context = (string) $context;
|
||||
$string = $this->_translate($string, $context);
|
||||
array_unshift($args, $string);
|
||||
return call_user_func_array('sprintf', $args);
|
||||
|
|
|
@ -1,52 +1,38 @@
|
|||
<?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;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type Service from \WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule
|
||||
* @phpstan-import-type ExtendingService from \WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule
|
||||
*/
|
||||
class ContainerConfigurator
|
||||
{
|
||||
/**
|
||||
* @var array<string, callable(ContainerInterface $container):mixed>
|
||||
*/
|
||||
private $services = [];
|
||||
/** @var array<string, Service> */
|
||||
private array $services = [];
|
||||
/** @var array<string, bool> */
|
||||
private array $factoryIds = [];
|
||||
private ServiceExtensions $extensions;
|
||||
private ?ContainerInterface $compiledContainer = null;
|
||||
/** @var ContainerInterface[] */
|
||||
private array $containers = [];
|
||||
|
||||
/**
|
||||
* @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 = [])
|
||||
public function __construct(array $containers = [], ?ServiceExtensions $extensions = null)
|
||||
{
|
||||
array_map([$this, 'addContainer'], $containers);
|
||||
$this->extensions = $extensions ?? new ServiceExtensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Allowing to add child containers.
|
||||
*
|
||||
* @param ContainerInterface $container
|
||||
* @return void
|
||||
*/
|
||||
public function addContainer(ContainerInterface $container): void
|
||||
{
|
||||
|
@ -55,7 +41,7 @@ class ContainerConfigurator
|
|||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param callable(ContainerInterface $container):mixed $factory
|
||||
* @param Service $factory
|
||||
*/
|
||||
public function addFactory(string $id, callable $factory): void
|
||||
{
|
||||
|
@ -67,8 +53,7 @@ class ContainerConfigurator
|
|||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param callable(ContainerInterface $container):mixed $service
|
||||
*
|
||||
* @param Service $service
|
||||
* @return void
|
||||
*/
|
||||
public function addService(string $id, callable $service): void
|
||||
|
@ -89,7 +74,6 @@ class ContainerConfigurator
|
|||
|
||||
/**
|
||||
* @param string $id
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasService(string $id): bool
|
||||
|
@ -109,37 +93,31 @@ class ContainerConfigurator
|
|||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param callable(mixed $service, ContainerInterface $container):mixed $extender
|
||||
*
|
||||
* @param ExtendingService $extender
|
||||
* @return void
|
||||
*/
|
||||
public function addExtension(string $id, callable $extender): void
|
||||
{
|
||||
if (!isset($this->extensions[$id])) {
|
||||
$this->extensions[$id] = [];
|
||||
}
|
||||
|
||||
$this->extensions[$id][] = $extender;
|
||||
$this->extensions->add($id, $extender);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasExtension(string $id): bool
|
||||
{
|
||||
return isset($this->extensions[$id]);
|
||||
return $this->extensions->has($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a read only version of this Container.
|
||||
*
|
||||
* @return ContainerInterface
|
||||
*
|
||||
* @phpstan-assert ContainerInterface $this->compiledContainer
|
||||
*/
|
||||
public function createReadOnlyContainer(): ContainerInterface
|
||||
{
|
||||
if (!$this->compiledContainer) {
|
||||
if ($this->compiledContainer === null) {
|
||||
$this->compiledContainer = new ReadOnlyContainer(
|
||||
$this->services,
|
||||
$this->factoryIds,
|
||||
|
|
|
@ -10,15 +10,8 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
|||
|
||||
class PackageProxyContainer implements ContainerInterface
|
||||
{
|
||||
/**
|
||||
* @var Package
|
||||
*/
|
||||
private $package;
|
||||
|
||||
/**
|
||||
* @var ContainerInterface|null
|
||||
*/
|
||||
private $container;
|
||||
private Package $package;
|
||||
private ?ContainerInterface $container = null;
|
||||
|
||||
/**
|
||||
* @param Package $package
|
||||
|
@ -31,8 +24,6 @@ class PackageProxyContainer implements ContainerInterface
|
|||
/**
|
||||
* @param string $id
|
||||
* @return mixed
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function get(string $id)
|
||||
{
|
||||
|
@ -44,8 +35,6 @@ class PackageProxyContainer implements ContainerInterface
|
|||
/**
|
||||
* @param string $id
|
||||
* @return bool
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function has(string $id): bool
|
||||
{
|
||||
|
@ -55,33 +44,30 @@ class PackageProxyContainer implements ContainerInterface
|
|||
/**
|
||||
* @return bool
|
||||
*
|
||||
* @throws \Exception
|
||||
* @psalm-assert-if-true ContainerInterface $this->container
|
||||
* @phpstan-assert-if-true ContainerInterface $this->container
|
||||
* @phpstan-assert-if-false null $this->container
|
||||
*/
|
||||
private function tryContainer(): bool
|
||||
{
|
||||
if ($this->container) {
|
||||
if ($this->container !== null) {
|
||||
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->package->hasContainer()
|
||||
|| $this->package->hasReachedStatus(Package::STATUS_INITIALIZED)
|
||||
) {
|
||||
$this->container = $this->package->container();
|
||||
}
|
||||
|
||||
return (bool)$this->container;
|
||||
return $this->container !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @return void
|
||||
*
|
||||
* @throws \Exception
|
||||
*
|
||||
* @psalm-assert ContainerInterface $this->container
|
||||
* @phpstan-assert ContainerInterface $this->container
|
||||
*/
|
||||
private function assertPackageBooted(string $id): void
|
||||
{
|
||||
|
@ -90,13 +76,11 @@ class PackageProxyContainer implements ContainerInterface
|
|||
}
|
||||
|
||||
$name = $this->package->name();
|
||||
$status = $this->package->statusIs(Package::STATUS_FAILED)
|
||||
? 'is errored'
|
||||
: 'is not ready yet';
|
||||
$status = $this->package->hasFailed() ? 'is errored' : 'is not ready yet';
|
||||
|
||||
throw new class ("Error retrieving service {$id} because package {$name} {$status}.")
|
||||
extends \Exception
|
||||
implements ContainerExceptionInterface {
|
||||
$error = "Error retrieving service {$id} because package {$name} {$status}.";
|
||||
throw new class (esc_html($error)) extends \Exception implements ContainerExceptionInterface
|
||||
{
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,58 +7,43 @@ namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container;
|
|||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type Service from \WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule
|
||||
* @phpstan-import-type ExtendingService from \WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule
|
||||
*/
|
||||
class ReadOnlyContainer implements ContainerInterface
|
||||
{
|
||||
/**
|
||||
* @var array<string, callable(\WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface $container):mixed>
|
||||
*/
|
||||
private $services;
|
||||
/** @var array<string, Service> */
|
||||
private array $services;
|
||||
/** @var array<string, bool> */
|
||||
private array $factoryIds;
|
||||
private ServiceExtensions $extensions;
|
||||
/** @var ContainerInterface[] */
|
||||
private array $containers;
|
||||
/** @var array<string, mixed> */
|
||||
private array $resolvedServices = [];
|
||||
|
||||
/**
|
||||
* @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, Service> $services
|
||||
* @param array<string, bool> $factoryIds
|
||||
* @param array<string, array<callable(mixed, ContainerInterface $container):mixed>> $extensions
|
||||
* @param ServiceExtensions|array<string, ExtendingService> $extensions
|
||||
* @param ContainerInterface[] $containers
|
||||
*/
|
||||
public function __construct(
|
||||
array $services,
|
||||
array $factoryIds,
|
||||
array $extensions,
|
||||
$extensions,
|
||||
array $containers
|
||||
) {
|
||||
|
||||
$this->services = $services;
|
||||
$this->factoryIds = $factoryIds;
|
||||
$this->extensions = $extensions;
|
||||
$this->extensions = $this->configureServiceExtensions($extensions);
|
||||
$this->containers = $containers;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $id)
|
||||
|
@ -69,7 +54,7 @@ class ReadOnlyContainer implements ContainerInterface
|
|||
|
||||
if (array_key_exists($id, $this->services)) {
|
||||
$service = $this->services[$id]($this);
|
||||
$resolved = $this->resolveExtensions($id, $service);
|
||||
$resolved = $this->extensions->resolve($service, $id, $this);
|
||||
|
||||
if (!isset($this->factoryIds[$id])) {
|
||||
$this->resolvedServices[$id] = $resolved;
|
||||
|
@ -83,19 +68,18 @@ class ReadOnlyContainer implements ContainerInterface
|
|||
if ($container->has($id)) {
|
||||
$service = $container->get($id);
|
||||
|
||||
return $this->resolveExtensions($id, $service);
|
||||
return $this->extensions->resolve($service, $id, $this);
|
||||
}
|
||||
}
|
||||
|
||||
throw new class ("Service with ID {$id} not found.")
|
||||
extends \Exception
|
||||
implements NotFoundExceptionInterface {
|
||||
$error = "Service with ID {$id} not found.";
|
||||
throw new class (esc_html($error)) extends \Exception implements NotFoundExceptionInterface
|
||||
{
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $id): bool
|
||||
|
@ -118,21 +102,43 @@ class ReadOnlyContainer implements ContainerInterface
|
|||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param mixed $service
|
||||
* Support extensions as array or ServiceExtensions instance for backward compatibility.
|
||||
*
|
||||
* @return mixed
|
||||
* With PHP 8+ we could use an actual union type, but when we bump to PHP 8 as min supported
|
||||
* version, we will probably bump major version as well, so we can just get rid of support
|
||||
* for array.
|
||||
*
|
||||
* @param mixed $extensions
|
||||
* @return ServiceExtensions
|
||||
*/
|
||||
private function resolveExtensions(string $id, $service)
|
||||
private function configureServiceExtensions($extensions): ServiceExtensions
|
||||
{
|
||||
if (!isset($this->extensions[$id])) {
|
||||
return $service;
|
||||
if ($extensions instanceof ServiceExtensions) {
|
||||
return $extensions;
|
||||
}
|
||||
|
||||
foreach ($this->extensions[$id] as $extender) {
|
||||
$service = $extender($service, $this);
|
||||
if (!is_array($extensions)) {
|
||||
$type = is_object($extensions) ? get_class($extensions) : gettype($extensions);
|
||||
throw new \TypeError(
|
||||
sprintf(
|
||||
'%s::%s(): Argument #3 ($extensions) must be of type %s|array, %s given',
|
||||
__CLASS__,
|
||||
'__construct',
|
||||
ServiceExtensions::class,
|
||||
esc_html($type)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $service;
|
||||
$servicesExtensions = new ServiceExtensions();
|
||||
foreach ($extensions as $id => $callback) {
|
||||
/**
|
||||
* @var string $id
|
||||
* @var ExtendingService $callback
|
||||
*/
|
||||
$servicesExtensions->add($id, $callback);
|
||||
}
|
||||
|
||||
return $servicesExtensions;
|
||||
}
|
||||
}
|
||||
|
|
194
lib/packages/Inpsyde/Modularity/Container/ServiceExtensions.php
Normal file
194
lib/packages/Inpsyde/Modularity/Container/ServiceExtensions.php
Normal file
|
@ -0,0 +1,194 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Container;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface as Container;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type ExtendingService from \WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule
|
||||
*/
|
||||
class ServiceExtensions
|
||||
{
|
||||
private const SERVICE_TYPE_NOT_CHANGED = 1;
|
||||
private const SERVICE_TYPE_CHANGED = 2;
|
||||
private const SERVICE_TYPE_NOT_OBJECT = 0;
|
||||
|
||||
/** @var array<string, list<ExtendingService>> */
|
||||
protected array $extensions = [];
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
final public static function typeId(string $type): string
|
||||
{
|
||||
return "@instanceof<{$type}>";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extensionId
|
||||
* @param ExtendingService $extender
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public function add(string $extensionId, callable $extender): ServiceExtensions
|
||||
{
|
||||
if (!isset($this->extensions[$extensionId])) {
|
||||
$this->extensions[$extensionId] = [];
|
||||
}
|
||||
$this->extensions[$extensionId][] = $extender;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $extensionId
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $extensionId): bool
|
||||
{
|
||||
return isset($this->extensions[$extensionId]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $service
|
||||
* @param string $id
|
||||
* @param Container $container
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
final public function resolve($service, string $id, Container $container)
|
||||
{
|
||||
$service = $this->resolveById($id, $service, $container);
|
||||
|
||||
return is_object($service)
|
||||
? $this->resolveByType(get_class($service), $service, $container)
|
||||
: $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
* @param mixed $service
|
||||
* @param Container $container
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
protected function resolveById(string $id, $service, Container $container)
|
||||
{
|
||||
foreach ($this->extensions[$id] ?? [] as $extender) {
|
||||
$service = $extender($service, $container);
|
||||
}
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $className
|
||||
* @param object $service
|
||||
* @param Container $container
|
||||
* @param string[] $extendedClasses
|
||||
*
|
||||
* @return mixed
|
||||
*
|
||||
* phpcs:disable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
|
||||
* phpcs:disable Syde.Functions.ReturnTypeDeclaration.NoReturnType
|
||||
*/
|
||||
protected function resolveByType(
|
||||
string $className,
|
||||
object $service,
|
||||
Container $container,
|
||||
array $extendedClasses = []
|
||||
) {
|
||||
// phpcs:enable SlevomatCodingStandard.Complexity.Cognitive.ComplexityTooHigh
|
||||
// phpcs:enable Syde.Functions.ReturnTypeDeclaration.NoReturnType
|
||||
|
||||
$extendedClasses[] = $className;
|
||||
|
||||
/** @var array<class-string, list<ExtendingService>> $allCallbacks */
|
||||
$allCallbacks = [];
|
||||
|
||||
// 1st group of extensions: targeting exact class
|
||||
$byClass = $this->extensions[self::typeId($className)] ?? null;
|
||||
if (($byClass !== null) && ($byClass !== [])) {
|
||||
$allCallbacks[$className] = $byClass;
|
||||
}
|
||||
|
||||
// 2nd group of extensions: targeting parent classes
|
||||
$parents = class_parents($service, false) ?: [];
|
||||
foreach ($parents as $parentName) {
|
||||
$byParent = $this->extensions[self::typeId($parentName)] ?? null;
|
||||
if (($byParent !== null) && ($byParent !== [])) {
|
||||
$allCallbacks[$parentName] = $byParent;
|
||||
}
|
||||
}
|
||||
|
||||
// 3rd group of extensions: targeting implemented interfaces
|
||||
$interfaces = class_implements($service, false) ?: [];
|
||||
foreach ($interfaces as $interfaceName) {
|
||||
$byInterface = $this->extensions[self::typeId($interfaceName)] ?? null;
|
||||
if (($byInterface !== null) && ($byInterface !== [])) {
|
||||
$allCallbacks[$interfaceName] = $byInterface;
|
||||
}
|
||||
}
|
||||
|
||||
$resultType = self::SERVICE_TYPE_NOT_CHANGED;
|
||||
/** @var class-string $type */
|
||||
foreach ($allCallbacks as $type => $extenders) {
|
||||
// When the previous group of callbacks resulted in a type change, we need to check
|
||||
// type before processing next group.
|
||||
if (($resultType === self::SERVICE_TYPE_CHANGED) && !is_a($service, $type)) {
|
||||
continue;
|
||||
}
|
||||
/** @var object $service */
|
||||
[$service, $resultType] = $this->extendByType($type, $service, $container, $extenders);
|
||||
if ($resultType === self::SERVICE_TYPE_NOT_OBJECT) {
|
||||
// Service is not an object anymore, let's return it.
|
||||
return $service;
|
||||
}
|
||||
}
|
||||
|
||||
// If type changed since beginning, let's start over.
|
||||
// We check if class was already extended to avoid infinite recursion. E.g. instead of:
|
||||
// `-> extend(A): B -> extend(B): A -> *loop* ->`
|
||||
// we have:
|
||||
// `-> extend(A): B -> extend(B): A -> return A`.
|
||||
$newClassName = get_class($service);
|
||||
if (!in_array($newClassName, $extendedClasses, true)) {
|
||||
return $this->resolveByType($newClassName, $service, $container, $extendedClasses);
|
||||
}
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param class-string $type
|
||||
* @param object $service
|
||||
* @param Container $container
|
||||
* @param list<ExtendingService> $extenders
|
||||
*
|
||||
* @return list{mixed, int}
|
||||
*/
|
||||
private function extendByType(
|
||||
string $type,
|
||||
object $service,
|
||||
Container $container,
|
||||
array $extenders
|
||||
): array {
|
||||
|
||||
foreach ($extenders as $extender) {
|
||||
$service = $extender($service, $container);
|
||||
if (!is_object($service)) {
|
||||
return [$service, self::SERVICE_TYPE_NOT_OBJECT];
|
||||
}
|
||||
if (!is_a($service, $type)) {
|
||||
return [$service, self::SERVICE_TYPE_CHANGED];
|
||||
}
|
||||
}
|
||||
|
||||
return [$service, self::SERVICE_TYPE_NOT_CHANGED];
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ 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.
|
||||
|
|
|
@ -4,9 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* @phpstan-type ExtendingService callable(mixed $service, ContainerInterface $container): mixed
|
||||
*/
|
||||
interface ExtendingModule extends Module
|
||||
{
|
||||
|
||||
/**
|
||||
* Return application services' extensions.
|
||||
*
|
||||
|
@ -18,7 +22,7 @@ interface ExtendingModule extends Module
|
|||
* 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>
|
||||
* @return array<string, ExtendingService>
|
||||
*/
|
||||
public function extensions(): array;
|
||||
}
|
||||
|
|
|
@ -4,6 +4,9 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type Service from ServiceModule
|
||||
*/
|
||||
interface FactoryModule extends Module
|
||||
{
|
||||
/**
|
||||
|
@ -12,7 +15,7 @@ interface FactoryModule extends Module
|
|||
* 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>
|
||||
* @return array<string, Service>
|
||||
*/
|
||||
public function factories(): array;
|
||||
}
|
||||
|
|
|
@ -9,12 +9,10 @@ namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
|
|||
*/
|
||||
interface Module
|
||||
{
|
||||
|
||||
/**
|
||||
* Unique identifier for your Module.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function id(): string;
|
||||
|
||||
}
|
||||
|
|
|
@ -4,16 +4,11 @@ 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
|
||||
|
|
|
@ -4,9 +4,13 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
|
||||
/**
|
||||
* @phpstan-type Service callable(ContainerInterface $container): mixed
|
||||
*/
|
||||
interface ServiceModule extends Module
|
||||
{
|
||||
|
||||
/**
|
||||
* Return application services' factories.
|
||||
*
|
||||
|
@ -15,7 +19,7 @@ interface ServiceModule extends Module
|
|||
* 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>
|
||||
* @return array<string, Service>
|
||||
*/
|
||||
public function services(): array;
|
||||
}
|
||||
|
|
|
@ -6,19 +6,22 @@ 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\ExtendingModule;
|
||||
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;
|
||||
|
||||
/**
|
||||
* @phpstan-import-type Service from \WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ServiceModule
|
||||
* @phpstan-import-type ExtendingService from \WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule
|
||||
*/
|
||||
class Package
|
||||
{
|
||||
/**
|
||||
* All the hooks fired in this class use this prefix.
|
||||
* @var string
|
||||
*/
|
||||
private const HOOK_PREFIX = 'inpsyde.modularity.';
|
||||
|
||||
|
@ -34,14 +37,13 @@ class Package
|
|||
* $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.
|
||||
* Custom action to be used to add modules and connect other packages.
|
||||
* It might also be used to access package properties.
|
||||
* Access container is not possible at this stage.
|
||||
*
|
||||
* @example
|
||||
* <code>
|
||||
|
@ -49,67 +51,64 @@ class Package
|
|||
*
|
||||
* add_action(
|
||||
* $package->hookName(Package::ACTION_INIT),
|
||||
* $callback
|
||||
* fn (Package $package) => // do something,
|
||||
* );
|
||||
* </code>
|
||||
*/
|
||||
public const ACTION_INIT = 'init';
|
||||
|
||||
/**
|
||||
* Custom action which is triggered after the application
|
||||
* is booted to access container and properties.
|
||||
* Very similar to `ACTION_INIT`, but it is static, so not dependent on package name.
|
||||
* It passes package name as first argument.
|
||||
*
|
||||
* @example
|
||||
* <code>
|
||||
* $package = Package::new();
|
||||
*
|
||||
* add_action(
|
||||
* $package->hookName(Package::ACTION_READY),
|
||||
* $callback
|
||||
* );
|
||||
* </code>
|
||||
* <code>
|
||||
* add_action(
|
||||
* Package::ACTION_MODULARITY_INIT,
|
||||
* fn (string $packageName, Package $package) => // do something,
|
||||
* 10,
|
||||
* 2
|
||||
* );
|
||||
* </code>
|
||||
*/
|
||||
public const ACTION_READY = 'ready';
|
||||
public const ACTION_MODULARITY_INIT = self::HOOK_PREFIX . self::ACTION_INIT;
|
||||
|
||||
/**
|
||||
* 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>
|
||||
* Action fired when it is safe to access container.
|
||||
* Add more modules is not anymore possible at this stage.
|
||||
*/
|
||||
public const ACTION_INITIALIZED = 'initialized';
|
||||
|
||||
/**
|
||||
* Action fired when plugin finished its bootstrapping process, all its hooks are added.
|
||||
* Add more modules is not anymore possible at this stage.
|
||||
*/
|
||||
public const ACTION_BOOTED = 'ready';
|
||||
|
||||
/**
|
||||
* Action fired when anything went wrong during the "build" procedure.
|
||||
*/
|
||||
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>
|
||||
* Action fired when anything went wrong during the "boot" procedure.
|
||||
*/
|
||||
public const ACTION_FAILED_BOOT = 'failed-boot';
|
||||
|
||||
/**
|
||||
* Custom action which is triggered when a package is connected.
|
||||
* Action fired when adding a module failed.
|
||||
*/
|
||||
public const ACTION_PACKAGE_CONNECTED = 'package-connected';
|
||||
public const ACTION_FAILED_ADD_MODULE = 'failed-add-module';
|
||||
|
||||
/**
|
||||
* Custom action which is triggered when a package cannot be connected.
|
||||
* Action fired when a package connection failed.
|
||||
*/
|
||||
public const ACTION_FAILED_CONNECTION = 'failed-connection';
|
||||
public const ACTION_FAILED_CONNECT = 'failed-connection';
|
||||
|
||||
/**
|
||||
* Action fired when a package is connected successfully.
|
||||
*/
|
||||
public const ACTION_PACKAGE_CONNECTED = 'package-connected';
|
||||
|
||||
/**
|
||||
* Module states can be used to get information about your module.
|
||||
|
@ -118,7 +117,7 @@ class Package
|
|||
* <code>
|
||||
* $package = Package::new();
|
||||
* $package->moduleIs(SomeModule::class, Package::MODULE_ADDED); // false
|
||||
* $package->boot(new SomeModule());
|
||||
* $package->addModule(new SomeModule());
|
||||
* $package->moduleIs(SomeModule::class, Package::MODULE_ADDED); // true
|
||||
* </code>
|
||||
*/
|
||||
|
@ -137,90 +136,81 @@ class Package
|
|||
* @example
|
||||
* <code>
|
||||
* $package = Package::new();
|
||||
* $package->statusIs(Package::IDLE); // true
|
||||
* $package->statusIs(Package::STATUS_IDLE); // true
|
||||
* $package->build();
|
||||
* $package->statusIs(Package::STATUS_INITIALIZED); // true
|
||||
* $package->boot();
|
||||
* $package->statusIs(Package::BOOTED); // true
|
||||
* $package->statusIs(Package::STATUS_DONE); // true
|
||||
* </code>
|
||||
*/
|
||||
public const STATUS_IDLE = 2;
|
||||
public const STATUS_INITIALIZING = 3;
|
||||
public const STATUS_INITIALIZED = 4;
|
||||
public const STATUS_MODULES_ADDED = 5;
|
||||
public const STATUS_READY = 7;
|
||||
public const STATUS_BOOTED = 8;
|
||||
public const STATUS_BOOTING = 5;
|
||||
public const STATUS_BOOTED = 7;
|
||||
public const STATUS_DONE = 8;
|
||||
public const STATUS_FAILED = -8;
|
||||
|
||||
/**
|
||||
* Current state of the application.
|
||||
*
|
||||
* @see Package::STATUS_*
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $status = self::STATUS_IDLE;
|
||||
// Deprecated flags
|
||||
/** @deprecated */
|
||||
public const STATUS_MODULES_ADDED = self::STATUS_BOOTING;
|
||||
/** @deprecated */
|
||||
public const ACTION_READY = self::ACTION_BOOTED;
|
||||
/** @deprecated */
|
||||
public const ACTION_FAILED_CONNECTION = self::ACTION_FAILED_CONNECT;
|
||||
|
||||
/**
|
||||
* Contains the progress of all modules.
|
||||
*
|
||||
* @see Package::moduleProgress()
|
||||
*
|
||||
* @var array<string, list<string>>
|
||||
*/
|
||||
private $moduleStatus = [self::MODULES_ALL => []];
|
||||
// Map of status to package-specific and global hook, both optional (i..e, null).
|
||||
private const STATUSES_ACTIONS_MAP = [
|
||||
self::STATUS_INITIALIZING => [self::ACTION_INIT, self::ACTION_MODULARITY_INIT],
|
||||
self::STATUS_INITIALIZED => [self::ACTION_INITIALIZED, null],
|
||||
self::STATUS_BOOTED => [self::ACTION_BOOTED, null],
|
||||
];
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
private const SUCCESS_STATUSES = [
|
||||
self::STATUS_IDLE => self::STATUS_IDLE,
|
||||
self::STATUS_INITIALIZING => self::STATUS_INITIALIZING,
|
||||
self::STATUS_INITIALIZED => self::STATUS_INITIALIZED,
|
||||
self::STATUS_BOOTING => self::STATUS_BOOTING,
|
||||
self::STATUS_BOOTED => self::STATUS_BOOTED,
|
||||
self::STATUS_DONE => self::STATUS_DONE,
|
||||
];
|
||||
|
||||
/**
|
||||
* @var list<ExecutableModule>
|
||||
*/
|
||||
private $executables = [];
|
||||
private const OPERATORS = [
|
||||
'<' => '<',
|
||||
'<=' => '<=',
|
||||
'>' => '>',
|
||||
'>=' => '>=',
|
||||
'==' => '==',
|
||||
'!=' => '!=',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var Properties
|
||||
*/
|
||||
private $properties;
|
||||
|
||||
/**
|
||||
* @var ContainerConfigurator
|
||||
*/
|
||||
private $containerConfigurator;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $built = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $hasContainer = false;
|
||||
|
||||
/**
|
||||
* @var \Throwable|null
|
||||
*/
|
||||
private $lastError = null;
|
||||
/** @var Package::STATUS_* */
|
||||
private int $status = self::STATUS_IDLE;
|
||||
/** @var array<string, list<string>> */
|
||||
private array $moduleStatus = [self::MODULES_ALL => []];
|
||||
/** @var array<string, bool> */
|
||||
private array $connectedPackages = [];
|
||||
/** @var list<ExecutableModule> */
|
||||
private array $executables = [];
|
||||
private Properties $properties;
|
||||
private ContainerConfigurator $containerConfigurator;
|
||||
private bool $built = false;
|
||||
private bool $hasContainer = false;
|
||||
private ?\Throwable $lastError = null;
|
||||
|
||||
/**
|
||||
* @param Properties $properties
|
||||
* @param ContainerInterface[] $containers
|
||||
*
|
||||
* @param ContainerInterface ...$containers
|
||||
* @return Package
|
||||
*/
|
||||
public static function new(Properties $properties, ContainerInterface ...$containers): Package
|
||||
public static function new(Properties $properties, ContainerInterface ...$containers): Package
|
||||
{
|
||||
return new self($properties, ...$containers);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Properties $properties
|
||||
* @param ContainerInterface[] $containers
|
||||
* @param ContainerInterface ...$containers
|
||||
*/
|
||||
private function __construct(Properties $properties, ContainerInterface ...$containers)
|
||||
{
|
||||
|
@ -229,7 +219,7 @@ class Package
|
|||
$this->containerConfigurator = new ContainerConfigurator($containers);
|
||||
$this->containerConfigurator->addService(
|
||||
self::PROPERTIES,
|
||||
static function () use ($properties) {
|
||||
static function () use ($properties): Properties {
|
||||
return $properties;
|
||||
}
|
||||
);
|
||||
|
@ -237,14 +227,14 @@ class Package
|
|||
|
||||
/**
|
||||
* @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()));
|
||||
$reason = sprintf('add module %s', $module->id());
|
||||
$this->assertStatus(self::STATUS_FAILED, $reason, '!=');
|
||||
$this->assertStatus(self::STATUS_INITIALIZING, $reason, '<=');
|
||||
|
||||
$registeredServices = $this->addModuleServices(
|
||||
$module,
|
||||
|
@ -271,7 +261,7 @@ class Package
|
|||
$status = $added ? self::MODULE_ADDED : self::MODULE_NOT_ADDED;
|
||||
$this->moduleProgress($module->id(), $status);
|
||||
} catch (\Throwable $throwable) {
|
||||
$this->handleFailure($throwable, self::ACTION_FAILED_BUILD);
|
||||
$this->handleFailure($throwable, self::ACTION_FAILED_ADD_MODULE);
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
@ -280,7 +270,6 @@ class Package
|
|||
/**
|
||||
* @param Package $package
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function connect(Package $package): bool
|
||||
{
|
||||
|
@ -290,33 +279,17 @@ class Package
|
|||
}
|
||||
|
||||
$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);
|
||||
return $this->handleConnectionFailure($packageName, 'already connected', false);
|
||||
}
|
||||
|
||||
// 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);
|
||||
$failed = $this->hasFailed();
|
||||
if ($failed || $this->hasReachedStatus(self::STATUS_INITIALIZED)) {
|
||||
$reason = $failed ? 'is errored' : 'has a built container already';
|
||||
$this->handleConnectionFailure($packageName, "current package {$reason}", true);
|
||||
}
|
||||
|
||||
$this->connectedPackages[$packageName] = true;
|
||||
|
@ -330,9 +303,10 @@ class Package
|
|||
}
|
||||
);
|
||||
|
||||
// If the other package is booted, we can obtain a container, otherwise
|
||||
// we build a proxy container
|
||||
$container = $package->statusIs(self::STATUS_BOOTED)
|
||||
// If we can obtain a container we do, otherwise we build a proxy container
|
||||
$packageHasContainer = $package->hasReachedStatus(self::STATUS_INITIALIZED)
|
||||
|| $package->hasContainer();
|
||||
$container = $packageHasContainer
|
||||
? $package->container()
|
||||
: new PackageProxyContainer($package);
|
||||
|
||||
|
@ -347,7 +321,10 @@ class Package
|
|||
|
||||
return true;
|
||||
} catch (\Throwable $throwable) {
|
||||
if (isset($packageName)) {
|
||||
if (
|
||||
isset($packageName)
|
||||
&& (($this->connectedPackages[$packageName] ?? false) !== true)
|
||||
) {
|
||||
$this->connectedPackages[$packageName] = false;
|
||||
}
|
||||
$this->handleFailure($throwable, self::ACTION_FAILED_BUILD);
|
||||
|
@ -362,17 +339,26 @@ class Package
|
|||
public function build(): Package
|
||||
{
|
||||
try {
|
||||
// Don't allow building the application multiple times.
|
||||
// Be tolerant about things like `$package->build()->build()`.
|
||||
// Sometimes, from the extern, we might want to call `build()` to ensure the container
|
||||
// is ready before accessing a service. And in that case we don't want to throw an
|
||||
// exception if the container is already built.
|
||||
if ($this->built && $this->statusIs(self::STATUS_INITIALIZED)) {
|
||||
return $this;
|
||||
}
|
||||
|
||||
// We expect `build` to be called only after `addModule()` or `connect()` which do
|
||||
// not change the status, so we expect status to be still "IDLE".
|
||||
// This will prevent invalid things like calling `build()` from inside something
|
||||
// hooking ACTION_INIT OR ACTION_INITIALIZED.
|
||||
$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 will change the status to "INITIALIZING" then fire the action that allow other
|
||||
// packages to add modules or connect packages.
|
||||
$this->progress(self::STATUS_INITIALIZING);
|
||||
|
||||
// This will change the status to "INITIALIZED" then fire an action when it is safe to
|
||||
// access the container, because from this moment on, container is locked from change.
|
||||
$this->progress(self::STATUS_INITIALIZED);
|
||||
} catch (\Throwable $throwable) {
|
||||
$this->handleFailure($throwable, self::ACTION_FAILED_BUILD);
|
||||
|
@ -386,37 +372,47 @@ class Package
|
|||
/**
|
||||
* @param Module ...$defaultModules Deprecated, use `addModule()` to add default modules.
|
||||
* @return bool
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function boot(Module ...$defaultModules): bool
|
||||
{
|
||||
try {
|
||||
// When package is done, nothing should happen to it calling boot again, but we call
|
||||
// false to signal something is off.
|
||||
if ($this->statusIs(self::STATUS_DONE)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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', '!=');
|
||||
// Make sure we call boot() on a non-failed instance, and also make a sanity check
|
||||
// on the status flow, e.g. prevent calling boot() from an action hook.
|
||||
$this->assertStatus(self::STATUS_INITIALIZED, 'boot application');
|
||||
|
||||
$this->progress(self::STATUS_MODULES_ADDED);
|
||||
// This will change status to STATUS_BOOTING "locking" subsequent call to `boot()`, but
|
||||
// no hook is fired here, because at this point we can not do anything more or less than
|
||||
// what can be done on the ACTION_INITIALIZED hook, so that hook is sufficient.
|
||||
$this->progress(self::STATUS_BOOTING);
|
||||
|
||||
$this->doExecute();
|
||||
|
||||
$this->progress(self::STATUS_READY);
|
||||
|
||||
do_action(
|
||||
$this->hookName(self::ACTION_READY),
|
||||
$this
|
||||
);
|
||||
// This will change status to STATUS_BOOTED and then fire an action that make it
|
||||
// possible to hook on a package that has finished its bootstrapping process, so all its
|
||||
// "executable" modules have been executed.
|
||||
$this->progress(self::STATUS_BOOTED);
|
||||
} catch (\Throwable $throwable) {
|
||||
$this->handleFailure($throwable, self::ACTION_FAILED_BOOT);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->progress(self::STATUS_BOOTED);
|
||||
// This will change the status to DONE and will not fire any action.
|
||||
// This is a status that proves that everything went well, not only the Package itself,
|
||||
// but also anything hooking Package's hooks.
|
||||
// The only way to move out of this status is a failure that might only happen directly
|
||||
// calling `addModule()`, `connect()` or `build()`.
|
||||
$this->progress(self::STATUS_DONE);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
@ -439,39 +435,67 @@ class Package
|
|||
);
|
||||
}
|
||||
|
||||
// We expect `boot()` to be called either:
|
||||
// 1. Directly after `addModule()`/`connect()`, without any `build()` call in between, so
|
||||
// status is IDLE and `$this->built` is `false`.
|
||||
// 2. After `build()` is called, so status is INITIALIZED and `$this->built` is `true`.
|
||||
// Any other usage is not allowed (e.g. calling `boot()` from an hook callback) and in that
|
||||
// case we return here, giving back control to `boot()` which will throw.
|
||||
$validFlows = (!$this->built && $this->statusIs(self::STATUS_IDLE))
|
||||
|| ($this->built && $this->statusIs(self::STATUS_INITIALIZED));
|
||||
|
||||
if (!$validFlows) {
|
||||
// If none of the two supported flows happened, we just return handling control back
|
||||
// to `boot()`, that will throw.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$this->built) {
|
||||
array_map([$this, 'addModule'], $defaultModules);
|
||||
// First valid flow: `boot()` was called directly after `addModule()`/`connect()`
|
||||
// without any call to `build()`. We can call `build()` and return, handing control
|
||||
// back to `boot()`. Before returning, if we had default modules passed to `boot()` we
|
||||
// already have fired a deprecation, so here we just add them dealing with back-compat.
|
||||
foreach ($defaultModules as $defaultModule) {
|
||||
$this->addModule($defaultModule);
|
||||
}
|
||||
$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.
|
||||
// Second valid flow: we have called `boot()` after `build()`. If we did it correctly,
|
||||
// without default modules passed to `boot()`, we can just return handing control back
|
||||
// to `boot()`.
|
||||
if (!$defaultModules) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If here, we have done something like: `$package->build()->boot($module1, $module2)`.
|
||||
// Passing modules to `boot()` was deprecated when `build()` was introduced, so whoever
|
||||
// added `build()` should have removed modules passed to `boot()`.
|
||||
// But we want to keep 100% backward compatibility so we still support this behavior
|
||||
// until the next major is released. To do that, we simulate IDLE status to prevent
|
||||
// `addModule()` from throwing when adding default modules.
|
||||
// But we can do that only if we don't have a compiled container yet.
|
||||
// If anything hooking ACTION_INIT called `container()` we have a compiled container
|
||||
// already, and we can't add modules, so we not going to simulate INIT status, which mean
|
||||
// the `$this->addModule()` call below will throw.
|
||||
$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;
|
||||
|
||||
if (!$this->hasContainer()) {
|
||||
$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 a module was already added via `addModule()` we can skip it, reducing the
|
||||
// chances of throwing an exception if not needed.
|
||||
if (!$this->moduleIs($defaultModule->id(), self::MODULE_ADDED)) {
|
||||
$this->addModule($defaultModule);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$this->status = $backup;
|
||||
if (!$this->hasFailed()) {
|
||||
$this->status = $backup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -482,7 +506,9 @@ class Package
|
|||
*/
|
||||
private function addModuleServices(Module $module, string $status): bool
|
||||
{
|
||||
/** @var null|array<string, Service|ExtendingService> $services */
|
||||
$services = null;
|
||||
/** @var null|callable(string, Service|ExtendingService): void $addCallback */
|
||||
$addCallback = null;
|
||||
switch ($status) {
|
||||
case self::MODULE_REGISTERED:
|
||||
|
@ -499,21 +525,16 @@ class Package
|
|||
break;
|
||||
}
|
||||
|
||||
if (!$services) {
|
||||
if (($services === null) || ($services === []) || ($addCallback === null)) {
|
||||
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 */
|
||||
foreach ($services as $id => $service) {
|
||||
$addCallback($id, $service);
|
||||
$ids[] = $id;
|
||||
}
|
||||
|
||||
$this->moduleProgress($module->id(), $status, $ids);
|
||||
|
||||
return true;
|
||||
|
@ -521,8 +542,6 @@ class Package
|
|||
|
||||
/**
|
||||
* @return void
|
||||
*
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function doExecute(): void
|
||||
{
|
||||
|
@ -530,9 +549,7 @@ class Package
|
|||
$success = $executable->run($this->container());
|
||||
$this->moduleProgress(
|
||||
$executable->id(),
|
||||
$success
|
||||
? self::MODULE_EXECUTED
|
||||
: self::MODULE_EXECUTION_FAILED
|
||||
$success ? self::MODULE_EXECUTED : self::MODULE_EXECUTION_FAILED,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -541,15 +558,20 @@ class Package
|
|||
* @param string $moduleId
|
||||
* @param string $status
|
||||
* @param list<string>|null $serviceIds
|
||||
*
|
||||
* @return void
|
||||
* @return void
|
||||
*/
|
||||
private function moduleProgress(string $moduleId, string $status, ?array $serviceIds = null)
|
||||
{
|
||||
isset($this->moduleStatus[$status]) or $this->moduleStatus[$status] = [];
|
||||
private function moduleProgress(
|
||||
string $moduleId,
|
||||
string $status,
|
||||
?array $serviceIds = null
|
||||
): void {
|
||||
|
||||
if (!isset($this->moduleStatus[$status])) {
|
||||
$this->moduleStatus[$status] = [];
|
||||
}
|
||||
$this->moduleStatus[$status][] = $moduleId;
|
||||
|
||||
if (!$serviceIds || !$this->properties->isDebug()) {
|
||||
if (($serviceIds === null) || ($serviceIds === []) || !$this->properties->isDebug()) {
|
||||
$this->moduleStatus[self::MODULES_ALL][] = "{$moduleId} {$status}";
|
||||
|
||||
return;
|
||||
|
@ -605,10 +627,9 @@ class Package
|
|||
* `inpsyde.modularity.my-plugin` anyway, so the file name is not relevant.
|
||||
*
|
||||
* @param string $suffix
|
||||
*
|
||||
* @return string
|
||||
* @see Package::name()
|
||||
*
|
||||
* @see Package::name()
|
||||
*/
|
||||
public function hookName(string $suffix = ''): string
|
||||
{
|
||||
|
@ -631,8 +652,6 @@ class Package
|
|||
|
||||
/**
|
||||
* @return ContainerInterface
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function container(): ContainerInterface
|
||||
{
|
||||
|
@ -642,6 +661,14 @@ class Package
|
|||
return $this->containerConfigurator->createReadOnlyContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasContainer(): bool
|
||||
{
|
||||
return $this->hasContainer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
|
@ -652,27 +679,94 @@ class Package
|
|||
|
||||
/**
|
||||
* @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;
|
||||
return $this->checkStatus($status);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function hasFailed(): bool
|
||||
{
|
||||
return $this->status === self::STATUS_FAILED;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $status
|
||||
* @return bool
|
||||
*/
|
||||
public function hasReachedStatus(int $status): bool
|
||||
{
|
||||
if ($this->hasFailed()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return isset(self::SUCCESS_STATUSES[$status]) && $this->checkStatus($status, '>=');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $status
|
||||
* @param value-of<Package::OPERATORS> $operator
|
||||
* @return bool
|
||||
*/
|
||||
private function checkStatus(int $status, string $operator = '=='): bool
|
||||
{
|
||||
assert(isset(self::OPERATORS[$operator]));
|
||||
|
||||
return version_compare((string) $this->status, (string) $status, $operator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Package::STATUS_* $status
|
||||
*/
|
||||
private function progress(int $status): void
|
||||
{
|
||||
$this->status = $status;
|
||||
|
||||
[$packageHookSuffix, $globalHook] = self::STATUSES_ACTIONS_MAP[$status] ?? [null, null];
|
||||
if ($packageHookSuffix !== null) {
|
||||
do_action($this->hookName($packageHookSuffix), $this);
|
||||
}
|
||||
if ($globalHook !== null) {
|
||||
do_action($globalHook, $this->name(), $this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $packageName
|
||||
* @param string $reason
|
||||
* @param bool $throw
|
||||
* @return bool
|
||||
*/
|
||||
private function handleConnectionFailure(string $packageName, string $reason, bool $throw): bool
|
||||
{
|
||||
$errorData = ['package' => $packageName, 'status' => $this->status];
|
||||
$message = "Failed connecting package {$packageName} because {$reason}.";
|
||||
|
||||
do_action(
|
||||
$this->hookName(self::ACTION_FAILED_CONNECT),
|
||||
$packageName,
|
||||
new \WP_Error('failed_connection', $message, $errorData)
|
||||
);
|
||||
|
||||
if ($throw) {
|
||||
throw new \Exception(
|
||||
esc_html($message),
|
||||
0,
|
||||
$this->lastError // phpcs:ignore WordPress.Security.EscapeOutput
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \Throwable $throwable
|
||||
* @param Package::ACTION_FAILED_* $action
|
||||
* @return void
|
||||
* @throws \Throwable
|
||||
*/
|
||||
private function handleFailure(\Throwable $throwable, string $action): void
|
||||
{
|
||||
|
@ -690,18 +784,15 @@ class Package
|
|||
/**
|
||||
* @param int $status
|
||||
* @param string $action
|
||||
* @param string $operator
|
||||
*
|
||||
* @throws \Exception
|
||||
* @psalm-suppress ArgumentTypeCoercion
|
||||
* @param value-of<Package::OPERATORS> $operator
|
||||
*/
|
||||
private function assertStatus(int $status, string $action, string $operator = '=='): void
|
||||
{
|
||||
if (!version_compare((string) $this->status, (string) $status, $operator)) {
|
||||
if (!$this->checkStatus($status, $operator)) {
|
||||
throw new \Exception(
|
||||
sprintf("Can't %s at this point of application.", $action),
|
||||
sprintf("Can't %s at this point of application.", esc_html($action)),
|
||||
0,
|
||||
$this->lastError
|
||||
$this->lastError // phpcs:ignore WordPress.Security.EscapeOutput
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -713,7 +804,6 @@ class Package
|
|||
* @param string $message
|
||||
* @param string $function
|
||||
* @param string $version
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function deprecatedArgument(string $message, string $function, string $version): void
|
||||
|
@ -721,7 +811,9 @@ class Package
|
|||
do_action('deprecated_argument_run', $function, $message, $version);
|
||||
|
||||
if (apply_filters('deprecated_argument_trigger_error', true)) {
|
||||
trigger_error($message, \E_USER_DEPRECATED);
|
||||
do_action('wp_trigger_error_run', $function, $message, \E_USER_DEPRECATED);
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
|
||||
trigger_error(esc_html($message), \E_USER_DEPRECATED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,47 +6,30 @@ 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;
|
||||
protected ?bool $isDebug = null;
|
||||
protected string $baseName;
|
||||
protected string $basePath;
|
||||
protected ?string $baseUrl;
|
||||
/** @var array<string, mixed> */
|
||||
protected array $properties;
|
||||
|
||||
/**
|
||||
* @param string $baseName
|
||||
* @param string $basePath
|
||||
* @param string|null $baseUrl
|
||||
* @param array $properties
|
||||
* @param array<string, mixed> $properties
|
||||
*/
|
||||
protected function __construct(
|
||||
string $baseName,
|
||||
string $basePath,
|
||||
string $baseUrl = null,
|
||||
?string $baseUrl = null,
|
||||
array $properties = []
|
||||
) {
|
||||
|
||||
$baseName = $this->sanitizeBaseName($baseName);
|
||||
$basePath = (string) trailingslashit($basePath);
|
||||
if ($baseUrl) {
|
||||
$baseUrl = (string) trailingslashit($baseUrl);
|
||||
$basePath = trailingslashit($basePath);
|
||||
if ($baseUrl !== null) {
|
||||
$baseUrl = trailingslashit($baseUrl);
|
||||
}
|
||||
|
||||
$this->baseName = $baseName;
|
||||
|
@ -58,11 +41,13 @@ class BaseProperties implements Properties
|
|||
/**
|
||||
* @param string $name
|
||||
*
|
||||
* @return string
|
||||
* @return lowercase-string
|
||||
*/
|
||||
protected function sanitizeBaseName(string $name): string
|
||||
{
|
||||
substr_count($name, '/') and $name = dirname($name);
|
||||
if (substr_count($name, '/')) {
|
||||
$name = dirname($name);
|
||||
}
|
||||
|
||||
return strtolower(pathinfo($name, PATHINFO_FILENAME));
|
||||
}
|
||||
|
@ -162,7 +147,9 @@ class BaseProperties implements Properties
|
|||
{
|
||||
$value = $this->get(self::PROP_REQUIRES_WP);
|
||||
|
||||
return $value && is_string($value) ? $value : null;
|
||||
return (($value !== '') && is_string($value))
|
||||
? $value
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -172,11 +159,13 @@ class BaseProperties implements Properties
|
|||
{
|
||||
$value = $this->get(self::PROP_REQUIRES_PHP);
|
||||
|
||||
return $value && is_string($value) ? $value : null;
|
||||
return (($value !== '') && is_string($value))
|
||||
? $value
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
* @return string[]
|
||||
*/
|
||||
public function tags(): array
|
||||
{
|
||||
|
@ -185,7 +174,8 @@ class BaseProperties implements Properties
|
|||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param null $default
|
||||
* @param mixed $default
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key, $default = null)
|
||||
|
@ -195,6 +185,7 @@ class BaseProperties implements Properties
|
|||
|
||||
/**
|
||||
* @param string $key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $key): bool
|
||||
|
|
|
@ -5,17 +5,26 @@ declare(strict_types=1);
|
|||
namespace WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties;
|
||||
|
||||
/**
|
||||
* Class LibraryProperties
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Properties
|
||||
* @phpstan-type ComposerAuthor array{
|
||||
* name: string,
|
||||
* email?: string,
|
||||
* homepage?: string,
|
||||
* role?: string,
|
||||
* }
|
||||
* @phpstan-type ComposerData array{
|
||||
* name: string,
|
||||
* version?: string,
|
||||
* require?: array<string, string>,
|
||||
* require-dev?: array<string, string>,
|
||||
* description?: string,
|
||||
* keywords?: string[],
|
||||
* authors?: ComposerAuthor[],
|
||||
* extra?: array{modularity?: array<string, string>},
|
||||
* }
|
||||
*/
|
||||
class LibraryProperties extends BaseProperties
|
||||
{
|
||||
/**
|
||||
* Allowed configuration in composer.json "extra.modularity".
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
/** Allowed configuration in composer.json "extra.modularity" */
|
||||
public const EXTRA_KEYS = [
|
||||
self::PROP_DOMAIN_PATH,
|
||||
self::PROP_NAME,
|
||||
|
@ -31,32 +40,33 @@ class LibraryProperties extends BaseProperties
|
|||
*
|
||||
* @return LibraryProperties
|
||||
*
|
||||
* @throws \Exception
|
||||
* @psalm-suppress MixedArrayAccess
|
||||
* phpcs:disable SlevomatCodingStandard.Complexity
|
||||
*/
|
||||
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.");
|
||||
}
|
||||
// phpcs:enable SlevomatCodingStandard.Complexity
|
||||
|
||||
$content = (string) file_get_contents($composerJsonFile);
|
||||
/** @var array $composerJsonData */
|
||||
$composerJsonData = json_decode($content, true);
|
||||
$composerJsonData = self::readComposerJsonData($composerJsonFile);
|
||||
|
||||
$properties = Properties::DEFAULT_PROPERTIES;
|
||||
$properties[self::PROP_DESCRIPTION] = $composerJsonData['description'] ?? '';
|
||||
$properties[self::PROP_TAGS] = $composerJsonData['keywords'] ?? [];
|
||||
|
||||
$authors = $composerJsonData['authors'] ?? [];
|
||||
if (!is_array($authors)) {
|
||||
$authors = [];
|
||||
}
|
||||
$names = [];
|
||||
foreach ((array) $authors as $author) {
|
||||
$name = $author['name'] ?? null;
|
||||
if ($name && is_string($name)) {
|
||||
foreach ($authors as $author) {
|
||||
if (!is_array($author)) {
|
||||
continue;
|
||||
}
|
||||
$name = $author['name'] ?? '';
|
||||
if (($name !== '') && is_string($name)) {
|
||||
$names[] = $name;
|
||||
}
|
||||
$url = $author['homepage'] ?? null;
|
||||
if ($url && !$properties['authorUri'] && is_string($url)) {
|
||||
$url = $author['homepage'] ?? '';
|
||||
if (($url !== '') && ($properties[self::PROP_AUTHOR_URI] === '') && is_string($url)) {
|
||||
$properties[self::PROP_AUTHOR_URI] = $url;
|
||||
}
|
||||
}
|
||||
|
@ -66,6 +76,9 @@ class LibraryProperties extends BaseProperties
|
|||
|
||||
// Custom settings which can be stored in composer.json "extra.modularity"
|
||||
$extra = $composerJsonData['extra']['modularity'] ?? [];
|
||||
if (!is_array($extra)) {
|
||||
$extra = [];
|
||||
}
|
||||
foreach (self::EXTRA_KEYS as $key) {
|
||||
$properties[$key] = $extra[$key] ?? '';
|
||||
}
|
||||
|
@ -74,39 +87,50 @@ class LibraryProperties extends BaseProperties
|
|||
$properties[self::PROP_REQUIRES_PHP] = self::extractPhpVersion($composerJsonData);
|
||||
|
||||
// composer.json might have "version" in root
|
||||
$version = $composerJsonData['version'] ?? null;
|
||||
if ($version && is_string($version)) {
|
||||
$version = $composerJsonData['version'] ?? '';
|
||||
if (($version !== '') && is_string($version)) {
|
||||
$properties[self::PROP_VERSION] = $version;
|
||||
}
|
||||
|
||||
[$baseName, $name] = static::buildNames($composerJsonData);
|
||||
$basePath = dirname($composerJsonFile);
|
||||
if (empty($properties[self::PROP_NAME])) {
|
||||
if (($properties[self::PROP_NAME] === '') || !is_string($properties[self::PROP_NAME])) {
|
||||
$properties[self::PROP_NAME] = $name;
|
||||
}
|
||||
|
||||
return new self(
|
||||
$baseName,
|
||||
$basePath,
|
||||
$baseUrl,
|
||||
$properties
|
||||
);
|
||||
return new self($baseName, $basePath, $baseUrl, $properties);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $composerJsonData
|
||||
* @param string $url
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ComposerData $composerJsonData
|
||||
*
|
||||
* @return array{string, string}
|
||||
*/
|
||||
private static function buildNames(array $composerJsonData): array
|
||||
protected 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"
|
||||
// From "syde/foo-bar-baz" to "Syde Foo Bar Baz"
|
||||
$name = mb_convert_case(
|
||||
str_replace(['-', '_', '.'], ' ', implode(' ', $packageNamePieces)),
|
||||
MB_CASE_TITLE
|
||||
MB_CASE_TITLE,
|
||||
);
|
||||
|
||||
return [$basename, $name];
|
||||
|
@ -122,88 +146,111 @@ class LibraryProperties extends BaseProperties
|
|||
* `5.6 || >= 7.1` returns `5.6`
|
||||
* `>= 7.1 < 8` returns `7.1`
|
||||
*
|
||||
* @param array $composerData
|
||||
* @param ComposerData $composerData
|
||||
* @param string $key
|
||||
*
|
||||
* @return string|null
|
||||
* @return string
|
||||
*/
|
||||
private static function extractPhpVersion(array $composerData, string $key = 'require'): ?string
|
||||
{
|
||||
protected 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))
|
||||
$base = $composerData[$key] ?? null;
|
||||
$requirement = is_array($base)
|
||||
? ($base['php'] ?? '')
|
||||
: '';
|
||||
$version = (($requirement !== '') && is_string($requirement))
|
||||
? trim($requirement)
|
||||
: null;
|
||||
if (!$version) {
|
||||
return $nextKey
|
||||
: '';
|
||||
if ($version === '') {
|
||||
return ($nextKey !== null)
|
||||
? 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);
|
||||
/** @var non-empty-string|null $found */
|
||||
$found = null;
|
||||
foreach ($alternatives as $alternative) {
|
||||
/** @var callable(string):?string $matcher */
|
||||
$itemFound = $matcher($alternative);
|
||||
if ($itemFound && (!$found || version_compare($itemFound, $found, '<'))) {
|
||||
$itemFound = static::parseVersion($alternative);
|
||||
if (
|
||||
($itemFound !== '')
|
||||
&& (($found === null) || version_compare($itemFound, $found, '<'))
|
||||
) {
|
||||
$found = $itemFound;
|
||||
}
|
||||
}
|
||||
|
||||
if ($found) {
|
||||
if ($found !== null) {
|
||||
return $found;
|
||||
}
|
||||
|
||||
return $nextKey
|
||||
return ($nextKey !== null)
|
||||
? static::extractPhpVersion($composerData, $nextKey)
|
||||
: null;
|
||||
: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @param string $version
|
||||
*
|
||||
* @return static
|
||||
*
|
||||
* @throws \Exception
|
||||
* @return string
|
||||
*/
|
||||
public function withBaseUrl(string $url): LibraryProperties
|
||||
protected static function parseVersion(string $version): string
|
||||
{
|
||||
if ($this->baseUrl !== null) {
|
||||
throw new \Exception(sprintf('%s::$baseUrl property is not overridable.', __CLASS__));
|
||||
$version = trim($version);
|
||||
if ($version === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$this->baseUrl = trailingslashit($url);
|
||||
// versions range like `>= 7.2.4 < 8`
|
||||
if (preg_match('{>=?([\s0-9\.]+)<}', $version, $matches)) {
|
||||
return trim($matches[1], " \t\n\r\0\x0B.");
|
||||
}
|
||||
|
||||
return $this;
|
||||
// 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 '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $composerJsonFile
|
||||
*
|
||||
* @return ComposerData
|
||||
* @throws \Exception
|
||||
*/
|
||||
private static function readComposerJsonData(string $composerJsonFile): array
|
||||
{
|
||||
if (!\is_file($composerJsonFile) || !\is_readable($composerJsonFile)) {
|
||||
throw new \Exception(
|
||||
esc_html("File {$composerJsonFile} does not exist or is not readable."),
|
||||
);
|
||||
}
|
||||
|
||||
$content = (string) file_get_contents($composerJsonFile);
|
||||
|
||||
/** @var ComposerData $composerJsonData */
|
||||
$composerJsonData = json_decode($content, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new \Exception(
|
||||
esc_html("Error reading file {$composerJsonFile}: " . json_last_error_msg()),
|
||||
);
|
||||
}
|
||||
|
||||
return $composerJsonData;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,24 +4,14 @@ 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.
|
||||
*/
|
||||
// Custom properties for Plugins
|
||||
public const PROP_NETWORK = 'network';
|
||||
public const PROP_REQUIRES_PLUGINS = 'requiresPlugins';
|
||||
|
||||
/**
|
||||
* Available methods of Properties::__call()
|
||||
* from plugin headers.
|
||||
*
|
||||
* @link https://developer.wordpress.org/reference/functions/get_plugin_data/
|
||||
* @see https://developer.wordpress.org/reference/functions/get_plugin_data/
|
||||
*/
|
||||
protected const HEADERS = [
|
||||
self::PROP_AUTHOR => 'Author',
|
||||
|
@ -37,36 +27,17 @@ class PluginProperties extends BaseProperties
|
|||
|
||||
// additional headers
|
||||
self::PROP_NETWORK => 'Network',
|
||||
self::PROP_REQUIRES_PLUGINS => 'RequiresPlugins',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $pluginMainFile;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $pluginBaseName;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $isMu;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $isActive;
|
||||
|
||||
/**
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $isNetworkActive;
|
||||
private string $pluginMainFile;
|
||||
private string $pluginBaseName;
|
||||
protected ?bool $isMu = null;
|
||||
protected ?bool $isActive = null;
|
||||
protected ?bool $isNetworkActive = null;
|
||||
|
||||
/**
|
||||
* @param string $pluginMainFile
|
||||
*
|
||||
* @return PluginProperties
|
||||
*/
|
||||
public static function new(string $pluginMainFile): PluginProperties
|
||||
|
@ -75,8 +46,6 @@ class PluginProperties extends BaseProperties
|
|||
}
|
||||
|
||||
/**
|
||||
* PluginProperties constructor.
|
||||
*
|
||||
* @param string $pluginMainFile
|
||||
*/
|
||||
protected function __construct(string $pluginMainFile)
|
||||
|
@ -85,7 +54,11 @@ class PluginProperties extends BaseProperties
|
|||
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||
}
|
||||
|
||||
$pluginData = get_plugin_data($pluginMainFile);
|
||||
// $markup = false, to avoid an incorrect early wptexturize call.
|
||||
// $translate = false, to avoid loading translations too early
|
||||
// @see https://core.trac.wordpress.org/ticket/49965
|
||||
// @see https://core.trac.wordpress.org/ticket/34114
|
||||
$pluginData = (array) get_plugin_data($pluginMainFile, false, false);
|
||||
$properties = Properties::DEFAULT_PROPERTIES;
|
||||
|
||||
// Map pluginData to internal structure.
|
||||
|
@ -93,6 +66,7 @@ class PluginProperties extends BaseProperties
|
|||
$properties[$key] = $pluginData[$pluginDataKey] ?? '';
|
||||
unset($pluginData[$pluginDataKey]);
|
||||
}
|
||||
/** @var array<string, mixed> $properties */
|
||||
$properties = array_merge($properties, $pluginData);
|
||||
|
||||
$this->pluginMainFile = wp_normalize_path($pluginMainFile);
|
||||
|
@ -119,14 +93,22 @@ class PluginProperties extends BaseProperties
|
|||
|
||||
/**
|
||||
* @return bool
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
public function network(): bool
|
||||
{
|
||||
return (bool) $this->get(self::PROP_NETWORK, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function requiresPlugins(): array
|
||||
{
|
||||
$value = $this->get(self::PROP_REQUIRES_PLUGINS);
|
||||
|
||||
return $value && is_string($value) ? explode(',', $value) : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
|
@ -163,10 +145,6 @@ class PluginProperties extends BaseProperties
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -17,9 +17,7 @@ interface Properties
|
|||
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 => '',
|
||||
|
@ -36,15 +34,13 @@ interface Properties
|
|||
|
||||
/**
|
||||
* @param string $key
|
||||
* @param null $default
|
||||
*
|
||||
* @param mixed $default
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(string $key, $default = null);
|
||||
|
||||
/**
|
||||
* @param string $key
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function has(string $key): bool;
|
||||
|
@ -103,6 +99,7 @@ interface Properties
|
|||
|
||||
/**
|
||||
* The home page of the plugin, theme or library.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function uri(): string;
|
||||
|
@ -122,7 +119,7 @@ interface Properties
|
|||
/**
|
||||
* Optional. Specify the minimum required PHP version.
|
||||
*
|
||||
* @return string
|
||||
* @return string|null
|
||||
*/
|
||||
public function requiresPhp(): ?string;
|
||||
|
||||
|
@ -130,10 +127,10 @@ interface Properties
|
|||
* 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 string[]
|
||||
*
|
||||
* @return array
|
||||
* @see https://developer.wordpress.org/reference/classes/wp_theme/#properties
|
||||
* @see https://getcomposer.org/doc/04-schema.md#keywords
|
||||
*/
|
||||
public function tags(): array;
|
||||
}
|
||||
|
|
|
@ -4,26 +4,12 @@ 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/
|
||||
*/
|
||||
|
||||
/** @see https://developer.wordpress.org/reference/classes/wp_theme/ */
|
||||
protected const HEADERS = [
|
||||
self::PROP_AUTHOR => 'Author',
|
||||
self::PROP_AUTHOR_URI => 'AuthorURI',
|
||||
|
@ -53,8 +39,6 @@ class ThemeProperties extends BaseProperties
|
|||
}
|
||||
|
||||
/**
|
||||
* ThemeProperties constructor.
|
||||
*
|
||||
* @param string $themeDirectory
|
||||
*/
|
||||
protected function __construct(string $themeDirectory)
|
||||
|
@ -67,13 +51,15 @@ class ThemeProperties extends BaseProperties
|
|||
$properties = Properties::DEFAULT_PROPERTIES;
|
||||
|
||||
foreach (self::HEADERS as $key => $themeKey) {
|
||||
/** @psalm-suppress DocblockTypeContradiction */
|
||||
$properties[$key] = $theme->get($themeKey) ?? '';
|
||||
$property = $theme->get($themeKey);
|
||||
if (is_string($property) || is_array($property)) {
|
||||
$properties[$key] = $property;
|
||||
}
|
||||
}
|
||||
|
||||
$baseName = $theme->get_stylesheet();
|
||||
$basePath = $theme->get_stylesheet_directory();
|
||||
$baseUrl = (string) trailingslashit($theme->get_stylesheet_directory_uri());
|
||||
$baseUrl = trailingslashit($theme->get_stylesheet_directory_uri());
|
||||
|
||||
parent::__construct(
|
||||
$baseName,
|
||||
|
@ -84,8 +70,6 @@ class ThemeProperties extends BaseProperties
|
|||
}
|
||||
|
||||
/**
|
||||
* If the theme is published.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function status(): string
|
||||
|
@ -93,6 +77,9 @@ class ThemeProperties extends BaseProperties
|
|||
return (string) $this->get(self::PROP_STATUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function template(): string
|
||||
{
|
||||
return (string) $this->get(self::PROP_TEMPLATE);
|
||||
|
@ -120,7 +107,7 @@ class ThemeProperties extends BaseProperties
|
|||
public function parentThemeProperties(): ?ThemeProperties
|
||||
{
|
||||
$template = $this->template();
|
||||
if (!$template) {
|
||||
if ($template === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Vendor\Psr\Container;
|
||||
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Base interface representing a generic exception in a container.
|
||||
*/
|
||||
interface ContainerExceptionInterface
|
||||
interface ContainerExceptionInterface extends Throwable
|
||||
{
|
||||
}
|
||||
|
|
|
@ -18,14 +18,7 @@ use WooCommerce\PayPalCommerce\AdminNotices\Endpoint\MuteMessageEndpoint;
|
|||
|
||||
return array(
|
||||
'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'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-admin-notices/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'admin-notices.renderer' => static function ( ContainerInterface $container ): RendererInterface {
|
||||
return new Renderer(
|
||||
|
|
|
@ -334,12 +334,22 @@ class ApplePayButton extends PaymentButton {
|
|||
this.checkEligibility();
|
||||
}
|
||||
|
||||
reinit() {
|
||||
async reinit() {
|
||||
// Missing (invalid) configuration indicates, that the first `init()` call did not happen yet.
|
||||
if ( ! this.validateConfiguration( true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensures transaction info is updated when cart or checkout update events are triggered.
|
||||
await this.contextHandler
|
||||
.transactionInfo()
|
||||
.then( ( transactionInfo ) => {
|
||||
this.transactionInfo = transactionInfo;
|
||||
} )
|
||||
.catch( ( error ) => {
|
||||
console.error( 'Failed to get transaction info:', error );
|
||||
} );
|
||||
|
||||
super.reinit();
|
||||
|
||||
this.init();
|
||||
|
|
|
@ -105,7 +105,8 @@ registerExpressPaymentMethod( {
|
|||
content: <ApplePayComponent isEditing={ false } />,
|
||||
edit: <ApplePayComponent isEditing={ true } />,
|
||||
ariaLabel: buttonData.title,
|
||||
canMakePayment: () => buttonData.enabled,
|
||||
canMakePayment: () =>
|
||||
buttonData.enabled && window.ApplePaySession?.canMakePayments(),
|
||||
supports: {
|
||||
features,
|
||||
style: [ 'height', 'borderRadius' ],
|
||||
|
|
|
@ -136,14 +136,7 @@ return array(
|
|||
return false;
|
||||
},
|
||||
'applepay.url' => static function ( ContainerInterface $container ): string {
|
||||
$path = realpath( __FILE__ );
|
||||
if ( false === $path ) {
|
||||
return '';
|
||||
}
|
||||
return plugins_url(
|
||||
'/modules/ppcp-applepay/',
|
||||
dirname( $path, 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-applepay/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'applepay.sdk_script_url' => static function ( ContainerInterface $container ): string {
|
||||
return 'https://applepay.cdn-apple.com/jsapi/v1/apple-pay-sdk.js';
|
||||
|
|
|
@ -121,7 +121,7 @@ export const populateWooFields = (
|
|||
address_1: address.addressLine1,
|
||||
address_2: address.addressLine2 || '',
|
||||
city: address.adminArea2,
|
||||
state: address.adminArea1,
|
||||
state: address.adminArea1 || '',
|
||||
postcode: address.postalCode,
|
||||
country: address.countryCode,
|
||||
phone: phoneNumber.nationalNumber,
|
||||
|
|
|
@ -27,8 +27,38 @@ const usePayPalScript = ( namespace, ppcpConfig, isConfigLoaded ) => {
|
|||
useEffect( () => {
|
||||
const loadScript = async () => {
|
||||
if ( ! isPayPalLoaded && isConfigLoaded ) {
|
||||
const axoConfig = window.wc_ppcp_axo;
|
||||
|
||||
try {
|
||||
await loadPayPalScript( namespace, ppcpConfig );
|
||||
const res = await fetch(
|
||||
axoConfig.ajax.axo_script_attributes.endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( {
|
||||
nonce: axoConfig.ajax.axo_script_attributes
|
||||
.nonce,
|
||||
} ),
|
||||
}
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
if ( ! json.success ) {
|
||||
log(
|
||||
`Failed to load axo script attributes: ${ json.data.message }`,
|
||||
'error'
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await loadPayPalScript( namespace, {
|
||||
...ppcpConfig,
|
||||
script_attributes: {
|
||||
...ppcpConfig.script_attributes,
|
||||
'data-sdk-client-token': json.data.sdk_client_token,
|
||||
},
|
||||
} );
|
||||
setIsPayPalLoaded( true );
|
||||
} catch ( error ) {
|
||||
log(
|
||||
|
|
|
@ -32,7 +32,7 @@ export const useShippingAddressChange = ( fastlaneSdk, setShippingAddress ) => {
|
|||
address_1: address.addressLine1,
|
||||
address_2: address.addressLine2 || '',
|
||||
city: address.adminArea2,
|
||||
state: address.adminArea1,
|
||||
state: address.adminArea1 || '',
|
||||
postcode: address.postalCode,
|
||||
country: address.countryCode,
|
||||
phone: phoneNumber.nationalNumber,
|
||||
|
|
|
@ -18,15 +18,7 @@ return array(
|
|||
return true;
|
||||
},
|
||||
'axoblock.url' => static function ( ContainerInterface $container ) : string {
|
||||
/**
|
||||
* The path cannot be false.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-axo-block/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-axo-block/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'axoblock.method' => static function ( ContainerInterface $container ) : AxoBlockPaymentMethod {
|
||||
return new AxoBlockPaymentMethod(
|
||||
|
|
|
@ -67,24 +67,6 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
add_action(
|
||||
'wp_loaded',
|
||||
function () use ( $c ) {
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_localized_script_data',
|
||||
function( array $localized_script_data ) use ( $c ) {
|
||||
if ( ! $c->has( 'axo.available' ) || ! $c->get( 'axo.available' ) ) {
|
||||
return $localized_script_data;
|
||||
}
|
||||
|
||||
$module = $this;
|
||||
$api = $c->get( 'api.sdk-client-token' );
|
||||
assert( $api instanceof SdkClientToken );
|
||||
|
||||
$logger = $c->get( 'woocommerce.logger.woocommerce' );
|
||||
assert( $logger instanceof LoggerInterface );
|
||||
|
||||
return $module->add_sdk_client_token_to_script_data( $api, $logger, $localized_script_data );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Param types removed to avoid third-party issues.
|
||||
*
|
||||
|
@ -146,37 +128,6 @@ class AxoBlockModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds id token to localized script data.
|
||||
*
|
||||
* @param SdkClientToken $api User id token api.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param array $localized_script_data The localized script data.
|
||||
* @return array
|
||||
*/
|
||||
private function add_sdk_client_token_to_script_data(
|
||||
SdkClientToken $api,
|
||||
LoggerInterface $logger,
|
||||
array $localized_script_data
|
||||
): array {
|
||||
try {
|
||||
$sdk_client_token = $api->sdk_client_token();
|
||||
$localized_script_data['axo'] = array(
|
||||
'sdk_client_token' => $sdk_client_token,
|
||||
);
|
||||
|
||||
} catch ( RuntimeException $exception ) {
|
||||
$error = $exception->getMessage();
|
||||
if ( is_a( $exception, PayPalApiException::class ) ) {
|
||||
$error = $exception->get_details( $error );
|
||||
}
|
||||
|
||||
$logger->error( $error );
|
||||
}
|
||||
|
||||
return $localized_script_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueues PayPal Insights analytics script for the Checkout block.
|
||||
*
|
||||
|
|
|
@ -11,7 +11,8 @@ namespace WooCommerce\PayPalCommerce\AxoBlock;
|
|||
|
||||
use WC_Payment_Gateway;
|
||||
use Automattic\WooCommerce\Blocks\Payments\Integrations\AbstractPaymentMethodType;
|
||||
use WooCommerce\PayPalCommerce\Axo\FrontendLoggerEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Axo\Endpoint\AxoScriptAttributes;
|
||||
use WooCommerce\PayPalCommerce\Axo\Endpoint\FrontendLogger;
|
||||
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
|
||||
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
|
||||
|
@ -257,9 +258,13 @@ class AxoBlockPaymentMethod extends AbstractPaymentMethodType {
|
|||
'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/',
|
||||
'module_url' => untrailingslashit( $this->module_url ),
|
||||
'ajax' => array(
|
||||
'frontend_logger' => array(
|
||||
'endpoint' => \WC_AJAX::get_endpoint( FrontendLoggerEndpoint::ENDPOINT ),
|
||||
'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ),
|
||||
'frontend_logger' => array(
|
||||
'endpoint' => \WC_AJAX::get_endpoint( FrontendLogger::ENDPOINT ),
|
||||
'nonce' => wp_create_nonce( FrontendLogger::nonce() ),
|
||||
),
|
||||
'axo_script_attributes' => array(
|
||||
'endpoint' => \WC_AJAX::get_endpoint( AxoScriptAttributes::ENDPOINT ),
|
||||
'nonce' => wp_create_nonce( AxoScriptAttributes::nonce() ),
|
||||
),
|
||||
),
|
||||
'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '',
|
||||
|
|
863
modules/ppcp-axo/docs/payment-test.html
Normal file
863
modules/ppcp-axo/docs/payment-test.html
Normal file
|
@ -0,0 +1,863 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<!--
|
||||
URL: /wp-content/plugins/woocommerce-paypal-payments/modules/ppcp-axo/docs/payment-test.html
|
||||
-->
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1.0'>
|
||||
<title>PayPal Fastlane Checkout - Minimal POC</title>
|
||||
</head>
|
||||
<body>
|
||||
<section>
|
||||
<h3>PayPal Fastlane Test</h3>
|
||||
<div id='test-form'>
|
||||
<p>
|
||||
PayPal client ID: <span id='client-id'></span>
|
||||
<button id='clear-credentials'>Clear</button>
|
||||
</p>
|
||||
<div id='fastlane-container'>
|
||||
<div class='form-group'>
|
||||
<label for='sca-method'>3DS Verification:</label>
|
||||
<select id='sca-method' style='width: 100%;'>
|
||||
<option value=''>NEVER - Do not ask for 3DS verification</option>
|
||||
<option value='SCA_WHEN_REQUIRED'>WHEN REQUIRED - Let PayPal decide if to show
|
||||
3DS
|
||||
</option>
|
||||
<option value='SCA_ALWAYS'>ALWAYS - Ask for 3DS verification</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class='form-group'>
|
||||
<label for='email'>Email Address</label>
|
||||
<input type='email' id='email' placeholder='Enter your email address'>
|
||||
</div>
|
||||
|
||||
<button id='lookupButton'>Continue</button>
|
||||
|
||||
<!-- Fastlane Watermark will be rendered here -->
|
||||
<div id='fastlaneWatermark'></div>
|
||||
|
||||
<!-- Shipping Address Container -->
|
||||
<div id='shippingAddressContainer' style='display: none;'>
|
||||
<h3>Shipping Address</h3>
|
||||
<div id='shippingAddressDetails'></div>
|
||||
<button id='changeShippingButton' style='display: none;'>Change Address</button>
|
||||
</div>
|
||||
|
||||
<!-- Payment Method Container -->
|
||||
<div id='paymentMethodContainer' style='display: none;'>
|
||||
<h3>Payment Method</h3>
|
||||
<div id='paymentMethodDetails'></div>
|
||||
<button id='changePaymentButton' style='display: none;'>Change Payment Method
|
||||
</button>
|
||||
|
||||
<!-- Fastlane Card Component will be rendered here for guests or members without cards -->
|
||||
<div id='fastlaneCardComponent' style='display: none;'></div>
|
||||
</div>
|
||||
|
||||
<!-- Checkout Container -->
|
||||
<div id='checkout-container' style='display: none;'>
|
||||
<h3>Order Summary</h3>
|
||||
<p>Product: Test Item</p>
|
||||
<p>Price: $10.00</p>
|
||||
<button id='placeOrderButton'>Place Order</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='logger' id='response-log'></div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
// Check if credentials exist in localStorage
|
||||
const PAYPAL_CLIENT_ID = localStorage.getItem('PAYPAL_CLIENT_ID');
|
||||
const PAYPAL_CLIENT_SECRET = localStorage.getItem('PAYPAL_CLIENT_SECRET');
|
||||
const SCA_METHOD = localStorage.getItem('SCA_METHOD') || '';
|
||||
|
||||
// Variables to store the Fastlane instance and other data
|
||||
let fastlane;
|
||||
let identity;
|
||||
let customerContextId;
|
||||
let renderFastlaneMemberExperience = false;
|
||||
let profileData = null;
|
||||
let sdkLoaded = false;
|
||||
|
||||
// If credentials don't exist, show input form
|
||||
if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) {
|
||||
document.getElementById('test-form').innerHTML = `
|
||||
<div id='credentials-form'>
|
||||
<h4>Enter PayPal API Credentials</h4>
|
||||
<div style='margin-bottom: 10px;'>
|
||||
<label for='client-id-input'>Client ID:</label>
|
||||
<input type='text' id='client-id-input' style='width: 100%;'>
|
||||
</div>
|
||||
<div style='margin-bottom: 10px;'>
|
||||
<label for='client-secret'>Client Secret:</label>
|
||||
<input type='text' id='client-secret' style='width: 100%;'>
|
||||
</div>
|
||||
<button id='save-credentials' style='padding: 8px 16px;'>Save Credentials</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('save-credentials').addEventListener('click', () => {
|
||||
const clientId = document.getElementById('client-id-input').value.trim();
|
||||
const clientSecret = document.getElementById('client-secret').value.trim();
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
alert('Please enter both Client ID and Client Secret');
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('PAYPAL_CLIENT_ID', clientId);
|
||||
localStorage.setItem('PAYPAL_CLIENT_SECRET', clientSecret);
|
||||
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const clientIdElement = document.getElementById('client-id');
|
||||
clientIdElement.textContent = PAYPAL_CLIENT_ID;
|
||||
document.getElementById('sca-method').value = SCA_METHOD;
|
||||
|
||||
// Add event listener for SCA method change
|
||||
document.getElementById('sca-method').addEventListener('change', () => {
|
||||
const scaMethod = document.getElementById('sca-method').value;
|
||||
localStorage.setItem('SCA_METHOD', scaMethod);
|
||||
logResponse(`3DS verification method changed to: ${scaMethod || 'NEVER'}`);
|
||||
});
|
||||
|
||||
document.getElementById('clear-credentials').addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to clear your PayPal credentials?')) {
|
||||
localStorage.removeItem('PAYPAL_CLIENT_ID');
|
||||
localStorage.removeItem('PAYPAL_CLIENT_SECRET');
|
||||
localStorage.removeItem('SCA_METHOD');
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize the app since we have credentials
|
||||
document.addEventListener('DOMContentLoaded', initializeApp);
|
||||
}
|
||||
|
||||
// Logging function for better debugging
|
||||
function logResponse(message, data) {
|
||||
const logger = document.getElementById('response-log');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
if (undefined === data) {
|
||||
logger.innerHTML += `<div>[${timestamp}] ${message}</div>`;
|
||||
console.log(message);
|
||||
} else {
|
||||
logger.innerHTML +=
|
||||
`<div>[${timestamp}] ${message}<pre>${JSON.stringify(data, null, 2)}</pre></div>`;
|
||||
console.log(message, data);
|
||||
}
|
||||
|
||||
logger.scrollTop = logger.scrollHeight;
|
||||
}
|
||||
|
||||
// Main initialization function
|
||||
async function initializeApp() {
|
||||
try {
|
||||
logResponse('[Mock Server] Generating client token...');
|
||||
|
||||
// Generate access token
|
||||
const tokenResponse = await fetch('https://api-m.sandbox.paypal.com/v1/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Basic ${btoa(`${PAYPAL_CLIENT_ID}:${PAYPAL_CLIENT_SECRET}`)}`,
|
||||
},
|
||||
body: 'grant_type=client_credentials',
|
||||
});
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error('Failed to get PayPal access token');
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
const accessToken = tokenData.access_token;
|
||||
logResponse('[Mock Server] Access token obtained');
|
||||
|
||||
const clientTokenBody = {
|
||||
customer_type: 'MERCHANT',
|
||||
features: ['FASTLANE', 'VAULT'],
|
||||
domains: [location.hostname],
|
||||
};
|
||||
logResponse('[Mock Server] Generate client token...', clientTokenBody);
|
||||
|
||||
// Generate client token for SDK
|
||||
const clientTokenResponse = await fetch('https://api-m.sandbox.paypal.com/v1/identity/generate-token',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Accept-Language': 'en_US',
|
||||
},
|
||||
body: JSON.stringify(clientTokenBody),
|
||||
},
|
||||
);
|
||||
|
||||
if (!clientTokenResponse.ok) {
|
||||
throw new Error('Failed to generate client token');
|
||||
}
|
||||
|
||||
const clientTokenData = await clientTokenResponse.json();
|
||||
const clientToken = clientTokenData.client_token;
|
||||
logResponse('[Mock Server] Client token generated', clientTokenData);
|
||||
|
||||
// Load the PayPal SDK
|
||||
loadPayPalSdk(clientToken);
|
||||
} catch (error) {
|
||||
logResponse('[Mock Server] Error:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Load PayPal SDK with the generated token
|
||||
function loadPayPalSdk(clientToken) {
|
||||
logResponse('Loading the PayPal JS SDK...');
|
||||
const script = document.createElement('script');
|
||||
script.src =
|
||||
`https://www.paypal.com/sdk/js?client-id=${PAYPAL_CLIENT_ID}&components=fastlane`;
|
||||
|
||||
script.setAttribute('data-sdk-client-token', clientToken);
|
||||
script.setAttribute('data-client-token', clientToken);
|
||||
|
||||
script.onload = async function() {
|
||||
logResponse('PayPal SDK loaded');
|
||||
sdkLoaded = true;
|
||||
|
||||
try {
|
||||
// Initialize Fastlane
|
||||
logResponse('Initialize Fastlane...');
|
||||
fastlane = await window.paypal.Fastlane();
|
||||
|
||||
// Set locale if needed (en_us is default)
|
||||
fastlane.setLocale('en_us');
|
||||
} catch (error) {
|
||||
logResponse('Failed to initialize Fastlane:', error);
|
||||
logResponse('-- Maybe it\'s not available/enabled for this account --');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get identity module for customer lookup and authentication
|
||||
logResponse('Get identity module and add watermark...');
|
||||
identity = fastlane.Identity();
|
||||
|
||||
// Render Fastlane watermark for transparency
|
||||
const watermarkElement = document.getElementById('fastlaneWatermark');
|
||||
identity.renderWatermark(watermarkElement);
|
||||
|
||||
// Add event listener for lookup button
|
||||
setupEventListeners();
|
||||
|
||||
logResponse('Fastlane initialized successfully');
|
||||
} catch (error) {
|
||||
logResponse('Failed to configure Fastlane:', error);
|
||||
}
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
// Setup all event listeners
|
||||
function setupEventListeners() {
|
||||
// Lookup button click handler
|
||||
document.getElementById('lookupButton').addEventListener('click', handleLookupButtonClick);
|
||||
|
||||
// Place order button handler
|
||||
document.getElementById('placeOrderButton')
|
||||
.addEventListener('click', handlePlaceOrderButtonClick);
|
||||
}
|
||||
|
||||
// Handle lookup button click
|
||||
async function handleLookupButtonClick() {
|
||||
const email = document.getElementById('email').value;
|
||||
|
||||
if (!email) {
|
||||
alert('Please enter your email address');
|
||||
return;
|
||||
}
|
||||
|
||||
logResponse(`Looking up customer email: ${email}`);
|
||||
|
||||
try {
|
||||
// Look up customer by email
|
||||
const lookupResult = await identity.lookupCustomerByEmail(email);
|
||||
customerContextId = lookupResult.customerContextId;
|
||||
|
||||
if (customerContextId) {
|
||||
// Email is associated with a Fastlane profile or PayPal member
|
||||
logResponse('Customer found with ID:', customerContextId);
|
||||
|
||||
// Trigger authentication flow (OTP)
|
||||
logResponse('Triggering authentication flow...');
|
||||
const authResult = await identity.triggerAuthenticationFlow(customerContextId);
|
||||
logResponse('Authentication result:', authResult);
|
||||
|
||||
if (authResult.authenticationState === 'succeeded') {
|
||||
// Authentication successful - we have profile data
|
||||
renderFastlaneMemberExperience = true;
|
||||
profileData = authResult.profileData;
|
||||
|
||||
// Display member checkout experience
|
||||
displayMemberCheckout(profileData);
|
||||
} else {
|
||||
// Authentication failed or was canceled
|
||||
logResponse('Authentication failed or was canceled');
|
||||
renderFastlaneMemberExperience = false;
|
||||
displayGuestCheckout();
|
||||
}
|
||||
} else {
|
||||
// No profile found - this is a guest
|
||||
logResponse('No Fastlane profile found - processing as guest');
|
||||
renderFastlaneMemberExperience = false;
|
||||
displayGuestCheckout();
|
||||
}
|
||||
} catch (error) {
|
||||
logResponse('Error during customer lookup:', error);
|
||||
displayGuestCheckout();
|
||||
}
|
||||
}
|
||||
|
||||
// Display checkout for authenticated Fastlane members
|
||||
function displayMemberCheckout(profile) {
|
||||
logResponse('Displaying member checkout with profile:', profile);
|
||||
|
||||
// Show shipping address if available
|
||||
if (profile.shippingAddress) {
|
||||
const shippingContainer = document.getElementById('shippingAddressContainer');
|
||||
const shippingDetails = document.getElementById('shippingAddressDetails');
|
||||
const changeShippingBtn = document.getElementById('changeShippingButton');
|
||||
|
||||
// Format and display address
|
||||
const address = profile.shippingAddress;
|
||||
shippingDetails.innerHTML = `
|
||||
<p>${profile.name || ''}</p>
|
||||
<p>${address.line1 || ''}</p>
|
||||
${address.line2 ? `<p>${address.line2}</p>` : ''}
|
||||
<p>${address.city || ''}, ${address.state || ''} ${address.postal_code || ''}</p>
|
||||
<p>${address.country_code || ''}</p>
|
||||
`;
|
||||
|
||||
// Show container and change button
|
||||
shippingContainer.style.display = 'block';
|
||||
changeShippingBtn.style.display = 'block';
|
||||
|
||||
// Handle change address button
|
||||
changeShippingBtn.onclick = function() {
|
||||
try {
|
||||
logResponse('Opening shipping address selector');
|
||||
// Show shipping address selector
|
||||
fastlane.Address().showShippingAddressSelector();
|
||||
} catch (error) {
|
||||
logResponse('Error showing address selector:', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Show payment method if available
|
||||
if (profile.card) {
|
||||
const paymentContainer = document.getElementById('paymentMethodContainer');
|
||||
const paymentDetails = document.getElementById('paymentMethodDetails');
|
||||
const changePaymentBtn = document.getElementById('changePaymentButton');
|
||||
|
||||
// Format and display card info
|
||||
const card = profile.card;
|
||||
paymentDetails.innerHTML = `
|
||||
<p>Card: ${card.brand || ''} ending in ${card.last_digits || ''}</p>
|
||||
<p>Expires: ${card.expiry || ''}</p>
|
||||
`;
|
||||
|
||||
// Show container and change button
|
||||
paymentContainer.style.display = 'block';
|
||||
changePaymentBtn.style.display = 'block';
|
||||
|
||||
// Handle change payment button
|
||||
changePaymentBtn.onclick = function() {
|
||||
try {
|
||||
logResponse('Opening card selector');
|
||||
// Show card selector
|
||||
fastlane.Payment().showCardSelector();
|
||||
} catch (error) {
|
||||
logResponse('Error showing card selector:', error);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// No card available, show card component
|
||||
displayCardComponent();
|
||||
}
|
||||
|
||||
// Show checkout container
|
||||
document.getElementById('checkout-container').style.display = 'block';
|
||||
}
|
||||
|
||||
// Display checkout for guests or unauthenticated users
|
||||
function displayGuestCheckout() {
|
||||
logResponse('Displaying guest checkout');
|
||||
|
||||
// Show shipping container with empty form
|
||||
const shippingContainer = document.getElementById('shippingAddressContainer');
|
||||
const shippingDetails = document.getElementById('shippingAddressDetails');
|
||||
|
||||
// Create shipping form for guest
|
||||
shippingDetails.innerHTML = `
|
||||
<div class='form-group'>
|
||||
<label for='name'>Full Name</label>
|
||||
<input type='text' id='name' placeholder='Enter your full name'>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label for='line1'>Address Line 1</label>
|
||||
<input type='text' id='line1' placeholder='Street address'>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label for='line2'>Address Line 2</label>
|
||||
<input type='text' id='line2' placeholder='Apt, suite, etc. (optional)'>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label for='city'>City</label>
|
||||
<input type='text' id='city' placeholder='City'>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label for='state'>State</label>
|
||||
<input type='text' id='state' placeholder='State'>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label for='postal_code'>Postal Code</label>
|
||||
<input type='text' id='postal_code' placeholder='Postal code'>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label for='country_code'>Country Code</label>
|
||||
<input type='text' id='country_code' placeholder='Country code (e.g., US)'>
|
||||
</div>
|
||||
`;
|
||||
|
||||
shippingContainer.style.display = 'block';
|
||||
|
||||
// Show card component for payment
|
||||
displayCardComponent();
|
||||
|
||||
// Show checkout container
|
||||
document.getElementById('checkout-container').style.display = 'block';
|
||||
}
|
||||
|
||||
// Display Fastlane card component
|
||||
function displayCardComponent() {
|
||||
try {
|
||||
const paymentContainer = document.getElementById('paymentMethodContainer');
|
||||
const cardComponent = document.getElementById('fastlaneCardComponent');
|
||||
|
||||
// Show containers
|
||||
paymentContainer.style.display = 'block';
|
||||
cardComponent.style.display = 'block';
|
||||
|
||||
logResponse('Initializing Fastlane card component');
|
||||
|
||||
// Create and render the card component
|
||||
const payment = fastlane.Payment();
|
||||
|
||||
// Customize the card component (optional)
|
||||
const cardStyle = {
|
||||
input: {
|
||||
color: '#333333',
|
||||
fontSize: '16px',
|
||||
fontFamily: 'Arial, sans-serif',
|
||||
},
|
||||
invalid: {
|
||||
color: '#e5424d',
|
||||
},
|
||||
base: {
|
||||
backgroundColor: '#ffffff',
|
||||
color: '#333333',
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize the card component with options
|
||||
payment.initCardComponent(cardComponent, {
|
||||
style: cardStyle, // Set fields that should be collected
|
||||
fields: {
|
||||
name: {
|
||||
required: true,
|
||||
},
|
||||
number: {
|
||||
required: true,
|
||||
},
|
||||
cvv: {
|
||||
required: true,
|
||||
},
|
||||
expiry: {
|
||||
required: true,
|
||||
},
|
||||
}, // Callbacks
|
||||
onReady: function() {
|
||||
logResponse('Card component is ready');
|
||||
},
|
||||
onChange: function(state) {
|
||||
logResponse('Card component state changed:', state);
|
||||
},
|
||||
onError: function(error) {
|
||||
logResponse('Card component error:', error);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logResponse('Error initializing card component:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle place order button click
|
||||
async function handlePlaceOrderButtonClick() {
|
||||
logResponse('Processing order...');
|
||||
|
||||
// Get the selected SCA method
|
||||
const scaMethod = document.getElementById('sca-method').value;
|
||||
logResponse(`3DS verification method: ${scaMethod || 'NEVER'}`);
|
||||
|
||||
try {
|
||||
if (renderFastlaneMemberExperience && profileData && profileData.card) {
|
||||
// For an authenticated Fastlane member with a saved card
|
||||
logResponse('Processing order for Fastlane member with saved card');
|
||||
|
||||
// Get access token for server call
|
||||
const tokenResponse = await fetch(
|
||||
'https://api-m.sandbox.paypal.com/v1/oauth2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Basic ${btoa(`${PAYPAL_CLIENT_ID}:${PAYPAL_CLIENT_SECRET}`)}`,
|
||||
},
|
||||
body: 'grant_type=client_credentials',
|
||||
},
|
||||
);
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error('Failed to get PayPal access token');
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
const accessToken = tokenData.access_token;
|
||||
|
||||
// Get payment token
|
||||
const payment = fastlane.Payment();
|
||||
const tokenizeResult = await payment.tokenizeCard();
|
||||
|
||||
if (!tokenizeResult || !tokenizeResult.token) {
|
||||
throw new Error('Failed to tokenize saved card');
|
||||
}
|
||||
|
||||
logResponse('Card tokenized:', tokenizeResult);
|
||||
|
||||
// Create the order request body
|
||||
const orderRequestBody = {
|
||||
intent: 'CAPTURE',
|
||||
payment_source: {
|
||||
card: {
|
||||
single_use_token: tokenizeResult.token,
|
||||
},
|
||||
},
|
||||
purchase_units: [
|
||||
{
|
||||
amount: {
|
||||
currency_code: 'USD',
|
||||
value: '10.00',
|
||||
},
|
||||
description: 'Test Item',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Add 3DS verification if specified
|
||||
if (scaMethod) {
|
||||
orderRequestBody.payment_source.card.attributes = {
|
||||
verification: {
|
||||
method: scaMethod,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
logResponse('Creating order with request:', orderRequestBody);
|
||||
|
||||
// Create order with Orders V2 API
|
||||
const orderResponse = await fetch('https://api-m.sandbox.paypal.com/v2/checkout/orders',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(orderRequestBody),
|
||||
},
|
||||
);
|
||||
|
||||
if (!orderResponse.ok) {
|
||||
const errorData = await orderResponse.json();
|
||||
throw new Error(`Failed to create order: ${JSON.stringify(errorData)}`);
|
||||
}
|
||||
|
||||
const orderData = await orderResponse.json();
|
||||
logResponse('Order created successfully:', orderData);
|
||||
|
||||
// Check if 3DS verification is required (PAYER_ACTION_REQUIRED)
|
||||
if (orderData.status === 'PAYER_ACTION_REQUIRED') {
|
||||
logResponse('3DS verification required');
|
||||
|
||||
// Find the payer-action link for 3DS verification
|
||||
const payerActionLink = orderData.links.find(link => link.rel === 'payer-action');
|
||||
|
||||
if (payerActionLink) {
|
||||
// In a real implementation, you would redirect the buyer to this URL
|
||||
logResponse('3DS verification URL:', payerActionLink.href);
|
||||
|
||||
// For this POC, we'll just show an alert with the URL
|
||||
alert(`3DS verification required. In a real implementation, you would redirect to: ${payerActionLink.href}`);
|
||||
|
||||
// After 3DS verification is complete, capture the payment
|
||||
logResponse('3DS verification complete. Capturing payment...');
|
||||
alert(
|
||||
'After 3DS verification, you would need to capture the payment using the Orders API');
|
||||
}
|
||||
} else {
|
||||
// No 3DS required, capture the payment directly
|
||||
alert('Order created successfully! Order ID: ' + orderData.id);
|
||||
}
|
||||
} else {
|
||||
// For guests or members without a saved card
|
||||
logResponse('Processing order for guest or member without saved card');
|
||||
|
||||
// Get payment token
|
||||
const payment = fastlane.Payment();
|
||||
const tokenizeResult = await payment.tokenizeCard();
|
||||
|
||||
if (!tokenizeResult || !tokenizeResult.token) {
|
||||
throw new Error('Failed to tokenize card');
|
||||
}
|
||||
|
||||
logResponse('Card tokenized:', tokenizeResult);
|
||||
|
||||
// Get access token for server call
|
||||
const tokenResponse = await fetch(
|
||||
'https://api-m.sandbox.paypal.com/v1/oauth2/token',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Basic ${btoa(`${PAYPAL_CLIENT_ID}:${PAYPAL_CLIENT_SECRET}`)}`,
|
||||
},
|
||||
body: 'grant_type=client_credentials',
|
||||
},
|
||||
);
|
||||
|
||||
if (!tokenResponse.ok) {
|
||||
throw new Error('Failed to get PayPal access token');
|
||||
}
|
||||
|
||||
const tokenData = await tokenResponse.json();
|
||||
const accessToken = tokenData.access_token;
|
||||
|
||||
// Collect shipping address from form
|
||||
const shippingAddress = {
|
||||
name: document.getElementById('name').value,
|
||||
address_line_1: document.getElementById('line1').value,
|
||||
address_line_2: document.getElementById('line2').value || '',
|
||||
admin_area_2: document.getElementById('city').value,
|
||||
admin_area_1: document.getElementById('state').value,
|
||||
postal_code: document.getElementById('postal_code').value,
|
||||
country_code: document.getElementById('country_code').value,
|
||||
};
|
||||
|
||||
// Create the order request body
|
||||
const orderRequestBody = {
|
||||
intent: 'CAPTURE',
|
||||
payment_source: {
|
||||
card: {
|
||||
single_use_token: tokenizeResult.token,
|
||||
store_in_vault: true, // This enables creating a Fastlane profile
|
||||
},
|
||||
},
|
||||
purchase_units: [
|
||||
{
|
||||
amount: {
|
||||
currency_code: 'USD',
|
||||
value: '10.00',
|
||||
},
|
||||
description: 'Test Item',
|
||||
shipping: {
|
||||
name: {
|
||||
full_name: shippingAddress.name,
|
||||
},
|
||||
address: {
|
||||
address_line_1: shippingAddress.address_line_1,
|
||||
address_line_2: shippingAddress.address_line_2,
|
||||
admin_area_2: shippingAddress.admin_area_2,
|
||||
admin_area_1: shippingAddress.admin_area_1,
|
||||
postal_code: shippingAddress.postal_code,
|
||||
country_code: shippingAddress.country_code,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Add 3DS verification if specified
|
||||
if (scaMethod) {
|
||||
orderRequestBody.payment_source.card.attributes = {
|
||||
verification: {
|
||||
method: scaMethod,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
logResponse('Creating order with request:', orderRequestBody);
|
||||
|
||||
// Create order with Orders V2 API
|
||||
const orderResponse = await fetch('https://api-m.sandbox.paypal.com/v2/checkout/orders',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(orderRequestBody),
|
||||
},
|
||||
);
|
||||
|
||||
if (!orderResponse.ok) {
|
||||
const errorData = await orderResponse.json();
|
||||
throw new Error(`Failed to create order: ${JSON.stringify(errorData)}`);
|
||||
}
|
||||
|
||||
const orderData = await orderResponse.json();
|
||||
logResponse('Order created successfully:', orderData);
|
||||
|
||||
// Check if 3DS verification is required (PAYER_ACTION_REQUIRED)
|
||||
if (orderData.status === 'PAYER_ACTION_REQUIRED') {
|
||||
logResponse('3DS verification required');
|
||||
|
||||
// Find the payer-action link for 3DS verification
|
||||
const payerActionLink = orderData.links.find(link => link.rel === 'payer-action');
|
||||
|
||||
if (payerActionLink) {
|
||||
// In a real implementation, you would redirect the buyer to this URL
|
||||
logResponse('3DS verification URL:', payerActionLink.href);
|
||||
|
||||
// For this POC, we'll just show an alert with the URL
|
||||
alert(`3DS verification required. In a real implementation, you would redirect to: ${payerActionLink.href}`);
|
||||
|
||||
// After 3DS verification is complete, capture the payment
|
||||
logResponse('3DS verification complete. Capturing payment...');
|
||||
alert(
|
||||
'After 3DS verification, you would need to capture the payment using the Orders API');
|
||||
}
|
||||
} else {
|
||||
// No 3DS required, capture the payment directly
|
||||
alert('Order created successfully! Order ID: ' + orderData.id);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logResponse('Error processing order:', error);
|
||||
alert('Error processing order: ' + error.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
input[type="text"],
|
||||
input[type="email"] {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #0070ba;
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #005ea6;
|
||||
}
|
||||
|
||||
.logger {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #eee;
|
||||
padding: 10px;
|
||||
min-height: 100px;
|
||||
max-height: calc(100vh - 260px);
|
||||
overflow-y: auto;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
|
||||
> div {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 0 0 10px 0;
|
||||
margin: 0 0 10px 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#shippingAddressContainer,
|
||||
#paymentMethodContainer,
|
||||
#fastlaneWatermark,
|
||||
#checkout-container {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
#fastlaneCardComponent {
|
||||
min-height: 150px;
|
||||
border: 1px solid #eee;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
#client-id {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
width: 300px;
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
|
@ -44,7 +44,7 @@ class ShippingView {
|
|||
? this.states[ countryCode ][ stateCode ]
|
||||
: stateCode;
|
||||
|
||||
if ( this.hasEmptyValues( data, stateName ) ) {
|
||||
if ( this.hasEmptyValues( data ) ) {
|
||||
return `
|
||||
<div style="margin-bottom: 20px;">
|
||||
<div class="axo-checkout-header-section">
|
||||
|
@ -71,9 +71,9 @@ class ShippingView {
|
|||
) }</div>
|
||||
<div>${ data.value( 'street1' ) }</div>
|
||||
<div>${ data.value( 'street2' ) }</div>
|
||||
<div>${ data.value(
|
||||
'city'
|
||||
) }, ${ stateName } ${ data.value( 'postCode' ) }</div>
|
||||
<div>${ data.value( 'city' ) }${
|
||||
stateName ? ', ' + stateName : ''
|
||||
} ${ data.value( 'postCode' ) }</div>
|
||||
<div>${ valueOfSelect(
|
||||
'#billing_country',
|
||||
countryCode
|
||||
|
@ -158,14 +158,13 @@ class ShippingView {
|
|||
} );
|
||||
}
|
||||
|
||||
hasEmptyValues( data, stateName ) {
|
||||
hasEmptyValues( data ) {
|
||||
return (
|
||||
! data.value( 'email' ) ||
|
||||
! data.value( 'firstName' ) ||
|
||||
! data.value( 'lastName' ) ||
|
||||
! data.value( 'street1' ) ||
|
||||
! data.value( 'city' ) ||
|
||||
! stateName
|
||||
! data.value( 'city' )
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,20 +2,41 @@ import AxoManager from './AxoManager';
|
|||
import { loadPayPalScript } from '../../../ppcp-button/resources/js/modules/Helper/PayPalScriptLoading';
|
||||
import { log } from './Helper/Debug';
|
||||
|
||||
( function ( { axoConfig, ppcpConfig, jQuery } ) {
|
||||
( function ( { axoConfig, ppcpConfig } ) {
|
||||
const namespace = 'ppcpPaypalClassicAxo';
|
||||
const bootstrap = () => {
|
||||
new AxoManager( namespace, axoConfig, ppcpConfig );
|
||||
};
|
||||
|
||||
document.addEventListener( 'DOMContentLoaded', () => {
|
||||
document.addEventListener( 'DOMContentLoaded', async () => {
|
||||
if ( typeof PayPalCommerceGateway === 'undefined' ) {
|
||||
console.error( 'AXO could not be configured.' );
|
||||
return;
|
||||
}
|
||||
|
||||
// Load PayPal
|
||||
loadPayPalScript( namespace, ppcpConfig )
|
||||
const res = await fetch(
|
||||
axoConfig.ajax.axo_script_attributes.endpoint,
|
||||
{
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify( {
|
||||
nonce: axoConfig.ajax.axo_script_attributes.nonce,
|
||||
} ),
|
||||
}
|
||||
);
|
||||
|
||||
const json = await res.json();
|
||||
if ( ! json.success ) {
|
||||
throw new Error( json.data.message );
|
||||
}
|
||||
|
||||
loadPayPalScript( namespace, {
|
||||
...ppcpConfig,
|
||||
script_attributes: {
|
||||
...ppcpConfig.script_attributes,
|
||||
'data-sdk-client-token': json.data.sdk_client_token,
|
||||
},
|
||||
} )
|
||||
.then( () => {
|
||||
bootstrap();
|
||||
} )
|
||||
|
@ -26,5 +47,4 @@ import { log } from './Helper/Debug';
|
|||
} )( {
|
||||
axoConfig: window.wc_ppcp_axo,
|
||||
ppcpConfig: window.PayPalCommerceGateway,
|
||||
jQuery: window.jQuery,
|
||||
} );
|
||||
|
|
|
@ -10,8 +10,10 @@ declare(strict_types=1);
|
|||
namespace WooCommerce\PayPalCommerce\Axo;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager;
|
||||
use WooCommerce\PayPalCommerce\Axo\Endpoint\AxoScriptAttributes;
|
||||
use WooCommerce\PayPalCommerce\Axo\Endpoint\FrontendLogger;
|
||||
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
|
||||
use WooCommerce\PayPalCommerce\Axo\Helper\ApmApplies;
|
||||
use WooCommerce\PayPalCommerce\Axo\Service\AxoApplies;
|
||||
use WooCommerce\PayPalCommerce\Axo\Helper\CompatibilityChecker;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Gateway\CreditCardGateway;
|
||||
|
@ -29,18 +31,20 @@ return array(
|
|||
return $eligibility_check();
|
||||
},
|
||||
'axo.eligibility.check' => static function ( ContainerInterface $container ): callable {
|
||||
$apm_applies = $container->get( 'axo.helpers.apm-applies' );
|
||||
assert( $apm_applies instanceof ApmApplies );
|
||||
$axo_applies = $container->get( 'axo.service.axo-applies' );
|
||||
assert( $axo_applies instanceof AxoApplies );
|
||||
|
||||
return static function () use ( $apm_applies ) : bool {
|
||||
return $apm_applies->for_country_currency() && $apm_applies->for_merchant();
|
||||
return static function () use ( $axo_applies ) : bool {
|
||||
return $axo_applies->for_country_currency() && $axo_applies->for_merchant();
|
||||
};
|
||||
},
|
||||
'axo.helpers.apm-applies' => static function ( ContainerInterface $container ) : ApmApplies {
|
||||
return new ApmApplies(
|
||||
'axo.service.axo-applies' => static function ( ContainerInterface $container ) : AxoApplies {
|
||||
return new AxoApplies(
|
||||
$container->get( 'axo.supported-country-currency-matrix' ),
|
||||
$container->get( 'api.shop.currency.getter' ),
|
||||
$container->get( 'api.shop.country' )
|
||||
$container->get( 'api.shop.country' ),
|
||||
$container->get( 'wcgateway.configuration.card-configuration' ),
|
||||
$container->get( 'wc-subscriptions.helper' )
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -59,14 +63,7 @@ return array(
|
|||
},
|
||||
|
||||
'axo.url' => static function ( ContainerInterface $container ): string {
|
||||
$path = realpath( __FILE__ );
|
||||
if ( false === $path ) {
|
||||
return '';
|
||||
}
|
||||
return plugins_url(
|
||||
'/modules/ppcp-axo/',
|
||||
dirname( $path, 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-axo/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
|
||||
'axo.manager' => static function ( ContainerInterface $container ): AxoManager {
|
||||
|
@ -280,13 +277,22 @@ return array(
|
|||
return '<div class="ppcp-notice ppcp-notice-warning"><p>' . $notice_content . '</p></div>';
|
||||
},
|
||||
|
||||
'axo.endpoint.frontend-logger' => static function ( ContainerInterface $container ): FrontendLoggerEndpoint {
|
||||
return new FrontendLoggerEndpoint(
|
||||
'axo.endpoint.frontend-logger' => static function ( ContainerInterface $container ): FrontendLogger {
|
||||
return new FrontendLogger(
|
||||
$container->get( 'button.request-data' ),
|
||||
$container->get( 'woocommerce.logger.woocommerce' )
|
||||
);
|
||||
},
|
||||
|
||||
'axo.endpoint.script-attributes' => static function ( ContainerInterface $container ): AxoScriptAttributes {
|
||||
return new AxoScriptAttributes(
|
||||
$container->get( 'button.request-data' ),
|
||||
$container->get( 'woocommerce.logger.woocommerce' ),
|
||||
$container->get( 'api.sdk-client-token' ),
|
||||
$container->get( 'axo.eligible' )
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* The list of Fastlane incompatible plugins.
|
||||
*
|
||||
|
|
|
@ -11,7 +11,8 @@ namespace WooCommerce\PayPalCommerce\Axo\Assets;
|
|||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
|
||||
use WooCommerce\PayPalCommerce\Axo\FrontendLoggerEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Axo\Endpoint\AxoScriptAttributes;
|
||||
use WooCommerce\PayPalCommerce\Axo\Endpoint\FrontendLogger;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
|
||||
use WooCommerce\PayPalCommerce\Session\SessionHandler;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\SettingsStatus;
|
||||
|
@ -224,9 +225,13 @@ class AxoManager {
|
|||
'icons_directory' => esc_url( $this->wcgateway_module_url ) . 'assets/images/axo/',
|
||||
'module_url' => untrailingslashit( $this->module_url ),
|
||||
'ajax' => array(
|
||||
'frontend_logger' => array(
|
||||
'endpoint' => \WC_AJAX::get_endpoint( FrontendLoggerEndpoint::ENDPOINT ),
|
||||
'nonce' => wp_create_nonce( FrontendLoggerEndpoint::nonce() ),
|
||||
'frontend_logger' => array(
|
||||
'endpoint' => \WC_AJAX::get_endpoint( FrontendLogger::ENDPOINT ),
|
||||
'nonce' => wp_create_nonce( FrontendLogger::nonce() ),
|
||||
),
|
||||
'axo_script_attributes' => array(
|
||||
'endpoint' => \WC_AJAX::get_endpoint( AxoScriptAttributes::ENDPOINT ),
|
||||
'nonce' => wp_create_nonce( AxoScriptAttributes::nonce() ),
|
||||
),
|
||||
),
|
||||
'logging_enabled' => $this->settings->has( 'logging_enabled' ) ? $this->settings->get( 'logging_enabled' ) : '',
|
||||
|
|
|
@ -9,12 +9,11 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Axo;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\Axo\Assets\AxoManager;
|
||||
use WooCommerce\PayPalCommerce\Axo\Endpoint\AxoScriptAttributes;
|
||||
use WooCommerce\PayPalCommerce\Axo\Endpoint\FrontendLogger;
|
||||
use WooCommerce\PayPalCommerce\Axo\Gateway\AxoGateway;
|
||||
use WooCommerce\PayPalCommerce\Axo\Service\AxoApplies;
|
||||
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
|
||||
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
|
||||
use WooCommerce\PayPalCommerce\Onboarding\Render\OnboardingOptionsRenderer;
|
||||
|
@ -26,7 +25,6 @@ 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;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\SettingsListener;
|
||||
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
|
||||
use WC_Payment_Gateways;
|
||||
|
@ -211,7 +209,10 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
$smart_button = $c->get( 'button.smart-button' );
|
||||
assert( $smart_button instanceof SmartButtonInterface );
|
||||
|
||||
if ( $this->should_render_fastlane( $c ) && $smart_button->should_load_ppcp_script() ) {
|
||||
$axo_applies = $c->get( 'axo.service.axo-applies' );
|
||||
assert( $axo_applies instanceof AxoApplies );
|
||||
|
||||
if ( $axo_applies->should_render_fastlane() && $smart_button->should_load_ppcp_script() ) {
|
||||
$manager->enqueue();
|
||||
}
|
||||
}
|
||||
|
@ -221,7 +222,10 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
add_action(
|
||||
$manager->checkout_button_renderer_hook(),
|
||||
function () use ( $c, $manager ) {
|
||||
if ( $this->should_render_fastlane( $c ) ) {
|
||||
$axo_applies = $c->get( 'axo.service.axo-applies' );
|
||||
assert( $axo_applies instanceof AxoApplies );
|
||||
|
||||
if ( $axo_applies->should_render_fastlane() ) {
|
||||
$manager->render_checkout_button();
|
||||
}
|
||||
}
|
||||
|
@ -274,19 +278,6 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
}
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_localized_script_data',
|
||||
function( array $localized_script_data ) use ( $c ) {
|
||||
$api = $c->get( 'api.sdk-client-token' );
|
||||
assert( $api instanceof SdkClientToken );
|
||||
|
||||
$logger = $c->get( 'woocommerce.logger.woocommerce' );
|
||||
assert( $logger instanceof LoggerInterface );
|
||||
|
||||
return $this->add_sdk_client_token_to_script_data( $api, $logger, $localized_script_data );
|
||||
}
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'ppcp_onboarding_dcc_table_rows',
|
||||
/**
|
||||
|
@ -316,8 +307,10 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
add_action(
|
||||
'template_redirect',
|
||||
function () use ( $c ) {
|
||||
$axo_applies = $c->get( 'axo.service.axo-applies' );
|
||||
assert( $axo_applies instanceof AxoApplies );
|
||||
|
||||
if ( $this->should_render_fastlane( $c ) ) {
|
||||
if ( $axo_applies->should_render_fastlane() ) {
|
||||
WC()->session->set( 'chosen_payment_method', AxoGateway::ID );
|
||||
}
|
||||
}
|
||||
|
@ -330,10 +323,10 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
);
|
||||
|
||||
add_action(
|
||||
'wc_ajax_' . FrontendLoggerEndpoint::ENDPOINT,
|
||||
'wc_ajax_' . FrontendLogger::ENDPOINT,
|
||||
static function () use ( $c ) {
|
||||
$endpoint = $c->get( 'axo.endpoint.frontend-logger' );
|
||||
assert( $endpoint instanceof FrontendLoggerEndpoint );
|
||||
assert( $endpoint instanceof FrontendLogger );
|
||||
|
||||
$endpoint->handle_request();
|
||||
}
|
||||
|
@ -367,38 +360,17 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
}
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
add_action(
|
||||
'wc_ajax_' . AxoScriptAttributes::ENDPOINT,
|
||||
static function () use ( $c ) {
|
||||
$endpoint = $c->get( 'axo.endpoint.script-attributes' );
|
||||
assert( $endpoint instanceof AxoScriptAttributes );
|
||||
|
||||
/**
|
||||
* Adds id token to localized script data.
|
||||
*
|
||||
* @param SdkClientToken $api User id token api.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
* @param array $localized_script_data The localized script data.
|
||||
* @return array
|
||||
*/
|
||||
private function add_sdk_client_token_to_script_data(
|
||||
SdkClientToken $api,
|
||||
LoggerInterface $logger,
|
||||
array $localized_script_data
|
||||
): array {
|
||||
try {
|
||||
$sdk_client_token = $api->sdk_client_token();
|
||||
$localized_script_data['axo'] = array(
|
||||
'sdk_client_token' => $sdk_client_token,
|
||||
);
|
||||
|
||||
} catch ( RuntimeException $exception ) {
|
||||
$error = $exception->getMessage();
|
||||
if ( is_a( $exception, PayPalApiException::class ) ) {
|
||||
$error = $exception->get_details( $error );
|
||||
$endpoint->handle_request();
|
||||
}
|
||||
);
|
||||
|
||||
$logger->error( $error );
|
||||
}
|
||||
|
||||
return $localized_script_data;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -409,31 +381,10 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
* @return bool
|
||||
*/
|
||||
private function hide_credit_card_when_using_fastlane( array $methods, ContainerInterface $c ): bool {
|
||||
return $this->should_render_fastlane( $c ) && isset( $methods[ CreditCardGateway::ID ] );
|
||||
}
|
||||
$axo_applies = $c->get( 'axo.service.axo-applies' );
|
||||
assert( $axo_applies instanceof AxoApplies );
|
||||
|
||||
/**
|
||||
* Condition to evaluate if Fastlane should be rendered.
|
||||
*
|
||||
* Fastlane should only render on the classic checkout, when Fastlane is enabled in the settings and also only for guest customers.
|
||||
*
|
||||
* @param ContainerInterface $c The container.
|
||||
* @return bool
|
||||
*/
|
||||
private function should_render_fastlane( ContainerInterface $c ): bool {
|
||||
|
||||
$dcc_configuration = $c->get( 'wcgateway.configuration.card-configuration' );
|
||||
assert( $dcc_configuration instanceof CardPaymentsConfiguration );
|
||||
|
||||
$subscription_helper = $c->get( 'wc-subscriptions.helper' );
|
||||
assert( $subscription_helper instanceof SubscriptionHelper );
|
||||
|
||||
return ! is_user_logged_in()
|
||||
&& CartCheckoutDetector::has_classic_checkout()
|
||||
&& $dcc_configuration->use_fastlane()
|
||||
&& ! $this->is_excluded_endpoint()
|
||||
&& is_checkout()
|
||||
&& ! $subscription_helper->cart_contains_subscription();
|
||||
return $axo_applies->should_render_fastlane() && isset( $methods[ CreditCardGateway::ID ] );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -443,8 +394,10 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
* @return void
|
||||
*/
|
||||
private function add_checkout_loader_markup( ContainerInterface $c ): void {
|
||||
$axo_applies = $c->get( 'axo.service.axo-applies' );
|
||||
assert( $axo_applies instanceof AxoApplies );
|
||||
|
||||
if ( $this->should_render_fastlane( $c ) ) {
|
||||
if ( $axo_applies->should_render_fastlane() ) {
|
||||
add_action(
|
||||
'woocommerce_checkout_before_customer_details',
|
||||
function () {
|
||||
|
@ -483,7 +436,6 @@ class AxoModule implements ServiceModule, ExtendingModule, ExecutableModule {
|
|||
* @return bool
|
||||
*/
|
||||
private function is_excluded_endpoint(): bool {
|
||||
// Exclude the Order Pay and Order Received endpoints.
|
||||
return is_wc_endpoint_url( 'order-pay' ) || is_wc_endpoint_url( 'order-received' );
|
||||
}
|
||||
|
||||
|
|
69
modules/ppcp-axo/src/Endpoint/AxoScriptAttributes.php
Normal file
69
modules/ppcp-axo/src/Endpoint/AxoScriptAttributes.php
Normal file
|
@ -0,0 +1,69 @@
|
|||
<?php
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Axo\Endpoint;
|
||||
|
||||
use Psr\Log\LoggerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\EndpointInterface;
|
||||
use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
|
||||
use WooCommerce\PayPalCommerce\Button\Helper\ContextTrait;
|
||||
|
||||
/**
|
||||
* Handles the request for the PayPal Axo script attributes.
|
||||
*/
|
||||
class AxoScriptAttributes implements EndpointInterface {
|
||||
use ContextTrait;
|
||||
|
||||
const ENDPOINT = 'ppc-axo-script-attributes';
|
||||
|
||||
private RequestData $request_data;
|
||||
private LoggerInterface $logger;
|
||||
private SdkClientToken $sdk_client_token;
|
||||
private bool $axo_eligible;
|
||||
|
||||
public function __construct(
|
||||
RequestData $request_data,
|
||||
LoggerInterface $logger,
|
||||
SdkClientToken $sdk_client_token,
|
||||
bool $axo_eligible
|
||||
) {
|
||||
$this->request_data = $request_data;
|
||||
$this->logger = $logger;
|
||||
$this->sdk_client_token = $sdk_client_token;
|
||||
$this->axo_eligible = $axo_eligible;
|
||||
}
|
||||
|
||||
public static function nonce(): string {
|
||||
return self::ENDPOINT;
|
||||
}
|
||||
|
||||
public function handle_request(): bool {
|
||||
$this->request_data->read_request( $this->nonce() );
|
||||
|
||||
if (
|
||||
! $this->axo_eligible
|
||||
|| is_user_logged_in()
|
||||
|| $this->is_paypal_continuation()
|
||||
) {
|
||||
wp_send_json_error( 'Failed to load axo script attributes.' );
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$token = $this->sdk_client_token->sdk_client_token();
|
||||
} catch ( PayPalApiException $exception ) {
|
||||
$this->logger->error( $exception->getMessage() );
|
||||
wp_send_json_error( $exception->getMessage() );
|
||||
return false;
|
||||
}
|
||||
|
||||
wp_send_json_success(
|
||||
array(
|
||||
'sdk_client_token' => $token,
|
||||
)
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Axo;
|
||||
namespace WooCommerce\PayPalCommerce\Axo\Endpoint;
|
||||
|
||||
use Exception;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
@ -17,7 +17,7 @@ use WooCommerce\PayPalCommerce\Button\Endpoint\RequestData;
|
|||
/**
|
||||
* Class FrontendLoggerEndpoint
|
||||
*/
|
||||
class FrontendLoggerEndpoint implements EndpointInterface {
|
||||
class FrontendLogger implements EndpointInterface {
|
||||
|
||||
const ENDPOINT = 'ppc-frontend-logger';
|
||||
|
|
@ -1,21 +1,21 @@
|
|||
<?php
|
||||
/**
|
||||
* ApmApplies helper.
|
||||
* AxoApplies helper.
|
||||
* Checks if AXO is available for a given country and currency.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\Axo\Helper
|
||||
* @package WooCommerce\PayPalCommerce\Axo\Service
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\Axo\Helper;
|
||||
namespace WooCommerce\PayPalCommerce\Axo\Service;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\CardPaymentsConfiguration;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\CartCheckoutDetector;
|
||||
use WooCommerce\PayPalCommerce\WcSubscriptions\Helper\SubscriptionHelper;
|
||||
|
||||
/**
|
||||
* Class ApmApplies
|
||||
*/
|
||||
class ApmApplies {
|
||||
class AxoApplies {
|
||||
|
||||
/**
|
||||
* The matrix which countries and currency combinations can be used for AXO.
|
||||
|
@ -38,6 +38,9 @@ class ApmApplies {
|
|||
*/
|
||||
private $country;
|
||||
|
||||
private CardPaymentsConfiguration $dcc_configuration;
|
||||
private SubscriptionHelper $subscription_helper;
|
||||
|
||||
/**
|
||||
* DccApplies constructor.
|
||||
*
|
||||
|
@ -48,11 +51,15 @@ class ApmApplies {
|
|||
public function __construct(
|
||||
array $allowed_country_currency_matrix,
|
||||
CurrencyGetter $currency,
|
||||
string $country
|
||||
string $country,
|
||||
CardPaymentsConfiguration $dcc_configuration,
|
||||
SubscriptionHelper $subscription_helper
|
||||
) {
|
||||
$this->allowed_country_currency_matrix = $allowed_country_currency_matrix;
|
||||
$this->currency = $currency;
|
||||
$this->country = $country;
|
||||
$this->dcc_configuration = $dcc_configuration;
|
||||
$this->subscription_helper = $subscription_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -79,4 +86,27 @@ class ApmApplies {
|
|||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if Fastlane should be rendered.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function should_render_fastlane(): bool {
|
||||
return ! is_user_logged_in()
|
||||
&& CartCheckoutDetector::has_classic_checkout()
|
||||
&& $this->dcc_configuration->use_fastlane()
|
||||
&& ! $this->is_excluded_endpoint()
|
||||
&& is_checkout()
|
||||
&& ! $this->subscription_helper->cart_contains_subscription();
|
||||
}
|
||||
|
||||
/**
|
||||
* Condition to evaluate if the current endpoint is excluded.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function is_excluded_endpoint(): bool {
|
||||
return is_wc_endpoint_url( 'order-pay' ) || is_wc_endpoint_url( 'order-received' );
|
||||
}
|
||||
}
|
|
@ -27,7 +27,7 @@ export const paypalAddressToWc = ( address ) => {
|
|||
admin_area_2: 'city',
|
||||
postal_code: 'postcode',
|
||||
};
|
||||
if ( address.city ) {
|
||||
if ( address?.city ) {
|
||||
// address not from API, such as onShippingChange
|
||||
map = {
|
||||
country_code: 'country',
|
||||
|
@ -38,7 +38,7 @@ export const paypalAddressToWc = ( address ) => {
|
|||
}
|
||||
const result = {};
|
||||
Object.entries( map ).forEach( ( [ paypalKey, wcKey ] ) => {
|
||||
if ( address[ paypalKey ] ) {
|
||||
if ( address?.[ paypalKey ] ) {
|
||||
result[ wcKey ] = address[ paypalKey ];
|
||||
}
|
||||
} );
|
||||
|
@ -115,7 +115,7 @@ export const paypalSubscriberToWc = ( subscriber ) => {
|
|||
* @return {Object}
|
||||
*/
|
||||
export const paypalOrderToWcShippingAddress = ( order ) => {
|
||||
const shipping = order.purchase_units?.[ 0 ]?.shipping;
|
||||
const shipping = order?.purchase_units?.[ 0 ]?.shipping;
|
||||
if ( ! shipping ) {
|
||||
return {};
|
||||
}
|
||||
|
@ -145,7 +145,7 @@ export const paypalOrderToWcShippingAddress = ( order ) => {
|
|||
export const paypalOrderToWcAddresses = ( order ) => {
|
||||
const shippingAddress = paypalOrderToWcShippingAddress( order );
|
||||
let billingAddress = shippingAddress;
|
||||
if ( order.payer ) {
|
||||
if ( order?.payer ) {
|
||||
billingAddress = paypalPayerToWc( order.payer );
|
||||
// no billing address, such as if billing address retrieval is not allowed in the merchant account
|
||||
if ( ! billingAddress.address_line_1 ) {
|
||||
|
|
|
@ -17,15 +17,7 @@ use WC_Cart;
|
|||
|
||||
return array(
|
||||
'blocks.url' => static function ( ContainerInterface $container ): string {
|
||||
/**
|
||||
* The path cannot be false.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-blocks/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-blocks/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'blocks.method' => static function ( ContainerInterface $container ): PayPalPaymentMethod {
|
||||
return new PayPalPaymentMethod(
|
||||
|
|
|
@ -260,7 +260,7 @@ class PayPalPaymentMethod extends AbstractPaymentMethodType {
|
|||
'src' => $this->gateway->icon,
|
||||
),
|
||||
),
|
||||
'description' => $this->gateway->description,
|
||||
'description' => $this->gateway->get_description(),
|
||||
'smartButtonsEnabled' => $smart_buttons_enabled,
|
||||
'placeOrderEnabled' => $place_order_enabled,
|
||||
'fundingSource' => $this->session_handler->funding_source(),
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { setVisible } from '../Helper/Hiding';
|
||||
import MessageRenderer from '../Renderer/MessageRenderer';
|
||||
import { waitForElement } from '../Helper/WaitForElement';
|
||||
|
||||
class MessagesBootstrap {
|
||||
constructor( gateway, messageRenderer ) {
|
||||
|
@ -93,17 +94,21 @@ class MessagesBootstrap {
|
|||
|
||||
render() {
|
||||
this.renderers.forEach( ( renderer ) => {
|
||||
const shouldShow = this.shouldShow( renderer );
|
||||
setVisible( renderer.config.wrapper, shouldShow );
|
||||
if ( ! shouldShow ) {
|
||||
return;
|
||||
}
|
||||
waitForElement( renderer.config.wrapper )
|
||||
.then( () => {
|
||||
const shouldShow = this.shouldShow( renderer );
|
||||
setVisible( renderer.config.wrapper, shouldShow );
|
||||
if ( ! shouldShow ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! renderer.shouldRender() ) {
|
||||
return;
|
||||
}
|
||||
if ( ! renderer.shouldRender() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderer.renderWithAmount( this.lastAmount );
|
||||
renderer.renderWithAmount( this.lastAmount );
|
||||
} )
|
||||
.catch( ( err ) => console.error( err ) );
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,6 @@
|
|||
import merge from 'deepmerge';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { keysToCamelCase } from './Utils';
|
||||
|
||||
const processAxoConfig = ( config ) => {
|
||||
const scriptOptions = {};
|
||||
const sdkClientToken = config?.axo?.sdk_client_token;
|
||||
const uuid = uuidv4().replace( /-/g, '' );
|
||||
if ( sdkClientToken && config?.user?.is_logged !== true ) {
|
||||
scriptOptions[ 'data-sdk-client-token' ] = sdkClientToken;
|
||||
scriptOptions[ 'data-client-metadata-id' ] = uuid;
|
||||
}
|
||||
return scriptOptions;
|
||||
};
|
||||
|
||||
const processUserIdToken = ( config ) => {
|
||||
const userIdToken = config?.save_payment_methods?.id_token;
|
||||
return userIdToken && config?.user?.is_logged === true
|
||||
|
@ -25,7 +13,8 @@ export const processConfig = ( config ) => {
|
|||
if ( config.script_attributes ) {
|
||||
scriptOptions = merge( scriptOptions, config.script_attributes );
|
||||
}
|
||||
const axoOptions = processAxoConfig( config );
|
||||
|
||||
const userIdTokenOptions = processUserIdToken( config );
|
||||
return merge.all( [ scriptOptions, axoOptions, userIdTokenOptions ] );
|
||||
|
||||
return merge.all( [ scriptOptions, userIdTokenOptions ] );
|
||||
};
|
||||
|
|
|
@ -1,31 +1,10 @@
|
|||
import { loadScript } from '@paypal/paypal-js';
|
||||
import dataClientIdAttributeHandler from '../DataClientIdAttributeHandler';
|
||||
import widgetBuilder from '../Renderer/WidgetBuilder';
|
||||
import { processConfig } from './ConfigProcessor';
|
||||
|
||||
const loadedScripts = new Map();
|
||||
const scriptPromises = new Map();
|
||||
|
||||
const handleDataClientIdAttribute = async ( scriptOptions, config ) => {
|
||||
if (
|
||||
config.data_client_id?.set_attribute &&
|
||||
config.vault_v3_enabled !== true
|
||||
) {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
dataClientIdAttributeHandler(
|
||||
scriptOptions,
|
||||
config.data_client_id,
|
||||
( paypal ) => {
|
||||
widgetBuilder.setPaypal( paypal );
|
||||
resolve( paypal );
|
||||
},
|
||||
reject
|
||||
);
|
||||
} );
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const loadPayPalScript = async ( namespace, config ) => {
|
||||
if ( ! namespace ) {
|
||||
throw new Error( 'Namespace is required' );
|
||||
|
@ -48,14 +27,6 @@ export const loadPayPalScript = async ( namespace, config ) => {
|
|||
'data-namespace': namespace,
|
||||
};
|
||||
|
||||
const dataClientIdResult = await handleDataClientIdAttribute(
|
||||
scriptOptions,
|
||||
config
|
||||
);
|
||||
if ( dataClientIdResult ) {
|
||||
return dataClientIdResult;
|
||||
}
|
||||
|
||||
const scriptPromise = new Promise( ( resolve, reject ) => {
|
||||
loadScript( scriptOptions )
|
||||
.then( ( script ) => {
|
||||
|
|
|
@ -1,10 +1,7 @@
|
|||
import dataClientIdAttributeHandler from '../DataClientIdAttributeHandler';
|
||||
import { loadScript } from '@paypal/paypal-js';
|
||||
import widgetBuilder from '../Renderer/WidgetBuilder';
|
||||
import merge from 'deepmerge';
|
||||
import { keysToCamelCase } from './Utils';
|
||||
import { getCurrentPaymentMethod } from './CheckoutMethodState';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
// This component may be used by multiple modules. This assures that options are shared between all instances.
|
||||
const scriptOptionsMap = {};
|
||||
|
@ -72,28 +69,6 @@ export const loadPaypalScript = ( config, onLoaded, onError = null ) => {
|
|||
scriptOptions = merge( scriptOptions, config.script_attributes );
|
||||
}
|
||||
|
||||
// Axo SDK options
|
||||
const sdkClientToken = config?.axo?.sdk_client_token;
|
||||
const uuid = uuidv4().replace( /-/g, '' );
|
||||
if ( sdkClientToken && config?.user?.is_logged !== true ) {
|
||||
scriptOptions[ 'data-sdk-client-token' ] = sdkClientToken;
|
||||
scriptOptions[ 'data-client-metadata-id' ] = uuid;
|
||||
}
|
||||
|
||||
// Load PayPal script for special case with data-client-token
|
||||
if (
|
||||
config.data_client_id?.set_attribute &&
|
||||
config.vault_v3_enabled !== '1'
|
||||
) {
|
||||
dataClientIdAttributeHandler(
|
||||
scriptOptions,
|
||||
config.data_client_id,
|
||||
callback,
|
||||
errorCallback
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Adds data-user-id-token to script options.
|
||||
const userIdToken = config?.save_payment_methods?.id_token;
|
||||
if ( userIdToken && config?.user?.is_logged === true ) {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/**
|
||||
* Waits for a DOM element using setTimeout polling
|
||||
* @param {string} selector - CSS selector for the element
|
||||
* @param {number} timeout - Maximum time to wait in milliseconds (default: 3000)
|
||||
* @param {number} interval - Polling interval in milliseconds (default: 100)
|
||||
* @return {Promise<Element>} - Resolves with the element or rejects if timeout
|
||||
*/
|
||||
export function waitForElement( selector, timeout = 3000, interval = 100 ) {
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const timeoutId = setTimeout( () => {
|
||||
clearInterval( intervalId );
|
||||
reject( `Element "${ selector }" not found within ${ timeout }ms` );
|
||||
}, timeout );
|
||||
|
||||
const element = document.querySelector( selector );
|
||||
if ( element ) {
|
||||
clearTimeout( timeoutId );
|
||||
resolve( element );
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalId = setInterval( () => {
|
||||
const el = document.querySelector( selector );
|
||||
|
||||
if ( el ) {
|
||||
clearTimeout( timeoutId );
|
||||
clearInterval( intervalId );
|
||||
resolve( el );
|
||||
}
|
||||
}, interval );
|
||||
} );
|
||||
}
|
|
@ -177,10 +177,7 @@ return array(
|
|||
);
|
||||
},
|
||||
'button.url' => static function ( ContainerInterface $container ): string {
|
||||
return plugins_url(
|
||||
'/modules/ppcp-button/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-button/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'button.pay-now-contexts' => static function ( ContainerInterface $container ): array {
|
||||
$defaults = array( 'checkout', 'pay-now' );
|
||||
|
|
|
@ -20,8 +20,7 @@ use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
|
|||
*/
|
||||
class CartScriptParamsEndpoint implements EndpointInterface {
|
||||
|
||||
|
||||
const ENDPOINT = 'ppc-cart-script-params';
|
||||
public const ENDPOINT = 'ppc-cart-script-params';
|
||||
|
||||
/**
|
||||
* The SmartButton.
|
||||
|
|
|
@ -159,7 +159,7 @@ class DisabledFundingSources {
|
|||
return $disable_funding;
|
||||
}
|
||||
|
||||
if ( ! is_checkout() || $this->dcc_configuration->use_acdc() ) {
|
||||
if ( ! is_checkout() || $this->dcc_configuration->use_acdc() || ! $this->dcc_configuration->is_bcdc_enabled() ) {
|
||||
// Non-checkout pages, or ACDC capability: Don't load card button.
|
||||
$disable_funding[] = 'card';
|
||||
}
|
||||
|
|
|
@ -101,15 +101,7 @@ return array(
|
|||
},
|
||||
|
||||
'compat.module.url' => static function ( ContainerInterface $container ): string {
|
||||
/**
|
||||
* The path cannot be false.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-compat/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-compat/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
|
||||
'compat.assets' => function( ContainerInterface $container ) : CompatAssets {
|
||||
|
|
|
@ -76,7 +76,7 @@ class SubscriptionsHandler {
|
|||
* @return array
|
||||
*/
|
||||
public function add_mock_ppec_gateway( $gateways ) {
|
||||
if ( ! isset( $gateways[ PPECHelper::PPEC_GATEWAY_ID ] ) && $this->should_mock_ppec_gateway() ) {
|
||||
if ( ! isset( $gateways[ PPECHelper::PPEC_GATEWAY_ID ] ) ) {
|
||||
$gateways[ PPECHelper::PPEC_GATEWAY_ID ] = $this->mock_gateway;
|
||||
}
|
||||
|
||||
|
@ -125,87 +125,25 @@ class SubscriptionsHandler {
|
|||
public function use_billing_agreement_as_token( $token, $customer, $order ) {
|
||||
if ( PPECHelper::PPEC_GATEWAY_ID === $order->get_payment_method() && wcs_order_contains_renewal( $order ) ) {
|
||||
$billing_agreement_id = $order->get_meta( '_ppec_billing_agreement_id', true );
|
||||
|
||||
if ( $billing_agreement_id ) {
|
||||
$token = new PaymentToken( $billing_agreement_id, new stdClass(), 'BILLING_AGREEMENT' );
|
||||
return new PaymentToken( $billing_agreement_id, new stdClass(), 'BILLING_AGREEMENT' );
|
||||
}
|
||||
|
||||
$subscriptions = wcs_get_subscriptions_for_renewal_order( $order );
|
||||
if ( ! empty( $subscriptions ) ) {
|
||||
$subscription = reset( $subscriptions ); // Get first subscription.
|
||||
$parent_order = $subscription->get_parent();
|
||||
|
||||
if ( $parent_order ) {
|
||||
$billing_agreement_id = $parent_order->get_meta( '_ppec_billing_agreement_id', true );
|
||||
|
||||
if ( $billing_agreement_id ) {
|
||||
return new PaymentToken( $billing_agreement_id, new stdClass(), 'BILLING_AGREEMENT' );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the mock PPEC gateway should be used or not.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_mock_ppec_gateway() {
|
||||
// Are we processing a renewal?
|
||||
if ( doing_action( 'woocommerce_scheduled_subscription_payment' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// My Account > Subscriptions.
|
||||
if ( is_wc_endpoint_url( 'subscriptions' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// phpcs:disable WordPress.Security.NonceVerification
|
||||
|
||||
// Checks that require Subscriptions.
|
||||
if ( class_exists( \WC_Subscriptions::class ) ) {
|
||||
// My Account > Subscriptions > (Subscription).
|
||||
if ( wcs_is_view_subscription_page() ) {
|
||||
$subscription = wcs_get_subscription( absint( get_query_var( 'view-subscription' ) ) );
|
||||
|
||||
return ( $subscription && PPECHelper::PPEC_GATEWAY_ID === $subscription->get_payment_method() );
|
||||
}
|
||||
|
||||
// Changing payment method?
|
||||
if ( is_wc_endpoint_url( 'order-pay' ) && isset( $_GET['change_payment_method'] ) ) {
|
||||
$subscription = wcs_get_subscription( absint( get_query_var( 'order-pay' ) ) );
|
||||
|
||||
return ( $subscription && PPECHelper::PPEC_GATEWAY_ID === $subscription->get_payment_method() );
|
||||
}
|
||||
|
||||
// Early renew (via modal).
|
||||
if ( isset( $_GET['process_early_renewal'], $_GET['subscription_id'] ) ) {
|
||||
$subscription = wcs_get_subscription( absint( $_GET['subscription_id'] ) );
|
||||
|
||||
return ( $subscription && PPECHelper::PPEC_GATEWAY_ID === $subscription->get_payment_method() );
|
||||
}
|
||||
}
|
||||
|
||||
// Admin-only from here onwards.
|
||||
if ( ! is_admin() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Are we saving metadata for a subscription?
|
||||
if ( doing_action( 'woocommerce_process_shop_order_meta' ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Are we editing an order or subscription tied to PPEC?
|
||||
$order_id = wc_clean( wp_unslash( $_GET['id'] ?? $_GET['post'] ?? $_POST['post_ID'] ?? '' ) );
|
||||
if ( $order_id ) {
|
||||
$order = wc_get_order( $order_id );
|
||||
return ( $order && PPECHelper::PPEC_GATEWAY_ID === $order->get_payment_method() );
|
||||
}
|
||||
|
||||
// Are we on the WC > Subscriptions screen?
|
||||
/**
|
||||
* Class exist in WooCommerce.
|
||||
*
|
||||
* @psalm-suppress UndefinedClass
|
||||
*/
|
||||
$post_type_or_page = class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled()
|
||||
? wc_clean( wp_unslash( $_GET['page'] ?? '' ) )
|
||||
: wc_clean( wp_unslash( $_GET['post_type'] ?? $_POST['post_type'] ?? '' ) );
|
||||
if ( $post_type_or_page === 'shop_subscription' || $post_type_or_page === 'wc-orders--shop_subscription' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
407
modules/ppcp-googlepay/docs/payment-test.html
Normal file
407
modules/ppcp-googlepay/docs/payment-test.html
Normal file
|
@ -0,0 +1,407 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang='en'>
|
||||
<!--
|
||||
|
||||
URL: /wp-content/plugins/woocommerce-paypal-payments/modules/ppcp-googlepay/docs/payment-test.html
|
||||
|
||||
-->
|
||||
<head>
|
||||
<meta charset='UTF-8'>
|
||||
<title>Google Pay Test</title>
|
||||
<script defer src='https://pay.google.com/gp/p/js/pay.js' onload='loadSdk()'></script>
|
||||
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
section {
|
||||
border: 1px solid #ddd;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.logger {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #eee;
|
||||
padding: 10px;
|
||||
min-height: 100px;
|
||||
max-height: calc(100vh - 296px);
|
||||
overflow-y: auto;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
|
||||
> div {
|
||||
border-bottom: 1px solid #eee;
|
||||
padding: 0 0 10px 0;
|
||||
margin: 0 0 10px 0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#google-pay-button {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
height: 24px;
|
||||
line-height: 24px;
|
||||
margin: 6px 0;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#credentials-form .form-group {
|
||||
flex-direction: column;
|
||||
height: 46px;
|
||||
}
|
||||
|
||||
#client-id {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
padding: 0 10px;
|
||||
width: 500px;
|
||||
display: inline-block;
|
||||
vertical-align: text-bottom;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<section>
|
||||
<h3>Google Pay with PayPal SDK Integration</h3>
|
||||
<div id='test-form'>
|
||||
<div class='form-group'>
|
||||
<div>
|
||||
PayPal client ID:
|
||||
<span id='client-id'></span>
|
||||
</div>
|
||||
<button id='clear-credentials'>Clear</button>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label for='sca-method'>3DS Verification:</label>
|
||||
<select id='sca-method'>
|
||||
<option value=''>NEVER - Do not ask for 3DS verification</option>
|
||||
<option value='SCA_WHEN_REQUIRED'>WHEN REQUIRED - Let PayPal decide if to show
|
||||
3DS
|
||||
</option>
|
||||
<option value='SCA_ALWAYS'>ALWAYS - Ask for 3DS verification</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id='google-pay-button'></div>
|
||||
</div>
|
||||
<div class='logger' id='response-log'></div>
|
||||
</section>
|
||||
|
||||
<!--
|
||||
|
||||
SCRIPT
|
||||
|
||||
-->
|
||||
|
||||
<script>
|
||||
// Check if credentials exist in localStorage
|
||||
const PAYPAL_CLIENT_ID = localStorage.getItem('PAYPAL_CLIENT_ID');
|
||||
const PAYPAL_CLIENT_SECRET = localStorage.getItem('PAYPAL_CLIENT_SECRET');
|
||||
|
||||
// If credentials don't exist, show input form
|
||||
if (!PAYPAL_CLIENT_ID || !PAYPAL_CLIENT_SECRET) {
|
||||
document.getElementById('test-form').innerHTML = `
|
||||
<div id='credentials-form'>
|
||||
<h4>Enter PayPal API Credentials</h4>
|
||||
<div class='form-group'>
|
||||
<label for='form-client-id'>Client ID:</label>
|
||||
<input type='text' id='form-client-id'>
|
||||
</div>
|
||||
<div class='form-group'>
|
||||
<label for='form-client-secret'>Client Secret:</label>
|
||||
<input type='text' id='form-client-secret'>
|
||||
</div>
|
||||
<button id='save-credentials'>Save Credentials</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
document.getElementById('save-credentials').addEventListener('click', () => {
|
||||
const clientId = document.getElementById('form-client-id').value.trim();
|
||||
const clientSecret = document.getElementById('form-client-secret').value.trim();
|
||||
|
||||
if (!clientId || !clientSecret) {
|
||||
alert('Please enter both Client ID and Client Secret');
|
||||
return;
|
||||
}
|
||||
|
||||
localStorage.setItem('PAYPAL_CLIENT_ID', clientId);
|
||||
localStorage.setItem('PAYPAL_CLIENT_SECRET', clientSecret);
|
||||
|
||||
window.location.reload();
|
||||
});
|
||||
});
|
||||
} else {
|
||||
const clientIdElement = document.getElementById('client-id');
|
||||
clientIdElement.textContent = PAYPAL_CLIENT_ID;
|
||||
|
||||
document.getElementById('clear-credentials').addEventListener('click', () => {
|
||||
if (confirm('Are you sure you want to clear your PayPal credentials?')) {
|
||||
localStorage.removeItem('PAYPAL_CLIENT_ID');
|
||||
localStorage.removeItem('PAYPAL_CLIENT_SECRET');
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
const selectSCA = document.getElementById('sca-method');
|
||||
|
||||
selectSCA.addEventListener('change', () => {
|
||||
const scaMethod = selectSCA.value;
|
||||
localStorage.setItem('SCA_METHOD', scaMethod);
|
||||
logResponse(`3DS verification method changed to: ${scaMethod || 'NEVER'}`);
|
||||
});
|
||||
selectSCA.value = localStorage.getItem('SCA_METHOD');
|
||||
}
|
||||
|
||||
// ---
|
||||
|
||||
let paymentsClient = null;
|
||||
let googlePayConfig = null;
|
||||
|
||||
function logResponse(message, data) {
|
||||
const logger = document.getElementById('response-log');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
|
||||
if (undefined === data) {
|
||||
logger.innerHTML += `<div>[${timestamp}] ${message}</div>`;
|
||||
console.log(message);
|
||||
} else {
|
||||
logger.innerHTML +=
|
||||
`<div>[${timestamp}] ${message}<pre>${JSON.stringify(data, null, 2)}</pre></div>`;
|
||||
console.log(message, data);
|
||||
}
|
||||
|
||||
logger.scrollTop = logger.scrollHeight;
|
||||
}
|
||||
|
||||
/*
|
||||
*
|
||||
*
|
||||
* Actual Google Pay test logic starts here
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
function loadSdk() {
|
||||
if (!PAYPAL_CLIENT_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
logResponse('Loading the JS SDK...');
|
||||
const script = document.createElement('script');
|
||||
script.src =
|
||||
'https://www.paypal.com/sdk/js?components=googlepay&client-id=' + PAYPAL_CLIENT_ID;
|
||||
script.onload = initializeDemo;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
async function initializeDemo() {
|
||||
logResponse(`PayPal SDK loaded for client ID ${PAYPAL_CLIENT_ID}`);
|
||||
|
||||
paymentsClient = new google.payments.api.PaymentsClient({
|
||||
environment: 'TEST',
|
||||
});
|
||||
|
||||
googlePayConfig = await paypal.Googlepay().config();
|
||||
logResponse('PayPal Google Pay config loaded:', googlePayConfig);
|
||||
|
||||
const isReadyToPayRequest = {
|
||||
apiVersion: 2,
|
||||
apiVersionMinor: 0,
|
||||
allowedPaymentMethods: googlePayConfig.allowedPaymentMethods,
|
||||
};
|
||||
|
||||
const response = await paymentsClient.isReadyToPay(isReadyToPayRequest);
|
||||
|
||||
if (response.result) {
|
||||
logResponse('Google Pay is available on this device/browser');
|
||||
|
||||
// Add Google Pay button to the UI!
|
||||
addGooglePayButton();
|
||||
} else {
|
||||
logResponse('Google Pay is not available on this device/browser');
|
||||
}
|
||||
}
|
||||
|
||||
function addGooglePayButton() {
|
||||
const paymentRequest = {
|
||||
apiVersion: 2,
|
||||
apiVersionMinor: 0,
|
||||
allowedPaymentMethods: googlePayConfig.allowedPaymentMethods,
|
||||
merchantInfo: {
|
||||
merchantName: 'Example Merchant',
|
||||
},
|
||||
transactionInfo: {
|
||||
totalPriceStatus: 'FINAL',
|
||||
totalPrice: '10.00',
|
||||
currencyCode: 'USD',
|
||||
countryCode: 'US',
|
||||
},
|
||||
};
|
||||
|
||||
const button = paymentsClient.createButton({
|
||||
buttonType: 'plain',
|
||||
buttonColor: 'black',
|
||||
buttonSizeMode: 'fill',
|
||||
onClick: async function() {
|
||||
await onGooglePayButtonClicked(paymentRequest);
|
||||
|
||||
logResponse('========== end of payment process');
|
||||
},
|
||||
});
|
||||
|
||||
document.getElementById('google-pay-button').appendChild(button);
|
||||
logResponse('Google Pay button added');
|
||||
}
|
||||
|
||||
async function onGooglePayButtonClicked(paymentRequest) {
|
||||
logResponse('Google Pay button clicked');
|
||||
|
||||
try {
|
||||
// 1. First get the Google Pay response
|
||||
const paymentData = await paymentsClient.loadPaymentData(paymentRequest);
|
||||
logResponse('Payment data received:', paymentData);
|
||||
|
||||
// 2. Create a PayPal order using the SDK
|
||||
const orderId = await createPayPalOrder();
|
||||
|
||||
// 3. Confirm the order with the payment data
|
||||
logResponse(`Confirm payment "${orderId}" via PayPal...`);
|
||||
const paypalOrder = await paypal.Googlepay().confirmOrder({
|
||||
orderId,
|
||||
paymentMethodData: paymentData.paymentMethodData,
|
||||
});
|
||||
|
||||
logResponse('Payment confirmation result:', paypalOrder);
|
||||
|
||||
// 4. Handle approval or additional actions
|
||||
if (!paypalOrder?.status) {
|
||||
logResponse('Unexpected approval response', paypalOrder);
|
||||
} else if (paypalOrder.status === 'APPROVED') {
|
||||
logResponse('Payment approved!', paypalOrder);
|
||||
} else if (paypalOrder.status === 'PAYER_ACTION_REQUIRED') {
|
||||
logResponse('Additional authentication required');
|
||||
const res = await paypal.Googlepay().initiatePayerAction({ orderId });
|
||||
logResponse('Authentication completed', res);
|
||||
}
|
||||
} catch (err) {
|
||||
logResponse('Payment error:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function createPayPalOrder() {
|
||||
logResponse('[Mock Server] Creating PayPal order directly from client...');
|
||||
|
||||
return fetch('https://api-m.sandbox.paypal.com/v1/oauth2/token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Authorization': `Basic ${btoa(`${PAYPAL_CLIENT_ID}:${PAYPAL_CLIENT_SECRET}`)}`,
|
||||
},
|
||||
body: 'grant_type=client_credentials',
|
||||
})
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to get PayPal access token');
|
||||
}
|
||||
logResponse('[Mock Server] Access token obtained');
|
||||
return response.json();
|
||||
})
|
||||
.then(function(tokenData) {
|
||||
const SCA_METHOD = localStorage.getItem('SCA_METHOD');
|
||||
const accessToken = tokenData.access_token;
|
||||
|
||||
const payer = {
|
||||
email_address: 'john.doe@personal.example.com',
|
||||
name: {
|
||||
given_name: 'John',
|
||||
surname: 'Doe',
|
||||
},
|
||||
address: {
|
||||
country_code: 'US',
|
||||
address_line_1: '456 Elm Street',
|
||||
admin_area_1: 'WA',
|
||||
admin_area_2: 'Seattle',
|
||||
postal_code: '85745',
|
||||
},
|
||||
phone: {
|
||||
phone_type: 'HOME',
|
||||
phone_number: { national_number: '5552223365' },
|
||||
},
|
||||
};
|
||||
|
||||
const body = {
|
||||
intent: 'CAPTURE',
|
||||
purchase_units: [
|
||||
{
|
||||
amount: {
|
||||
currency_code: 'USD',
|
||||
value: '10.00',
|
||||
},
|
||||
description: 'Sample Item',
|
||||
},
|
||||
],
|
||||
payer,
|
||||
};
|
||||
|
||||
if ('SCA_ALWAYS' === SCA_METHOD || 'SCA_WHEN_REQUIRED' === SCA_METHOD) {
|
||||
body.payment_source = {
|
||||
google_pay: {
|
||||
attributes: { verification: { method: SCA_METHOD } },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
logResponse('[Mock Server] Create PayPal order', body.payment_source);
|
||||
return fetch('https://api-m.sandbox.paypal.com/v2/checkout/orders', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
})
|
||||
.then(function(response) {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create PayPal order');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(function(orderData) {
|
||||
logResponse('[Mock Server] Order created with ID: ' + orderData.id);
|
||||
return orderData.id;
|
||||
})
|
||||
.catch(function(err) {
|
||||
logResponse('[Mock Server] Error creating order: ' + err.message);
|
||||
|
||||
// For demo purposes, if the direct API call fails, fall back to a mock order ID
|
||||
const mockOrderId = 'MOCK_ORDER_' + Math.random().toString(36).substring(2, 15);
|
||||
logResponse('[Mock Server] Using mock order ID for demo: ' + mockOrderId);
|
||||
return mockOrderId;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -205,14 +205,7 @@ return array(
|
|||
},
|
||||
|
||||
'googlepay.url' => static function ( ContainerInterface $container ): string {
|
||||
$path = realpath( __FILE__ );
|
||||
if ( false === $path ) {
|
||||
return '';
|
||||
}
|
||||
return plugins_url(
|
||||
'/modules/ppcp-googlepay/',
|
||||
dirname( $path, 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-googlepay/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
|
||||
'googlepay.sdk_url' => static function ( ContainerInterface $container ): string {
|
||||
|
|
|
@ -17,6 +17,7 @@ use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
|
|||
use WooCommerce\PayPalCommerce\Googlepay\Endpoint\UpdatePaymentDataEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Googlepay\Helper\ApmProductStatus;
|
||||
use WooCommerce\PayPalCommerce\Googlepay\Helper\AvailabilityNotice;
|
||||
use WooCommerce\PayPalCommerce\Settings\SettingsModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
|
||||
|
@ -149,9 +150,11 @@ class GooglepayModule implements ServiceModule, ExtendingModule, ExecutableModul
|
|||
add_action(
|
||||
'woocommerce_blocks_payment_method_type_registration',
|
||||
function( PaymentMethodRegistry $payment_method_registry ) use ( $c, $button ): void {
|
||||
if ( $button->is_enabled() ) {
|
||||
$payment_method_registry->register( $c->get( 'googlepay.blocks-payment-method' ) );
|
||||
if ( SettingsModule::should_use_the_old_ui() && ! $button->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payment_method_registry->register( $c->get( 'googlepay.blocks-payment-method' ) );
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -14,15 +14,7 @@ use WooCommerce\PayPalCommerce\LocalAlternativePaymentMethods\LocalApmProductSta
|
|||
|
||||
return array(
|
||||
'ppcp-local-apms.url' => static function ( ContainerInterface $container ): string {
|
||||
/**
|
||||
* The path cannot be false.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-local-alternative-payment-methods/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-local-alternative-payment-methods/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'ppcp-local-apms.payment-methods' => static function( ContainerInterface $container ): array {
|
||||
return array(
|
||||
|
|
|
@ -123,10 +123,7 @@ return array(
|
|||
},
|
||||
|
||||
'onboarding.url' => static function ( ContainerInterface $container ): string {
|
||||
return plugins_url(
|
||||
'/modules/ppcp-onboarding/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-onboarding/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
|
||||
'onboarding.endpoint.login-seller' => static function ( ContainerInterface $container ) : LoginSellerEndpoint {
|
||||
|
|
|
@ -43,15 +43,7 @@ return array(
|
|||
);
|
||||
},
|
||||
'order-tracking.module.url' => static function ( ContainerInterface $container ): string {
|
||||
/**
|
||||
* The path cannot be false.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-order-tracking/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-order-tracking/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'order-tracking.meta-box.renderer' => static function ( ContainerInterface $container ): MetaBoxRenderer {
|
||||
return new MetaBoxRenderer(
|
||||
|
|
|
@ -14,15 +14,7 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
|||
|
||||
return array(
|
||||
'paylater-block.url' => static function ( ContainerInterface $container ): string {
|
||||
/**
|
||||
* Cannot return false for this path.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-paylater-block/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-paylater-block/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'paylater-block.renderer' => static function (): PayLaterBlockRenderer {
|
||||
return new PayLaterBlockRenderer();
|
||||
|
|
|
@ -101,13 +101,8 @@ class PayLaterBlockModule implements ServiceModule, ExtendingModule, ExecutableM
|
|||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Cannot return false for this path.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
register_block_type(
|
||||
dirname( realpath( __FILE__ ), 2 ),
|
||||
$c->get( 'ppcp.path-to-plugin-folder' ) . 'modules/ppcp-paylater-block/',
|
||||
array(
|
||||
'render_callback' => function ( array $attributes ) use ( $c ) {
|
||||
$renderer = $c->get( 'paylater-block.renderer' );
|
||||
|
|
|
@ -19,17 +19,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
|||
|
||||
return array(
|
||||
'paylater-configurator.url' => static function ( ContainerInterface $container ): string {
|
||||
/**
|
||||
* The return value must not contain a trailing slash.
|
||||
*
|
||||
* Cannot return false for this path.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-paylater-configurator',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-paylater-configurator', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'paylater-configurator.factory.config' => static function ( ContainerInterface $container ): ConfigFactory {
|
||||
return new ConfigFactory();
|
||||
|
|
|
@ -13,15 +13,7 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
|||
|
||||
return array(
|
||||
'paylater-wc-blocks.url' => static function ( ContainerInterface $container ): string {
|
||||
/**
|
||||
* Cannot return false for this path.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-paylater-wc-blocks/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-paylater-wc-blocks/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
|
||||
'paylater-wc-blocks.cart-renderer' => static function ( ContainerInterface $container ): PayLaterWCBlocksRenderer {
|
||||
|
|
|
@ -192,13 +192,10 @@ class PayLaterWCBlocksModule implements ServiceModule, ExtendingModule, Executab
|
|||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cannot return false for this path.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
$path_to_module_js_folder = $c->get( 'ppcp.path-to-plugin-folder' ) . 'modules/ppcp-paylater-wc-blocks/resources/js/';
|
||||
|
||||
register_block_type(
|
||||
dirname( realpath( __FILE__ ), 2 ) . '/resources/js/CartPayLaterMessagesBlock',
|
||||
$path_to_module_js_folder . 'CartPayLaterMessagesBlock',
|
||||
array(
|
||||
'render_callback' => function ( array $attributes ) use ( $c ) {
|
||||
return PayLaterWCBlocksUtils::render_paylater_block(
|
||||
|
@ -211,13 +208,8 @@ class PayLaterWCBlocksModule implements ServiceModule, ExtendingModule, Executab
|
|||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Cannot return false for this path.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
register_block_type(
|
||||
dirname( realpath( __FILE__ ), 2 ) . '/resources/js/CheckoutPayLaterMessagesBlock',
|
||||
$path_to_module_js_folder . 'CheckoutPayLaterMessagesBlock',
|
||||
array(
|
||||
'render_callback' => function ( array $attributes ) use ( $c ) {
|
||||
return PayLaterWCBlocksUtils::render_paylater_block(
|
||||
|
|
|
@ -30,15 +30,7 @@ return array(
|
|||
);
|
||||
},
|
||||
'paypal-subscriptions.module.url' => static function ( ContainerInterface $container ): string {
|
||||
/**
|
||||
* The path cannot be false.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-paypal-subscriptions/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-paypal-subscriptions/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'paypal-subscriptions.renewal-handler' => static function ( ContainerInterface $container ): RenewalHandler {
|
||||
return new RenewalHandler( $container->get( 'woocommerce.logger.woocommerce' ) );
|
||||
|
|
|
@ -87,15 +87,7 @@ return array(
|
|||
);
|
||||
},
|
||||
'save-payment-methods.module.url' => static function ( ContainerInterface $container ): string {
|
||||
/**
|
||||
* The path cannot be false.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-save-payment-methods/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-save-payment-methods/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'save-payment-methods.endpoint.create-setup-token' => static function ( ContainerInterface $container ): CreateSetupToken {
|
||||
return new CreateSetupToken(
|
||||
|
|
|
@ -1,9 +1,50 @@
|
|||
.ppcp--static-value {
|
||||
@include font(13, 26, 400);
|
||||
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.ppcp--static-value-with-copy {
|
||||
display: inline-flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
overflow: visible;
|
||||
|
||||
.ppcp--static-value-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
word-break: break-all;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 37ch;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.ppcp--static-value-with-copy) {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ppcp-copy-button {
|
||||
display: flex;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: $color-gray-700;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: $color-blueberry;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
// Fix the checkbox layout (add gap between checkbox and label).
|
||||
|
|
|
@ -1,9 +1,31 @@
|
|||
import { Action } from '../Elements';
|
||||
import classNames from 'classnames';
|
||||
import CopyButton from '../Elements/CopyButton';
|
||||
|
||||
const ControlStaticValue = ( { value } ) => (
|
||||
<Action>
|
||||
<div className="ppcp--static-value">{ value }</div>
|
||||
</Action>
|
||||
);
|
||||
const ControlStaticValue = ( {
|
||||
value,
|
||||
showCopy = false,
|
||||
copyButtonProps = {},
|
||||
className,
|
||||
...props
|
||||
} ) => {
|
||||
const wrapperClass = classNames( 'ppcp--static-value', {
|
||||
'ppcp--static-value-with-copy': showCopy,
|
||||
'ppcp--has-copy': showCopy,
|
||||
} );
|
||||
|
||||
return (
|
||||
<Action className={ className } { ...props }>
|
||||
{ showCopy ? (
|
||||
<div className={ wrapperClass }>
|
||||
<div className="ppcp--static-value-text">{ value }</div>
|
||||
<CopyButton value={ value } { ...copyButtonProps } />
|
||||
</div>
|
||||
) : (
|
||||
<div className={ wrapperClass }>{ value }</div>
|
||||
) }
|
||||
</Action>
|
||||
);
|
||||
};
|
||||
|
||||
export default ControlStaticValue;
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import { __ } from '@wordpress/i18n';
|
||||
import { speak } from '@wordpress/a11y';
|
||||
import { Tooltip } from '@wordpress/components';
|
||||
import { SVG, Path } from '@wordpress/primitives';
|
||||
import classNames from 'classnames';
|
||||
import { useCopyToClipboard } from '../../../hooks/useCopyToClipboard';
|
||||
|
||||
const COPY_CONFIRMATION_DURATION = 1000;
|
||||
|
||||
/**
|
||||
* Copy button component with tooltip and icon transition
|
||||
* @param {Object} props - Component props
|
||||
* @param {string} props.value - The text value to copy to clipboard
|
||||
* @param {string} [props.className] - Additional CSS class names
|
||||
* @param {string} [props.ariaLabel] - Custom aria-label for the button
|
||||
*/
|
||||
const CopyButton = ( { value, className, ariaLabel, ...props } ) => {
|
||||
const { copy, copied, error } = useCopyToClipboard( {
|
||||
successDuration: COPY_CONFIRMATION_DURATION,
|
||||
} );
|
||||
|
||||
const buttonClass = classNames( 'ppcp-copy-button', className );
|
||||
|
||||
const getTooltipText = () => {
|
||||
if ( copied ) {
|
||||
return __( 'Copied!', 'woocommerce-paypal-payments' );
|
||||
}
|
||||
if ( error ) {
|
||||
return __( 'Failed to copy', 'woocommerce-paypal-payments' );
|
||||
}
|
||||
return __( 'Copy to clipboard', 'woocommerce-paypal-payments' );
|
||||
};
|
||||
|
||||
const handleCopy = async () => {
|
||||
if ( ! value ) {
|
||||
return;
|
||||
}
|
||||
await copy( value );
|
||||
|
||||
if ( copied ) {
|
||||
speak(
|
||||
__( 'Copied to clipboard', 'woocommerce-paypal-payments' ),
|
||||
'assertive'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if ( error ) {
|
||||
speak(
|
||||
__(
|
||||
'Failed to copy to clipboard',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'assertive'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Tooltip
|
||||
text={ getTooltipText() }
|
||||
placement="top"
|
||||
delay={ 100 }
|
||||
hideOnClick={ false }
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={ handleCopy }
|
||||
className={ buttonClass }
|
||||
disabled={ ! value }
|
||||
aria-label={ ariaLabel || getTooltipText() }
|
||||
{ ...props }
|
||||
>
|
||||
{ copied ? <CheckIcon /> : <CopyIcon /> }
|
||||
</button>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const CopyIcon = () => (
|
||||
<SVG
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<Path
|
||||
fillRule="evenodd"
|
||||
d="M16 16v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V9a1 1 0 0 1 1-1h3V5a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1h-3zm2.5-10.5v9H16V9a1 1 0 0 0-1-1H9.5V5.5h9z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</SVG>
|
||||
);
|
||||
|
||||
const CheckIcon = () => (
|
||||
<SVG
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<Path d="M9 16.17L4.83 12L3.41 13.41L9 19L21 7L19.59 5.59L9 16.17Z" />
|
||||
</SVG>
|
||||
);
|
||||
|
||||
export default CopyButton;
|
|
@ -64,10 +64,19 @@ export const getSteps = ( flags ) => {
|
|||
// Casual selling: Unlock the "Personal Account" choice.
|
||||
( step ) => flags.canUseCasualSelling || step.id !== 'business',
|
||||
// Skip payment methods screen.
|
||||
( step ) =>
|
||||
step.id !== 'methods' ||
|
||||
( ! flags.shouldSkipPaymentMethods &&
|
||||
! ( ownBrandOnly && isCasualSeller ) ),
|
||||
( step ) => {
|
||||
if ( step.id !== 'methods' ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const isBrandedBCDC = ownBrandOnly && ! flags.canUseCardPayments;
|
||||
const shouldSkip =
|
||||
flags.shouldSkipPaymentMethods ||
|
||||
isCasualSeller ||
|
||||
isBrandedBCDC;
|
||||
|
||||
return ! shouldSkip;
|
||||
},
|
||||
] );
|
||||
|
||||
const totalStepsCount = steps.length;
|
||||
|
|
|
@ -37,17 +37,23 @@ const ConnectionStatus = () => {
|
|||
title={ __( 'Merchant ID', 'woocommerce-paypal-payments' ) }
|
||||
className="ppcp--no-gap"
|
||||
>
|
||||
<ControlStaticValue value={ merchant.id } />
|
||||
<ControlStaticValue value={ merchant.id } showCopy={ true } />
|
||||
</SettingsBlock>
|
||||
<SettingsBlock
|
||||
title={ __( 'Email address', 'woocommerce-paypal-payments' ) }
|
||||
>
|
||||
<ControlStaticValue value={ merchant.email } />
|
||||
<ControlStaticValue
|
||||
value={ merchant.email }
|
||||
showCopy={ true }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
<SettingsBlock
|
||||
title={ __( 'Client ID', 'woocommerce-paypal-payments' ) }
|
||||
>
|
||||
<ControlStaticValue value={ merchant.clientId } />
|
||||
<ControlStaticValue
|
||||
value={ merchant.clientId }
|
||||
showCopy={ true }
|
||||
/>
|
||||
</SettingsBlock>
|
||||
</SettingsCard>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import { useState, useRef } from '@wordpress/element';
|
||||
|
||||
/**
|
||||
* Custom hook for handling copy to clipboard functionality
|
||||
*
|
||||
* @param {Object} options - Configuration options
|
||||
* @param {number} options.successDuration - How long to show success state (ms)
|
||||
* @return {Object} Copy functionality and state
|
||||
*/
|
||||
export const useCopyToClipboard = ( options = {} ) => {
|
||||
const { successDuration = 1000 } = options;
|
||||
const [ copied, setCopied ] = useState( false );
|
||||
const [ error, setError ] = useState( false );
|
||||
const timerRef = useRef( null );
|
||||
|
||||
const copy = async ( text ) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText( text );
|
||||
|
||||
clearTimeout( timerRef.current );
|
||||
setCopied( true );
|
||||
setError( false );
|
||||
|
||||
timerRef.current = setTimeout(
|
||||
() => setCopied( false ),
|
||||
successDuration
|
||||
);
|
||||
} catch ( err ) {
|
||||
console.error( 'Copy failed:', err );
|
||||
setError( true );
|
||||
setCopied( false );
|
||||
}
|
||||
};
|
||||
|
||||
return { copy, copied, error };
|
||||
};
|
||||
|
||||
export default useCopyToClipboard;
|
|
@ -80,15 +80,7 @@ use WooCommerce\PayPalCommerce\WcGateway\Helper\MerchantDetails;
|
|||
|
||||
$services = array(
|
||||
'settings.url' => static function ( ContainerInterface $container ) : string {
|
||||
/**
|
||||
* The path cannot be false.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-settings/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-settings/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'settings.data.onboarding' => static function ( ContainerInterface $container ) : OnboardingProfile {
|
||||
$can_use_casual_selling = $container->get( 'settings.casual-selling.eligible' );
|
||||
|
@ -369,7 +361,8 @@ $services = array(
|
|||
$merchant_id = $container->get( 'api.partner_merchant_id' );
|
||||
$button_language_choices = $container->get( 'wcgateway.wp-paypal-locales-map' );
|
||||
$partner_attribution = $container->get( 'api.helper.partner-attribution' );
|
||||
return new ScriptDataHandler( $settings, $settings_url, $paylater_is_available, $store_country, $merchant_id, $button_language_choices, $partner_attribution );
|
||||
$path_to_module_assets_folder = $container->get( 'ppcp.path-to-plugin-folder' ) . 'modules/ppcp-settings/assets';
|
||||
return new ScriptDataHandler( $settings, $settings_url, $paylater_is_available, $store_country, $merchant_id, $button_language_choices, $partner_attribution, $path_to_module_assets_folder );
|
||||
},
|
||||
'settings.service.data-migration' => static fn( ContainerInterface $c ): MigrationManager => new MigrationManager(
|
||||
$c->get( 'settings.service.data-migration.general-settings' ),
|
||||
|
@ -417,7 +410,8 @@ $services = array(
|
|||
'settings.data.definition.todos' => static function ( ContainerInterface $container ) : TodosDefinition {
|
||||
return new TodosDefinition(
|
||||
$container->get( 'settings.service.todos_eligibilities' ),
|
||||
$container->get( 'settings.data.general' )
|
||||
$container->get( 'settings.data.general' ),
|
||||
$container->get( 'settings.data.todos' )
|
||||
);
|
||||
},
|
||||
'settings.data.definition.methods' => static function ( ContainerInterface $container ) : PaymentMethodsDefinition {
|
||||
|
|
|
@ -24,6 +24,7 @@ class SwitchSettingsUiEndpoint {
|
|||
|
||||
public const ENDPOINT = 'ppcp-settings-switch-ui';
|
||||
public const OPTION_NAME_SHOULD_USE_OLD_UI = 'woocommerce_ppcp-settings-should-use-old-ui';
|
||||
public const OPTION_NAME_MIGRATION_IS_DONE = 'woocommerce_ppcp-settings-migration-is-done';
|
||||
|
||||
protected RequestData $request_data;
|
||||
protected LoggerInterface $logger;
|
||||
|
@ -70,6 +71,8 @@ class SwitchSettingsUiEndpoint {
|
|||
$this->onboarding_profile->save();
|
||||
|
||||
$this->settings_data_migration->migrate();
|
||||
|
||||
update_option( self::OPTION_NAME_MIGRATION_IS_DONE, true );
|
||||
wp_send_json_success();
|
||||
} catch ( Exception $error ) {
|
||||
wp_send_json_error( array( 'message' => $error->getMessage() ), 500 );
|
||||
|
|
|
@ -136,7 +136,7 @@ class PaymentMethodsDefinition {
|
|||
$gateway = $this->wc_gateways[ $gateway_id ] ?? null;
|
||||
|
||||
$gateway_title = $gateway ? $gateway->get_title() : $title;
|
||||
$gateway_description = $gateway ? $gateway->get_description() : $description;
|
||||
$gateway_description = $gateway->settings['description'] ?? $description;
|
||||
$enabled = $this->settings->is_method_enabled( $gateway_id );
|
||||
$config = array(
|
||||
'id' => $gateway_id,
|
||||
|
@ -159,7 +159,7 @@ class PaymentMethodsDefinition {
|
|||
),
|
||||
'checkoutPageDescription' => array(
|
||||
'type' => 'text',
|
||||
'default' => $gateway ? $gateway->get_description() : '',
|
||||
'default' => $gateway_description,
|
||||
'label' => __( 'Checkout page description', 'woocommerce-paypal-payments' ),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -9,6 +9,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\Settings\Data\Definition;
|
||||
|
||||
use WooCommerce\PayPalCommerce\Settings\Ajax\SwitchSettingsUiEndpoint;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\TodosModel;
|
||||
use WooCommerce\PayPalCommerce\Settings\Service\TodosEligibilityService;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
|
||||
|
||||
|
@ -34,18 +36,23 @@ class TodosDefinition {
|
|||
*/
|
||||
protected GeneralSettings $settings;
|
||||
|
||||
protected TodosModel $todos;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param TodosEligibilityService $eligibilities The todos eligibility service.
|
||||
* @param GeneralSettings $settings The general settings service.
|
||||
* @param TodosModel $todos The todos model instance.
|
||||
*/
|
||||
public function __construct(
|
||||
TodosEligibilityService $eligibilities,
|
||||
GeneralSettings $settings
|
||||
GeneralSettings $settings,
|
||||
TodosModel $todos
|
||||
) {
|
||||
$this->eligibilities = $eligibilities;
|
||||
$this->settings = $settings;
|
||||
$this->todos = $todos;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,7 +63,7 @@ class TodosDefinition {
|
|||
public function get(): array {
|
||||
$eligibility_checks = $this->eligibilities->get_eligibility_checks();
|
||||
|
||||
return array(
|
||||
$todo_items = array(
|
||||
'enable_fastlane' => array(
|
||||
'title' => __( 'Enable Fastlane', 'woocommerce-paypal-payments' ),
|
||||
'description' => __( 'Accelerate your guest checkout with Fastlane by PayPal', 'woocommerce-paypal-payments' ),
|
||||
|
@ -228,5 +235,63 @@ class TodosDefinition {
|
|||
'priority' => 13,
|
||||
),
|
||||
);
|
||||
|
||||
$todo_items['check_settings_after_migration'] = array(
|
||||
'title' => __( "You're now using the new PayPal Payments interface!", 'woocommerce-paypal-payments' ),
|
||||
'description' => __( 'Complete the items below to ensure your payment configuration is optimized for your store.', 'woocommerce-paypal-payments' ),
|
||||
'isEligible' => fn(): bool => $this->is_settings_migration_done() && ! $this->are_all_todos_completed( $todo_items ),
|
||||
'action' => array(
|
||||
'type' => 'tab',
|
||||
'tab' => 'overview',
|
||||
),
|
||||
'priority' => 0,
|
||||
);
|
||||
|
||||
return $todo_items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the settings migration to the new UI has been completed.
|
||||
*
|
||||
* @return bool True if the migration is marked as done, false otherwise.
|
||||
*/
|
||||
protected function is_settings_migration_done(): bool {
|
||||
return '1' === get_option( SwitchSettingsUiEndpoint::OPTION_NAME_MIGRATION_IS_DONE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether all todos have been completed or dismissed appropriately.
|
||||
*
|
||||
* A to-do is considered completed if:
|
||||
* - It's eligible (based on the callable `isEligible`), AND
|
||||
* - It is either:
|
||||
* - A "completeOnClick" type and is present in the completed list, OR
|
||||
* - Not a "completeOnClick" type and is present in the dismissed list.
|
||||
*
|
||||
* @param array $todos The array of to-do definitions.
|
||||
* @return bool True if all to-dos are completed or dismissed as expected, false otherwise.
|
||||
*/
|
||||
protected function are_all_todos_completed( array $todos ): bool {
|
||||
$dismissed = $this->todos->get_dismissed_todos();
|
||||
$completed = $this->todos->get_completed_onclick_todos();
|
||||
|
||||
foreach ( $todos as $id => $todo ) {
|
||||
if ( ! is_callable( $todo['isEligible'] ) || ! call_user_func( $todo['isEligible'] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$is_click_to_complete = $todo['action']['completeOnClick'] ?? false;
|
||||
|
||||
if ( $is_click_to_complete && ! in_array( $id, $completed, true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! $is_click_to_complete && ! in_array( $id, $dismissed, true ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -59,6 +59,8 @@ class ScriptDataHandler {
|
|||
*/
|
||||
protected PartnerAttribution $partner_attribution;
|
||||
|
||||
protected string $path_to_module_assets_folder;
|
||||
|
||||
/**
|
||||
* ScriptDataHandler constructor.
|
||||
*
|
||||
|
@ -69,6 +71,7 @@ class ScriptDataHandler {
|
|||
* @param string $merchant_id The merchant ID.
|
||||
* @param array $button_language_choices The button language choices.
|
||||
* @param PartnerAttribution $partner_attribution The partner attribution object.
|
||||
* @param string $path_to_module_assets_folder The path to mpdule assets folder.
|
||||
*/
|
||||
public function __construct(
|
||||
Settings $settings,
|
||||
|
@ -77,15 +80,17 @@ class ScriptDataHandler {
|
|||
string $store_country,
|
||||
string $merchant_id,
|
||||
array $button_language_choices,
|
||||
PartnerAttribution $partner_attribution
|
||||
PartnerAttribution $partner_attribution,
|
||||
string $path_to_module_assets_folder
|
||||
) {
|
||||
$this->settings = $settings;
|
||||
$this->settings_url = $settings_url;
|
||||
$this->paylater_is_available = $paylater_is_available;
|
||||
$this->store_country = $store_country;
|
||||
$this->merchant_id = $merchant_id;
|
||||
$this->button_language_choices = $button_language_choices;
|
||||
$this->partner_attribution = $partner_attribution;
|
||||
$this->settings = $settings;
|
||||
$this->settings_url = $settings_url;
|
||||
$this->paylater_is_available = $paylater_is_available;
|
||||
$this->store_country = $store_country;
|
||||
$this->merchant_id = $merchant_id;
|
||||
$this->button_language_choices = $button_language_choices;
|
||||
$this->partner_attribution = $partner_attribution;
|
||||
$this->path_to_module_assets_folder = $path_to_module_assets_folder;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -109,7 +114,7 @@ class ScriptDataHandler {
|
|||
*
|
||||
* @psalm-suppress UnresolvableInclude
|
||||
*/
|
||||
$script_asset_file = require dirname( realpath( __FILE__ ) ?: '', 3 ) . '/assets/index.asset.php';
|
||||
$script_asset_file = require $this->path_to_module_assets_folder . '/index.asset.php';
|
||||
|
||||
$module_url = $this->settings_url;
|
||||
|
||||
|
@ -127,12 +132,8 @@ class ScriptDataHandler {
|
|||
'woocommerce-paypal-payments',
|
||||
);
|
||||
|
||||
/**
|
||||
* Require resolves.
|
||||
*
|
||||
* @psalm-suppress UnresolvableInclude
|
||||
*/
|
||||
$style_asset_file = require dirname( realpath( __FILE__ ) ?: '', 3 ) . '/assets/style.asset.php';
|
||||
/** @psalm-suppress UnresolvableInclude */
|
||||
$style_asset_file = require $this->path_to_module_assets_folder . '/style.asset.php';
|
||||
|
||||
wp_register_style(
|
||||
'ppcp-admin-settings',
|
||||
|
|
|
@ -144,12 +144,8 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
static function () use ( $container ) {
|
||||
$module_url = $container->get( 'settings.url' );
|
||||
|
||||
/**
|
||||
* Require resolves.
|
||||
*
|
||||
* @psalm-suppress UnresolvableInclude
|
||||
*/
|
||||
$script_asset_file = require dirname( realpath( __FILE__ ) ?: '', 2 ) . '/assets/switchSettingsUi.asset.php';
|
||||
/** @psalm-suppress UnresolvableInclude */
|
||||
$script_asset_file = require $container->get( 'ppcp.path-to-plugin-folder' ) . 'modules/ppcp-settings/assets/switchSettingsUi.asset.php';
|
||||
|
||||
wp_register_script(
|
||||
'ppcp-switch-settings-ui',
|
||||
|
@ -531,6 +527,17 @@ class SettingsModule implements ServiceModule, ExecutableModule {
|
|||
2
|
||||
);
|
||||
|
||||
add_filter(
|
||||
'woocommerce_paypal_payments_paypal_gateway_icon',
|
||||
function ( string $icon_url ) use ( $container ) {
|
||||
$payment_settings = $container->get( 'settings.data.payment' );
|
||||
assert( $payment_settings instanceof PaymentSettings );
|
||||
|
||||
// If "Show logo" is disabled, return an empty string to hide the icon.
|
||||
return $payment_settings->get_paypal_show_logo() ? $icon_url : '';
|
||||
}
|
||||
);
|
||||
|
||||
add_filter( 'woocommerce_paypal_payments_card_button_gateway_should_register_gateway', '__return_true' );
|
||||
|
||||
add_filter(
|
||||
|
|
|
@ -15,6 +15,8 @@ use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
|
||||
use WooCommerce\PayPalCommerce\Button\Helper\MessagesApply;
|
||||
use WooCommerce\PayPalCommerce\Compat\PPEC\PPECHelper;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\GeneralSettings;
|
||||
use WooCommerce\PayPalCommerce\Settings\SettingsModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ModuleClassNameIdTrait;
|
||||
|
@ -85,6 +87,9 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo
|
|||
|
||||
$subscription_mode_options = $c->get( 'wcgateway.settings.fields.subscriptions_mode_options' );
|
||||
|
||||
/* @var GeneralSettings $general_settings General plugin settings. */
|
||||
$general_settings = $c->get( 'settings.data.general' );
|
||||
|
||||
// Feature flag convention.
|
||||
// phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
|
||||
$items = array(
|
||||
|
@ -96,16 +101,17 @@ class StatusReportModule implements ServiceModule, ExtendingModule, ExecutableMo
|
|||
$this->onboarded( $bearer, $is_connected )
|
||||
),
|
||||
),
|
||||
array(
|
||||
'label' => esc_html__( 'Branded only', 'woocommerce-paypal-payments' ),
|
||||
'exported_label' => 'Branded only',
|
||||
'description' => esc_html__( 'Whether the plugin is in Branded only mode or not.', 'woocommerce-paypal-payments' ),
|
||||
'value' => $this->bool_to_html( $general_settings->own_brand_only() ),
|
||||
),
|
||||
array(
|
||||
'label' => esc_html__( 'New UI active', 'woocommerce-paypal-payments' ),
|
||||
'exported_label' => 'New UI active',
|
||||
'description' => esc_html__( 'Indicates whether the new Settings UI is enabled.', 'woocommerce-paypal-payments' ),
|
||||
'value' => $this->bool_to_html(
|
||||
apply_filters(
|
||||
'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled',
|
||||
'1' === get_option( 'woocommerce-ppcp-is-new-merchant' ) || getenv( 'PCP_SETTINGS_ENABLED' ) === '1'
|
||||
)
|
||||
),
|
||||
'value' => $this->bool_to_html( ! SettingsModule::should_use_the_old_ui() ),
|
||||
),
|
||||
array(
|
||||
'label' => esc_html__( 'Shop country code', 'woocommerce-paypal-payments' ),
|
||||
|
|
|
@ -36,6 +36,7 @@ return array(
|
|||
WebhookRegistrar::KEY,
|
||||
'ppcp_payment_tokens_migration_initialized',
|
||||
SwitchSettingsUiEndpoint::OPTION_NAME_SHOULD_USE_OLD_UI,
|
||||
SwitchSettingsUiEndpoint::OPTION_NAME_MIGRATION_IS_DONE,
|
||||
);
|
||||
},
|
||||
|
||||
|
@ -79,15 +80,7 @@ return array(
|
|||
},
|
||||
|
||||
'uninstall.module-url' => static function ( ContainerInterface $container ): string {
|
||||
/**
|
||||
* The path cannot be false.
|
||||
*
|
||||
* @psalm-suppress PossiblyFalseArgument
|
||||
*/
|
||||
return plugins_url(
|
||||
'/modules/ppcp-uninstall/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-uninstall/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
|
||||
'uninstall.clear-db-assets' => function( ContainerInterface $container ) : ClearDatabaseAssets {
|
||||
|
|
|
@ -13,10 +13,7 @@ use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
|||
|
||||
return array(
|
||||
'vaulting.module-url' => static function ( ContainerInterface $container ): string {
|
||||
return plugins_url(
|
||||
'/modules/ppcp-vaulting/',
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( '/modules/ppcp-vaulting/', $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'vaulting.repository.payment-token' => static function ( ContainerInterface $container ): PaymentTokenRepository {
|
||||
$factory = $container->get( 'api.factory.payment-token' );
|
||||
|
|
|
@ -1353,19 +1353,13 @@ return array(
|
|||
);
|
||||
},
|
||||
'wcgateway.url' => static function ( ContainerInterface $container ): string {
|
||||
return plugins_url(
|
||||
$container->get( 'wcgateway.relative-path' ),
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
);
|
||||
return plugins_url( $container->get( 'wcgateway.relative-path' ), $container->get( 'ppcp.path-to-plugin-main-file' ) );
|
||||
},
|
||||
'wcgateway.relative-path' => static function( ContainerInterface $container ): string {
|
||||
return 'modules/ppcp-wc-gateway/';
|
||||
},
|
||||
'wcgateway.absolute-path' => static function( ContainerInterface $container ): string {
|
||||
return plugin_dir_path(
|
||||
dirname( realpath( __FILE__ ), 3 ) . '/woocommerce-paypal-payments.php'
|
||||
) .
|
||||
$container->get( 'wcgateway.relative-path' );
|
||||
return plugin_dir_path( $container->get( 'ppcp.path-to-plugin-main-file' ) ) . $container->get( 'wcgateway.relative-path' );
|
||||
},
|
||||
'wcgateway.endpoint.return-url' => static function ( ContainerInterface $container ) : ReturnUrlEndpoint {
|
||||
$gateway = $container->get( 'wcgateway.paypal-gateway' );
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue