mirror of
https://github.com/woocommerce/woocommerce-paypal-payments.git
synced 2025-08-30 05:00:51 +08:00
Merge trunk
This commit is contained in:
commit
dad1d4bb35
251 changed files with 8543 additions and 2882 deletions
2
.github/workflows/integration.yml
vendored
2
.github/workflows/integration.yml
vendored
|
@ -12,7 +12,7 @@ jobs:
|
|||
|
||||
name: PHP ${{ matrix.php-versions }} WC ${{ matrix.wc-versions }}
|
||||
steps:
|
||||
- uses: ddev/github-action-setup-ddev@v1
|
||||
- uses: ddev/github-action-setup-ddev@1c7ef18595da42355373cb6d9417a6f44d758b93 # v1.10.1
|
||||
with:
|
||||
autostart: false
|
||||
|
||||
|
|
2
.github/workflows/package-new.yml
vendored
2
.github/workflows/package-new.yml
vendored
|
@ -27,7 +27,7 @@ jobs:
|
|||
|
||||
create_archive:
|
||||
needs: check_version
|
||||
uses: inpsyde/reusable-workflows/.github/workflows/build-plugin-archive.yml@main
|
||||
uses: inpsyde/reusable-workflows/.github/workflows/build-plugin-archive.yml@a9af34f34e95cbe18703198c7e972e97ebcd7473
|
||||
with:
|
||||
PHP_VERSION: 7.4
|
||||
NODE_VERSION: 22
|
||||
|
|
2
.github/workflows/package.yml
vendored
2
.github/workflows/package.yml
vendored
|
@ -25,7 +25,7 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
|
||||
with:
|
||||
php-version: 7.4
|
||||
|
||||
|
|
4
.github/workflows/php.yml
vendored
4
.github/workflows/php.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
uses: shivammathur/setup-php@0f7f1d08e3e32076e51cae65eb0b0c871405b16e # v2.34.1
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
|
||||
|
@ -22,7 +22,7 @@ jobs:
|
|||
run: composer validate
|
||||
|
||||
- name: Install dependencies
|
||||
uses: ramsey/composer-install@v1
|
||||
uses: ramsey/composer-install@a7320a0581dcd0432930c48a0e7ced67e6ec17e8 # v1.3.0
|
||||
with:
|
||||
composer-options: "--prefer-dist"
|
||||
|
||||
|
|
2
.github/workflows/spell-check.yml
vendored
2
.github/workflows/spell-check.yml
vendored
|
@ -14,7 +14,7 @@ jobs:
|
|||
|
||||
- name: Check spelling
|
||||
id: spelling
|
||||
uses: crate-ci/typos@v1.30.2
|
||||
uses: crate-ci/typos@7bc041cbb7ca9167c9e0e4ccbb26f48eb0f9d4e0 # v1.30.2
|
||||
with:
|
||||
# Path to config file
|
||||
config: .github/workflows-config/typos.toml
|
||||
|
|
|
@ -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,5 +1,39 @@
|
|||
*** Changelog ***
|
||||
|
||||
= 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
|
||||
* Enhancement - Improved PHP 8.4 compatibility #3534
|
||||
* Fix - `INVALID_REQUEST` error due to wrong `landing_page` value after upgrade to 3.0.7 #3521
|
||||
* Fix - Incorrect Amount via Express Payment for WooCommerce Product Bundles #3516
|
||||
* Fix - Onboarding failed via "Connect to PayPal" in new UI due to race condition #3385
|
||||
* Fix - Fatal error when PayPal Payments is active without WooCommerce #3502
|
||||
* Fix - PayPal Subscription transaction failed in various scenarios #3515
|
||||
* Fix - Rounding differences potentially lead to order failure (author @luzat) #3373
|
||||
* Fix - Google Pay payment on block checkout may fail when ACDC is default payment selection #3506
|
||||
* Fix - Product Prices Disappear in some cases when WooCommerce Subscriptions is active #3519
|
||||
|
||||
= 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.
|
||||
* Enhancement - Add Contact Module feature
|
||||
* Enhancement - Add WooCommerce Tracks integration
|
||||
* Enhancement - Onboarding notification for Firefox browser #3433
|
||||
* Enhancement - Reset BN code on plugin uninstall #3471
|
||||
* Enhancement - Add "Stay updated with PayPal" option in the old and new settings UI #3430
|
||||
* Enhancement - Add French Territories to the supported ACDC countries list #3438
|
||||
* Enhancement - Auto-enable logging during onboarding #3369
|
||||
* Fix - DUPLICATE_INVOICE_ID in Sandbox due to missing invoice prefix #3435
|
||||
* Fix - Subscription product could not be unlinked from PayPal Subscription #3429
|
||||
* Fix - PayPal button greyed out on single product page for variable products with >2 attributes #3395
|
||||
* Fix - APMs automatically enabled despite selecting "No, ..." during onboarding #3362
|
||||
* Fix - Ditch items logic does not work when using saved card payment #3476
|
||||
* Fix - billing-agreements endpoint called too frequently when not enabled for Reference Transactions #3459
|
||||
|
||||
= 3.0.6 - 2025-05-27 =
|
||||
* Enhancement - Implement 3D secure check for Google Pay #3163
|
||||
* Enhancement - Add options for "Disable Credit Cards" and "Language" #3226
|
||||
|
|
|
@ -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": {
|
||||
|
|
787
composer.lock
generated
787
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
11
modules.php
11
modules.php
|
@ -33,6 +33,7 @@ return function ( string $root_dir ): iterable {
|
|||
( require "$modules_dir/ppcp-blocks/module.php" )(),
|
||||
( require "$modules_dir/ppcp-paypal-subscriptions/module.php" )(),
|
||||
( require "$modules_dir/ppcp-local-alternative-payment-methods/module.php" )(),
|
||||
( require "$modules_dir/ppcp-settings/module.php" )(),
|
||||
);
|
||||
// phpcs:disable WordPress.NamingConventions.ValidHookName.UseUnderscores
|
||||
|
||||
|
@ -91,15 +92,5 @@ return function ( string $root_dir ): iterable {
|
|||
$modules[] = ( require "$modules_dir/ppcp-axo-block/module.php" )();
|
||||
}
|
||||
|
||||
$show_new_ux = '1' === get_option( 'woocommerce-ppcp-is-new-merchant' );
|
||||
$preview_new_ux = '1' === getenv( 'PCP_SETTINGS_ENABLED' );
|
||||
|
||||
if ( apply_filters(
|
||||
'woocommerce.feature-flags.woocommerce_paypal_payments.settings_enabled',
|
||||
$show_new_ux || $preview_new_ux
|
||||
) ) {
|
||||
$modules[] = ( require "$modules_dir/ppcp-settings/module.php" )();
|
||||
}
|
||||
|
||||
return $modules;
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -80,7 +80,7 @@ class Renderer implements RendererInterface {
|
|||
|
||||
printf(
|
||||
'<div class="notice notice-%s %s" %s%s><p>%s</p></div>',
|
||||
$message->type(),
|
||||
esc_attr( $message->type() ),
|
||||
( $message->is_dismissible() ) ? 'is-dismissible' : '',
|
||||
( $message->wrapper() ? sprintf( 'data-ppcp-wrapper="%s"', esc_attr( $message->wrapper() ) ) : '' ),
|
||||
// Use `empty()` in condition, to avoid false phpcs warning.
|
||||
|
|
|
@ -9,45 +9,33 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ClientCredentials;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\SdkClientToken;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\UserIdToken;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
|
||||
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingSubscriptions;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\CatalogProducts;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingPlans;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory;
|
||||
use WooCommerce\PayPalCommerce\Session\SessionHandler;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\PayPalBearer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\BillingAgreementsEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\IdentityToken;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\LoginSeller;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\OrderEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\Orders;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnerReferrals;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentMethodTokensEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentsEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokenEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PaymentTokensEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\WebhookEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\AddressFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\AmountFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\AuthorizationFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\BillingCycleFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\CaptureFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\CardAuthenticationResultFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ContactPreferenceFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ExchangeRateFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\FraudProcessorResponseFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ItemFactory;
|
||||
|
@ -56,33 +44,46 @@ use WooCommerce\PayPalCommerce\ApiClient\Factory\OrderFactory;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PatchCollectionFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayeeFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PayerFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentPreferencesFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentsFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenActionLinksFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PaymentTokenFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlanFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PlatformFeeFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ProductFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\PurchaseUnitFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\RefundPayerFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerPayableBreakdownFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerReceivableBreakdownFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\SellerStatusFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingOptionFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ReturnUrlFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ShippingPreferenceFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookEventFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\WebhookFactory;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\ReferenceTransactionStatus;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\Cache;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\CurrencyGetter;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\DccApplies;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\FailureRegistry;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderHelper;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\OrderTransient;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\PartnerAttribution;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\CustomerRepository;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\OrderRepository;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\PartnerReferralsData;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Repository\PayeeRepository;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\ConnectBearer;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
|
||||
use WooCommerce\PayPalCommerce\Settings\Enum\InstallationPathEnum;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Factory\ContactPreferenceFactory;
|
||||
use WooCommerce\PayPalCommerce\Common\Pattern\SingletonDecorator;
|
||||
use WooCommerce\PayPalCommerce\Session\SessionHandler;
|
||||
use WooCommerce\PayPalCommerce\Settings\Data\SettingsModel;
|
||||
use WooCommerce\PayPalCommerce\Settings\Enum\InstallationPathEnum;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Psr\Container\ContainerInterface;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\EnvironmentConfig;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Settings\Settings;
|
||||
|
||||
return array(
|
||||
'api.host' => static function( ContainerInterface $container ) : string {
|
||||
|
@ -274,13 +275,10 @@ return array(
|
|||
$container->get( 'woocommerce.logger.woocommerce' )
|
||||
);
|
||||
},
|
||||
'api.endpoint.billing-agreements' => static function ( ContainerInterface $container ): BillingAgreementsEndpoint {
|
||||
return new BillingAgreementsEndpoint(
|
||||
$container->get( 'api.host' ),
|
||||
$container->get( 'api.bearer' ),
|
||||
$container->get( 'woocommerce.logger.woocommerce' )
|
||||
);
|
||||
},
|
||||
'api.reference-transaction-status' => static fn ( ContainerInterface $container ): ReferenceTransactionStatus => new ReferenceTransactionStatus(
|
||||
$container->get( 'api.endpoint.partners' ),
|
||||
$container->get( 'api.reference-transaction-status-cache' )
|
||||
),
|
||||
'api.endpoint.catalog-products' => static function ( ContainerInterface $container ): CatalogProducts {
|
||||
return new CatalogProducts(
|
||||
$container->get( 'api.host' ),
|
||||
|
@ -413,6 +411,9 @@ return array(
|
|||
$container->get( 'api.factory.shipping-option' )
|
||||
);
|
||||
},
|
||||
'api.factory.return-url' => static function ( ContainerInterface $container ): ReturnUrlFactory {
|
||||
return new ReturnUrlFactory();
|
||||
},
|
||||
'api.factory.shipping-preference' => static function ( ContainerInterface $container ): ShippingPreferenceFactory {
|
||||
return new ShippingPreferenceFactory();
|
||||
},
|
||||
|
@ -783,6 +784,11 @@ return array(
|
|||
'amex' => array( 'JPY' ),
|
||||
'jcb' => array( 'JPY' ),
|
||||
),
|
||||
'YT' => $mastercard_visa_amex, // Mayotte.
|
||||
'RE' => $mastercard_visa_amex, // Reunion.
|
||||
'GP' => $mastercard_visa_amex, // Guadelope.
|
||||
'GF' => $mastercard_visa_amex, // French Guiana.
|
||||
'MQ' => $mastercard_visa_amex, // Martinique.
|
||||
)
|
||||
);
|
||||
},
|
||||
|
@ -870,6 +876,9 @@ return array(
|
|||
'api.user-id-token-cache' => static function( ContainerInterface $container ): Cache {
|
||||
return new Cache( 'ppcp-id-token-cache' );
|
||||
},
|
||||
'api.reference-transaction-status-cache' => static function( ContainerInterface $container ): Cache {
|
||||
return new Cache( 'ppcp-reference-transaction-status-cache' );
|
||||
},
|
||||
'api.user-id-token' => static function( ContainerInterface $container ): UserIdToken {
|
||||
return new UserIdToken(
|
||||
$container->get( 'api.host' ),
|
||||
|
|
|
@ -1,147 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* The billing agreements endpoint.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Endpoint
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Endpoint;
|
||||
|
||||
use Exception;
|
||||
use stdClass;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Authentication\Bearer;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\PayPalApiException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
/**
|
||||
* Class BillingAgreementsEndpoint
|
||||
*/
|
||||
class BillingAgreementsEndpoint {
|
||||
use RequestTrait;
|
||||
|
||||
/**
|
||||
* The host.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $host;
|
||||
|
||||
/**
|
||||
* The bearer.
|
||||
*
|
||||
* @var Bearer
|
||||
*/
|
||||
private $bearer;
|
||||
|
||||
/**
|
||||
* The logger.
|
||||
*
|
||||
* @var LoggerInterface
|
||||
*/
|
||||
private $logger;
|
||||
|
||||
/**
|
||||
* BillingAgreementsEndpoint constructor.
|
||||
*
|
||||
* @param string $host The host.
|
||||
* @param Bearer $bearer The bearer.
|
||||
* @param LoggerInterface $logger The logger.
|
||||
*/
|
||||
public function __construct(
|
||||
string $host,
|
||||
Bearer $bearer,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->host = $host;
|
||||
$this->bearer = $bearer;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a billing agreement token.
|
||||
*
|
||||
* @param string $description The description.
|
||||
* @param string $return_url The return URL.
|
||||
* @param string $cancel_url The cancel URL.
|
||||
*
|
||||
* @throws RuntimeException If the request fails.
|
||||
* @throws PayPalApiException If the request fails.
|
||||
*/
|
||||
public function create_token( string $description, string $return_url, string $cancel_url ): stdClass {
|
||||
$data = array(
|
||||
'description' => $description,
|
||||
'payer' => array(
|
||||
'payment_method' => 'PAYPAL',
|
||||
),
|
||||
'plan' => array(
|
||||
'type' => 'MERCHANT_INITIATED_BILLING',
|
||||
'merchant_preferences' => array(
|
||||
'return_url' => $return_url,
|
||||
'cancel_url' => $cancel_url,
|
||||
'skip_shipping_address' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$bearer = $this->bearer->bearer();
|
||||
$url = trailingslashit( $this->host ) . 'v1/billing-agreements/agreement-tokens';
|
||||
$args = array(
|
||||
'method' => 'POST',
|
||||
'headers' => array(
|
||||
'Authorization' => 'Bearer ' . $bearer->token(),
|
||||
'Content-Type' => 'application/json',
|
||||
),
|
||||
'body' => wp_json_encode( $data ),
|
||||
);
|
||||
$response = $this->request( $url, $args );
|
||||
|
||||
if ( is_wp_error( $response ) || ! is_array( $response ) ) {
|
||||
throw new RuntimeException( 'Not able to create a billing agreement token.' );
|
||||
}
|
||||
|
||||
$json = json_decode( $response['body'] );
|
||||
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
||||
if ( 201 !== $status_code ) {
|
||||
throw new PayPalApiException(
|
||||
$json,
|
||||
$status_code
|
||||
);
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if reference transactions are enabled in account.
|
||||
*
|
||||
* @throws RuntimeException If the request fails (no auth, no connection, etc.).
|
||||
*/
|
||||
public function reference_transaction_enabled(): bool {
|
||||
try {
|
||||
if ( wc_string_to_bool( get_transient( 'ppcp_reference_transaction_enabled' ) ) === true ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->is_request_logging_enabled = false;
|
||||
|
||||
try {
|
||||
$this->create_token(
|
||||
'Checking if reference transactions are enabled',
|
||||
'https://example.com/return',
|
||||
'https://example.com/cancel'
|
||||
);
|
||||
} finally {
|
||||
$this->is_request_logging_enabled = true;
|
||||
}
|
||||
|
||||
set_transient( 'ppcp_reference_transaction_enabled', true, MONTH_IN_SECONDS );
|
||||
return true;
|
||||
} catch ( Exception $exception ) {
|
||||
set_transient( 'ppcp_reference_transaction_enabled', false, HOUR_IN_SECONDS );
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -176,10 +176,10 @@ class OrderEndpoint {
|
|||
public function create(
|
||||
array $items,
|
||||
string $shipping_preference,
|
||||
Payer $payer = null,
|
||||
?Payer $payer = null,
|
||||
string $payment_method = '',
|
||||
array $request_data = array(),
|
||||
PaymentSource $payment_source = null
|
||||
?PaymentSource $payment_source = null
|
||||
): Order {
|
||||
$bearer = $this->bearer->bearer();
|
||||
$data = array(
|
||||
|
@ -444,9 +444,7 @@ class OrderEndpoint {
|
|||
}
|
||||
$response = $this->request( $url, $args );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$error = new RuntimeException(
|
||||
__( 'Could not retrieve order.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
$error = new RuntimeException( 'Could not retrieve order.' );
|
||||
$this->logger->warning( $error->getMessage() );
|
||||
|
||||
throw $error;
|
||||
|
@ -456,7 +454,7 @@ class OrderEndpoint {
|
|||
|
||||
if ( 404 === $status_code || empty( $response['body'] ) ) {
|
||||
$error = new RuntimeException(
|
||||
__( 'Could not retrieve order.', 'woocommerce-paypal-payments' ),
|
||||
'Could not retrieve order.',
|
||||
404
|
||||
);
|
||||
$this->logger->warning(
|
||||
|
@ -585,9 +583,6 @@ class OrderEndpoint {
|
|||
$data = array(
|
||||
'payment_source' => $payment_source,
|
||||
'processing_instruction' => 'ORDER_COMPLETE_ON_PAYMENT_APPROVAL',
|
||||
'application_context' => array(
|
||||
'locale' => 'es-MX',
|
||||
),
|
||||
);
|
||||
|
||||
$args = array(
|
||||
|
|
|
@ -113,9 +113,7 @@ class WebhookEndpoint {
|
|||
$response = $this->request( $url, $args );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Not able to create a webhook.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'Not able to create a webhook.' );
|
||||
}
|
||||
|
||||
$json = json_decode( $response['body'] );
|
||||
|
@ -151,9 +149,7 @@ class WebhookEndpoint {
|
|||
$response = $this->request( $url, $args );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Not able to load webhooks list.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'Not able to load webhooks list.' );
|
||||
}
|
||||
|
||||
$json = json_decode( $response['body'] );
|
||||
|
@ -195,9 +191,7 @@ class WebhookEndpoint {
|
|||
$response = $this->request( $url, $args );
|
||||
|
||||
if ( $response instanceof WP_Error ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Not able to delete the webhook.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'Not able to delete the webhook.' );
|
||||
}
|
||||
|
||||
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
||||
|
@ -250,9 +244,7 @@ class WebhookEndpoint {
|
|||
$response = $this->request( $url, $args );
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Not able to simulate webhook.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'Not able to simulate webhook.' );
|
||||
}
|
||||
$json = json_decode( $response['body'] );
|
||||
$status_code = (int) wp_remote_retrieve_response_code( $response );
|
||||
|
@ -312,9 +304,7 @@ class WebhookEndpoint {
|
|||
);
|
||||
$response = $this->request( $url, $args );
|
||||
if ( is_wp_error( $response ) ) {
|
||||
$error = new RuntimeException(
|
||||
__( 'Not able to verify webhook event.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
$error = new RuntimeException( 'Not able to verify webhook event.' );
|
||||
$this->logger->log(
|
||||
'warning',
|
||||
$error->getMessage(),
|
||||
|
@ -340,9 +330,7 @@ class WebhookEndpoint {
|
|||
public function verify_current_request_for_webhook( Webhook $webhook ): bool {
|
||||
|
||||
if ( ! $webhook->id() ) {
|
||||
$error = new RuntimeException(
|
||||
__( 'Not a valid webhook to verify.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
$error = new RuntimeException( 'Not a valid webhook to verify.' );
|
||||
$this->logger->log( 'warning', $error->getMessage(), array( 'webhook' => $webhook ) );
|
||||
throw $error;
|
||||
}
|
||||
|
@ -369,11 +357,7 @@ class WebhookEndpoint {
|
|||
|
||||
$error = new RuntimeException(
|
||||
sprintf(
|
||||
// translators: %s is the headers key.
|
||||
__(
|
||||
'Not a valid webhook event. Header %s is missing',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
'Not a valid webhook event. Header %s is missing',
|
||||
$key
|
||||
)
|
||||
);
|
||||
|
|
|
@ -41,7 +41,7 @@ class Amount {
|
|||
* @param Money $money The money.
|
||||
* @param AmountBreakdown|null $breakdown The breakdown.
|
||||
*/
|
||||
public function __construct( Money $money, AmountBreakdown $breakdown = null ) {
|
||||
public function __construct( Money $money, ?AmountBreakdown $breakdown = null ) {
|
||||
$this->money = $money;
|
||||
$this->breakdown = $breakdown;
|
||||
}
|
||||
|
|
|
@ -62,8 +62,7 @@ class AuthorizationStatus {
|
|||
if ( ! in_array( $status, self::VALID_STATUS, true ) ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
// translators: %s is the current status.
|
||||
__( '%s is not a valid status', 'woocommerce-paypal-payments' ),
|
||||
'%s is not a valid status',
|
||||
$status
|
||||
)
|
||||
);
|
||||
|
|
|
@ -175,6 +175,10 @@ class ExperienceContext {
|
|||
* @param string|null $new_value The value to set.
|
||||
*/
|
||||
public function with_landing_page( ?string $new_value ): ExperienceContext {
|
||||
if ( $new_value && strtoupper( $new_value ) === 'BILLING' ) {
|
||||
$new_value = self::LANDING_PAGE_GUEST_CHECKOUT;
|
||||
}
|
||||
|
||||
$obj = clone $this;
|
||||
|
||||
$obj->landing_page = $new_value;
|
||||
|
|
|
@ -114,13 +114,13 @@ class Item {
|
|||
Money $unit_amount,
|
||||
int $quantity,
|
||||
string $description = '',
|
||||
Money $tax = null,
|
||||
?Money $tax = null,
|
||||
string $sku = '',
|
||||
string $category = 'PHYSICAL_GOODS',
|
||||
string $url = '',
|
||||
string $image_url = '',
|
||||
float $tax_rate = 0,
|
||||
string $cart_item_key = null
|
||||
?string $cart_item_key = null
|
||||
) {
|
||||
|
||||
$this->name = $name;
|
||||
|
|
|
@ -9,6 +9,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
|
||||
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* Class Order
|
||||
*/
|
||||
|
@ -25,7 +27,7 @@ class Order {
|
|||
/**
|
||||
* The create time.
|
||||
*
|
||||
* @var \DateTime|null
|
||||
* @var DateTime|null
|
||||
*/
|
||||
private $create_time;
|
||||
|
||||
|
@ -60,7 +62,7 @@ class Order {
|
|||
/**
|
||||
* The update time.
|
||||
*
|
||||
* @var \DateTime|null
|
||||
* @var DateTime|null
|
||||
*/
|
||||
private $update_time;
|
||||
|
||||
|
@ -70,6 +72,10 @@ class Order {
|
|||
* @var PaymentSource|null
|
||||
*/
|
||||
private $payment_source;
|
||||
/**
|
||||
* @var mixed|null
|
||||
*/
|
||||
private $links;
|
||||
|
||||
/**
|
||||
* Order constructor.
|
||||
|
@ -82,18 +88,19 @@ class Order {
|
|||
* @param PaymentSource|null $payment_source The payment source.
|
||||
* @param Payer|null $payer The payer.
|
||||
* @param string $intent The intent.
|
||||
* @param \DateTime|null $create_time The create time.
|
||||
* @param \DateTime|null $update_time The update time.
|
||||
* @param DateTime|null $create_time The create time.
|
||||
* @param DateTime|null $update_time The update time.
|
||||
*/
|
||||
public function __construct(
|
||||
string $id,
|
||||
array $purchase_units,
|
||||
OrderStatus $order_status,
|
||||
PaymentSource $payment_source = null,
|
||||
Payer $payer = null,
|
||||
?PaymentSource $payment_source = null,
|
||||
?Payer $payer = null,
|
||||
string $intent = 'CAPTURE',
|
||||
\DateTime $create_time = null,
|
||||
\DateTime $update_time = null
|
||||
?DateTime $create_time = null,
|
||||
?DateTime $update_time = null,
|
||||
$links = null
|
||||
) {
|
||||
|
||||
$this->id = $id;
|
||||
|
@ -104,6 +111,7 @@ class Order {
|
|||
$this->create_time = $create_time;
|
||||
$this->update_time = $update_time;
|
||||
$this->payment_source = $payment_source;
|
||||
$this->links = $links;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -118,7 +126,7 @@ class Order {
|
|||
/**
|
||||
* Returns the create time.
|
||||
*
|
||||
* @return \DateTime|null
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function create_time() {
|
||||
return $this->create_time;
|
||||
|
@ -127,7 +135,7 @@ class Order {
|
|||
/**
|
||||
* Returns the update time.
|
||||
*
|
||||
* @return \DateTime|null
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function update_time() {
|
||||
return $this->update_time;
|
||||
|
@ -179,6 +187,15 @@ class Order {
|
|||
return $this->payment_source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the links.
|
||||
*
|
||||
* @return mixed|null
|
||||
*/
|
||||
public function links() {
|
||||
return $this->links;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the object as array.
|
||||
*
|
||||
|
@ -206,6 +223,10 @@ class Order {
|
|||
$order['update_time'] = $this->update_time()->format( 'Y-m-d\TH:i:sO' );
|
||||
}
|
||||
|
||||
if ( $this->links ) {
|
||||
$order['links'] = $this->links();
|
||||
}
|
||||
|
||||
return $order;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,8 +51,7 @@ class OrderStatus {
|
|||
if ( ! in_array( $status, self::VALID_STATUS, true ) ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
// translators: %s is the current status.
|
||||
__( '%s is not a valid status', 'woocommerce-paypal-payments' ),
|
||||
'%s is not a valid status',
|
||||
$status
|
||||
)
|
||||
);
|
||||
|
|
|
@ -9,6 +9,8 @@ declare(strict_types=1);
|
|||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Entity;
|
||||
|
||||
use DateTime;
|
||||
|
||||
/**
|
||||
* Class Payer
|
||||
* The customer who sends the money.
|
||||
|
@ -39,7 +41,7 @@ class Payer {
|
|||
/**
|
||||
* The birth date.
|
||||
*
|
||||
* @var \DateTime|null
|
||||
* @var DateTime|null
|
||||
*/
|
||||
private $birthdate;
|
||||
|
||||
|
@ -71,7 +73,7 @@ class Payer {
|
|||
* @param string $email_address The email.
|
||||
* @param string $payer_id The payer id.
|
||||
* @param Address|null $address The address.
|
||||
* @param \DateTime|null $birthdate The birth date.
|
||||
* @param DateTime|null $birthdate The birth date.
|
||||
* @param PhoneWithType|null $phone The phone.
|
||||
* @param PayerTaxInfo|null $tax_info The tax info.
|
||||
*/
|
||||
|
@ -79,10 +81,10 @@ class Payer {
|
|||
?PayerName $name,
|
||||
string $email_address,
|
||||
string $payer_id,
|
||||
Address $address = null,
|
||||
\DateTime $birthdate = null,
|
||||
PhoneWithType $phone = null,
|
||||
PayerTaxInfo $tax_info = null
|
||||
?Address $address = null,
|
||||
?DateTime $birthdate = null,
|
||||
?PhoneWithType $phone = null,
|
||||
?PayerTaxInfo $tax_info = null
|
||||
) {
|
||||
|
||||
$this->name = $name;
|
||||
|
@ -133,7 +135,7 @@ class Payer {
|
|||
/**
|
||||
* Returns the birth date.
|
||||
*
|
||||
* @return \DateTime|null
|
||||
* @return DateTime|null
|
||||
*/
|
||||
public function birthdate() {
|
||||
return $this->birthdate;
|
||||
|
|
|
@ -51,8 +51,7 @@ class PayerTaxInfo {
|
|||
if ( ! in_array( $type, self::VALID_TYPES, true ) ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
// translators: %s is the current type.
|
||||
__( '%s is not a valid tax type.', 'woocommerce-paypal-payments' ),
|
||||
'%s is not a valid tax type.',
|
||||
$type
|
||||
)
|
||||
);
|
||||
|
|
|
@ -50,9 +50,7 @@ class PaymentToken {
|
|||
*/
|
||||
public function __construct( string $id, stdClass $source, string $type = self::TYPE_PAYMENT_METHOD_TOKEN ) {
|
||||
if ( ! in_array( $type, self::get_valid_types(), true ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Not a valid payment source type.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'Not a valid payment source type.' );
|
||||
}
|
||||
$this->id = $id;
|
||||
$this->type = $type;
|
||||
|
|
|
@ -109,13 +109,13 @@ class PurchaseUnit {
|
|||
public function __construct(
|
||||
Amount $amount,
|
||||
array $items = array(),
|
||||
Shipping $shipping = null,
|
||||
?Shipping $shipping = null,
|
||||
string $reference_id = 'default',
|
||||
string $description = '',
|
||||
string $custom_id = '',
|
||||
string $invoice_id = '',
|
||||
string $soft_descriptor = '',
|
||||
Payments $payments = null
|
||||
?Payments $payments = null
|
||||
) {
|
||||
|
||||
$this->amount = $amount;
|
||||
|
|
|
@ -54,7 +54,7 @@ class RefundCapture {
|
|||
Capture $capture,
|
||||
string $invoice_id,
|
||||
string $note_to_payer = '',
|
||||
Amount $amount = null
|
||||
?Amount $amount = null
|
||||
) {
|
||||
$this->capture = $capture;
|
||||
$this->invoice_id = $invoice_id;
|
||||
|
|
|
@ -17,44 +17,50 @@ class Shipping {
|
|||
/**
|
||||
* The name.
|
||||
*
|
||||
* @var string
|
||||
* @var string|null
|
||||
*/
|
||||
private $name;
|
||||
private ?string $name;
|
||||
|
||||
/**
|
||||
* The address.
|
||||
*
|
||||
* @var Address
|
||||
* @var Address|null
|
||||
*/
|
||||
private $address;
|
||||
private ?Address $address;
|
||||
|
||||
/**
|
||||
* Custom contact email address, usually added via the Contact Module.
|
||||
*/
|
||||
private ?string $email_address = null;
|
||||
private ?string $email_address;
|
||||
|
||||
/**
|
||||
* Custom contact phone number, usually added via the Contact Module.
|
||||
*/
|
||||
private ?Phone $phone_number = null;
|
||||
private ?Phone $phone_number;
|
||||
|
||||
/**
|
||||
* Shipping methods.
|
||||
*
|
||||
* @var ShippingOption[]
|
||||
*/
|
||||
private $options;
|
||||
private array $options;
|
||||
|
||||
/**
|
||||
* Shipping constructor.
|
||||
*
|
||||
* @param string $name The name.
|
||||
* @param Address $address The address.
|
||||
* @param string|null $name The name.
|
||||
* @param Address|null $address The address.
|
||||
* @param string|null $email_address Contact email.
|
||||
* @param Phone|null $phone_number Contact phone.
|
||||
* @param ShippingOption[] $options Shipping methods.
|
||||
*/
|
||||
public function __construct( string $name, Address $address, string $email_address = null, Phone $phone_number = null, array $options = array() ) {
|
||||
public function __construct(
|
||||
string $name = null,
|
||||
Address $address = null,
|
||||
?string $email_address = null,
|
||||
?Phone $phone_number = null,
|
||||
array $options = array()
|
||||
) {
|
||||
$this->name = $name;
|
||||
$this->address = $address;
|
||||
$this->email_address = $email_address;
|
||||
|
@ -65,18 +71,18 @@ class Shipping {
|
|||
/**
|
||||
* Returns the name.
|
||||
*
|
||||
* @return string
|
||||
* @return null|string
|
||||
*/
|
||||
public function name(): string {
|
||||
public function name(): ?string {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the shipping address.
|
||||
*
|
||||
* @return Address
|
||||
* @return null|Address
|
||||
*/
|
||||
public function address(): Address {
|
||||
public function address(): ?Address {
|
||||
return $this->address;
|
||||
}
|
||||
|
||||
|
@ -113,19 +119,26 @@ class Shipping {
|
|||
* @return array
|
||||
*/
|
||||
public function to_array(): array {
|
||||
$result = array(
|
||||
'name' => array(
|
||||
'full_name' => $this->name(),
|
||||
),
|
||||
'address' => $this->address()->to_array(),
|
||||
);
|
||||
$result = array();
|
||||
|
||||
$name = $this->name();
|
||||
if ( $name ) {
|
||||
$result['name'] = array(
|
||||
'full_name' => $name,
|
||||
);
|
||||
}
|
||||
|
||||
$address = $this->address();
|
||||
if ( $address ) {
|
||||
$result['address'] = $address->to_array();
|
||||
}
|
||||
|
||||
$contact_email = $this->email_address();
|
||||
$contact_phone = $this->phone_number();
|
||||
|
||||
if ( $contact_email ) {
|
||||
$result['email_address'] = $contact_email;
|
||||
}
|
||||
|
||||
$contact_phone = $this->phone_number();
|
||||
if ( $contact_phone ) {
|
||||
$result['phone_number'] = $contact_phone->to_array();
|
||||
}
|
||||
|
@ -138,6 +151,7 @@ class Shipping {
|
|||
$this->options
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,9 +36,9 @@ class PayPalApiException extends RuntimeException {
|
|||
* @param stdClass|null $response The JSON object.
|
||||
* @param int $status_code The HTTP status code.
|
||||
*/
|
||||
public function __construct( stdClass $response = null, int $status_code = 0 ) {
|
||||
public function __construct( ?stdClass $response = null, int $status_code = 0 ) {
|
||||
if ( is_null( $response ) ) {
|
||||
$response = new \stdClass();
|
||||
$response = new stdClass();
|
||||
}
|
||||
if ( ! isset( $response->message ) ) {
|
||||
$response->message = sprintf(
|
||||
|
@ -63,7 +63,7 @@ class PayPalApiException extends RuntimeException {
|
|||
/**
|
||||
* The JSON response object.
|
||||
*
|
||||
* @var \stdClass $response
|
||||
* @var stdClass $response
|
||||
*/
|
||||
$this->response = $response;
|
||||
$this->status_code = $status_code;
|
||||
|
|
|
@ -198,12 +198,15 @@ class AmountFactory {
|
|||
/**
|
||||
* Returns an Amount object based off a PayPal Response.
|
||||
*
|
||||
* @param \stdClass $data The JSON object.
|
||||
* @param mixed $data The JSON object.
|
||||
*
|
||||
* @return Amount
|
||||
* @throws RuntimeException When JSON object is malformed.
|
||||
* @return Amount|null
|
||||
*/
|
||||
public function from_paypal_response( \stdClass $data ): Amount {
|
||||
public function from_paypal_response( $data ) {
|
||||
if ( null === $data || ! $data instanceof \stdClass ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$money = $this->money_factory->from_paypal_response( $data );
|
||||
$breakdown = ( isset( $data->breakdown ) ) ? $this->break_down( $data->breakdown ) : null;
|
||||
return new Amount( $money, $breakdown );
|
||||
|
@ -242,8 +245,7 @@ class AmountFactory {
|
|||
if ( ! isset( $item->value ) || ! is_numeric( $item->value ) ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
// translators: %s is the current breakdown key.
|
||||
__( 'No value given for breakdown %s', 'woocommerce-paypal-payments' ),
|
||||
'No value given for breakdown %s',
|
||||
$key
|
||||
)
|
||||
);
|
||||
|
@ -251,8 +253,7 @@ class AmountFactory {
|
|||
if ( ! isset( $item->currency_code ) ) {
|
||||
throw new RuntimeException(
|
||||
sprintf(
|
||||
// translators: %s is the current breakdown key.
|
||||
__( 'No currency given for breakdown %s', 'woocommerce-paypal-payments' ),
|
||||
'No currency given for breakdown %s',
|
||||
$key
|
||||
)
|
||||
);
|
||||
|
|
|
@ -45,15 +45,11 @@ class AuthorizationFactory {
|
|||
*/
|
||||
public function from_paypal_response( \stdClass $data ): Authorization {
|
||||
if ( ! isset( $data->id ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Does not contain an id.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'Does not contain an id.' );
|
||||
}
|
||||
|
||||
if ( ! isset( $data->status ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Does not contain status.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'Does not contain status.' );
|
||||
}
|
||||
|
||||
$reason = $data->status_details->reason ?? null;
|
||||
|
|
|
@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Capture;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatus;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\CaptureStatusDetails;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Class CaptureFactory
|
||||
|
@ -63,6 +64,7 @@ class CaptureFactory {
|
|||
* @param \stdClass $data The PayPal response.
|
||||
*
|
||||
* @return Capture
|
||||
* @throws RuntimeException When capture amount data is invalid.
|
||||
*/
|
||||
public function from_paypal_response( \stdClass $data ) : Capture {
|
||||
$reason = $data->status_details->reason ?? null;
|
||||
|
@ -74,13 +76,18 @@ class CaptureFactory {
|
|||
$this->fraud_processor_response_factory->from_paypal_response( $data->processor_response )
|
||||
: null;
|
||||
|
||||
$amount = $this->amount_factory->from_paypal_response( $data->amount );
|
||||
if ( null === $amount ) {
|
||||
throw new RuntimeException( 'Invalid capture amount data.' );
|
||||
}
|
||||
|
||||
return new Capture(
|
||||
(string) $data->id,
|
||||
new CaptureStatus(
|
||||
(string) $data->status,
|
||||
$reason ? new CaptureStatusDetails( $reason ) : null
|
||||
),
|
||||
$this->amount_factory->from_paypal_response( $data->amount ),
|
||||
$amount,
|
||||
(bool) $data->final_capture,
|
||||
(string) $data->seller_protection->status,
|
||||
(string) $data->invoice_id,
|
||||
|
|
|
@ -100,6 +100,34 @@ class ExperienceContextBuilder {
|
|||
return $builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a custom return URL.
|
||||
*
|
||||
* @param string $url The return URL.
|
||||
*/
|
||||
public function with_custom_return_url( string $url ): ExperienceContextBuilder {
|
||||
$builder = clone $this;
|
||||
|
||||
$builder->experience_context = $builder->experience_context
|
||||
->with_return_url( $url );
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a custom cancel URL.
|
||||
*
|
||||
* @param string $url The cancel URL.
|
||||
*/
|
||||
public function with_custom_cancel_url( string $url ): ExperienceContextBuilder {
|
||||
$builder = clone $this;
|
||||
|
||||
$builder->experience_context = $builder->experience_context
|
||||
->with_cancel_url( $url );
|
||||
|
||||
return $builder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the current brand name from the settings.
|
||||
*/
|
||||
|
|
|
@ -179,19 +179,13 @@ class ItemFactory {
|
|||
*/
|
||||
public function from_paypal_response( \stdClass $data ): Item {
|
||||
if ( ! isset( $data->name ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No name for item given', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No name for item given' );
|
||||
}
|
||||
if ( ! isset( $data->quantity ) || ! is_numeric( $data->quantity ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No quantity for item given', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No quantity for item given' );
|
||||
}
|
||||
if ( ! isset( $data->unit_amount->value ) || ! isset( $data->unit_amount->currency_code ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No money values for item given', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No money values for item given' );
|
||||
}
|
||||
|
||||
$unit_amount = new Money( (float) $data->unit_amount->value, $data->unit_amount->currency_code );
|
||||
|
|
|
@ -68,7 +68,8 @@ class OrderFactory {
|
|||
$order->payer(),
|
||||
$order->intent(),
|
||||
$order->create_time(),
|
||||
$order->update_time()
|
||||
$order->update_time(),
|
||||
$order->links()
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -81,70 +82,153 @@ class OrderFactory {
|
|||
* @throws RuntimeException When JSON object is malformed.
|
||||
*/
|
||||
public function from_paypal_response( \stdClass $order_data ): Order {
|
||||
if ( ! isset( $order_data->id ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Order does not contain an id.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
}
|
||||
if ( ! isset( $order_data->purchase_units ) || ! is_array( $order_data->purchase_units ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Order does not contain items.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
}
|
||||
if ( ! isset( $order_data->status ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Order does not contain status.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
}
|
||||
if ( ! isset( $order_data->intent ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Order does not contain intent.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
}
|
||||
$this->validate_order_id( $order_data );
|
||||
|
||||
$purchase_units = array_map(
|
||||
function ( \stdClass $data ): PurchaseUnit {
|
||||
return $this->purchase_unit_factory->from_paypal_response( $data );
|
||||
},
|
||||
$order_data->purchase_units
|
||||
);
|
||||
|
||||
$create_time = ( isset( $order_data->create_time ) ) ?
|
||||
\DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->create_time )
|
||||
: null;
|
||||
$update_time = ( isset( $order_data->update_time ) ) ?
|
||||
\DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->update_time )
|
||||
: null;
|
||||
$payer = ( isset( $order_data->payer ) ) ?
|
||||
$this->payer_factory->from_paypal_response( $order_data->payer )
|
||||
: null;
|
||||
|
||||
$payment_source = null;
|
||||
if ( isset( $order_data->payment_source ) ) {
|
||||
$json_encoded_payment_source = wp_json_encode( $order_data->payment_source );
|
||||
if ( $json_encoded_payment_source ) {
|
||||
$payment_source_as_array = json_decode( $json_encoded_payment_source, true );
|
||||
if ( $payment_source_as_array ) {
|
||||
$name = array_key_first( $payment_source_as_array );
|
||||
if ( $name ) {
|
||||
$payment_source = new PaymentSource(
|
||||
$name,
|
||||
$order_data->payment_source->$name
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$purchase_units = $this->create_purchase_units( $order_data );
|
||||
$status = $this->create_order_status( $order_data );
|
||||
$intent = $this->get_intent( $order_data );
|
||||
$timestamps = $this->create_timestamps( $order_data );
|
||||
$payer = $this->create_payer( $order_data );
|
||||
$payment_source = $this->create_payment_source( $order_data );
|
||||
$links = $order_data->links ?? null;
|
||||
|
||||
return new Order(
|
||||
$order_data->id,
|
||||
$purchase_units,
|
||||
new OrderStatus( $order_data->status ),
|
||||
$status,
|
||||
$payment_source,
|
||||
$payer,
|
||||
$order_data->intent,
|
||||
$create_time,
|
||||
$update_time
|
||||
$intent,
|
||||
$timestamps['create_time'],
|
||||
$timestamps['update_time'],
|
||||
$links
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the order data contains a required ID.
|
||||
*
|
||||
* @param \stdClass $order_data The order data.
|
||||
*
|
||||
* @throws RuntimeException When ID is missing.
|
||||
*/
|
||||
private function validate_order_id( \stdClass $order_data ): void {
|
||||
if ( ! isset( $order_data->id ) ) {
|
||||
throw new RuntimeException( 'Order does not contain an id.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates purchase units from order data.
|
||||
*
|
||||
* @param \stdClass $order_data The order data.
|
||||
*
|
||||
* @return array Array of PurchaseUnit objects.
|
||||
*/
|
||||
private function create_purchase_units( \stdClass $order_data ): array {
|
||||
if ( ! isset( $order_data->purchase_units ) || ! is_array( $order_data->purchase_units ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$purchase_units = array();
|
||||
foreach ( $order_data->purchase_units as $data ) {
|
||||
$purchase_unit = $this->purchase_unit_factory->from_paypal_response( $data );
|
||||
if ( null !== $purchase_unit ) {
|
||||
$purchase_units[] = $purchase_unit;
|
||||
}
|
||||
}
|
||||
|
||||
return $purchase_units;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates order status from order data.
|
||||
*
|
||||
* @param \stdClass $order_data The order data.
|
||||
*
|
||||
* @return OrderStatus
|
||||
*/
|
||||
private function create_order_status( \stdClass $order_data ): OrderStatus {
|
||||
$status_value = $order_data->status ?? 'PAYER_ACTION_REQUIRED';
|
||||
return new OrderStatus( $status_value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the intent from order data.
|
||||
*
|
||||
* @param \stdClass $order_data The order data.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function get_intent( \stdClass $order_data ): string {
|
||||
return $order_data->intent ?? 'CAPTURE';
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates timestamps from order data.
|
||||
*
|
||||
* @param \stdClass $order_data The order data.
|
||||
*
|
||||
* @return array Array with 'create_time' and 'update_time' keys.
|
||||
*/
|
||||
private function create_timestamps( \stdClass $order_data ): array {
|
||||
$create_time = isset( $order_data->create_time ) ?
|
||||
\DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->create_time ) :
|
||||
null;
|
||||
|
||||
$update_time = isset( $order_data->update_time ) ?
|
||||
\DateTime::createFromFormat( 'Y-m-d\TH:i:sO', $order_data->update_time ) :
|
||||
null;
|
||||
|
||||
return array(
|
||||
'create_time' => $create_time,
|
||||
'update_time' => $update_time,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates payer from order data.
|
||||
*
|
||||
* @param \stdClass $order_data The order data.
|
||||
*
|
||||
* @return mixed Payer object or null.
|
||||
*/
|
||||
private function create_payer( \stdClass $order_data ) {
|
||||
return isset( $order_data->payer ) ?
|
||||
$this->payer_factory->from_paypal_response( $order_data->payer ) :
|
||||
null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates payment source from order data.
|
||||
*
|
||||
* @param \stdClass $order_data The order data.
|
||||
*
|
||||
* @return PaymentSource|null
|
||||
*/
|
||||
private function create_payment_source( \stdClass $order_data ): ?PaymentSource {
|
||||
if ( ! isset( $order_data->payment_source ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$json_encoded_payment_source = wp_json_encode( $order_data->payment_source );
|
||||
if ( ! $json_encoded_payment_source ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$payment_source_as_array = json_decode( $json_encoded_payment_source, true );
|
||||
if ( ! $payment_source_as_array ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$source_name = array_key_first( $payment_source_as_array );
|
||||
if ( ! $source_name ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PaymentSource(
|
||||
$source_name,
|
||||
$order_data->payment_source->$source_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,9 +27,7 @@ class PaymentTokenFactory {
|
|||
*/
|
||||
public function from_paypal_response( $data ): PaymentToken {
|
||||
if ( ! isset( $data->id ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No id for payment token given', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No id for payment token given' );
|
||||
}
|
||||
|
||||
return new PaymentToken(
|
||||
|
|
|
@ -57,24 +57,16 @@ class PlanFactory {
|
|||
*/
|
||||
public function from_paypal_response( stdClass $data ): Plan {
|
||||
if ( ! isset( $data->id ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No id for given plan', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No id for given plan' );
|
||||
}
|
||||
if ( ! isset( $data->name ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No name for plan given', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No name for plan given' );
|
||||
}
|
||||
if ( ! isset( $data->product_id ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No product id for given plan', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No product id for given plan' );
|
||||
}
|
||||
if ( ! isset( $data->billing_cycles ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No billing cycles for given plan', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No billing cycles for given plan' );
|
||||
}
|
||||
|
||||
$billing_cycles = array();
|
||||
|
|
|
@ -28,14 +28,10 @@ class ProductFactory {
|
|||
*/
|
||||
public function from_paypal_response( stdClass $data ): Product {
|
||||
if ( ! isset( $data->id ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No id for product given', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No id for product given' );
|
||||
}
|
||||
if ( ! isset( $data->name ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No name for product given', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No name for product given' );
|
||||
}
|
||||
|
||||
return new Product(
|
||||
|
|
|
@ -15,6 +15,7 @@ use WooCommerce\PayPalCommerce\ApiClient\Entity\PurchaseUnit;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Helper\PurchaseUnitSanitizer;
|
||||
use WooCommerce\PayPalCommerce\Webhooks\CustomIds;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Address;
|
||||
|
||||
/**
|
||||
* Class PurchaseUnitFactory
|
||||
|
@ -88,7 +89,7 @@ class PurchaseUnitFactory {
|
|||
PaymentsFactory $payments_factory,
|
||||
string $prefix = 'WC-',
|
||||
string $soft_descriptor = '',
|
||||
PurchaseUnitSanitizer $sanitizer = null
|
||||
?PurchaseUnitSanitizer $sanitizer = null
|
||||
) {
|
||||
|
||||
$this->amount_factory = $amount_factory;
|
||||
|
@ -108,21 +109,20 @@ class PurchaseUnitFactory {
|
|||
* @return PurchaseUnit
|
||||
*/
|
||||
public function from_wc_order( \WC_Order $order ): PurchaseUnit {
|
||||
$amount = $this->amount_factory->from_wc_order( $order );
|
||||
$items = array_filter(
|
||||
$amount = $this->amount_factory->from_wc_order( $order );
|
||||
$items = array_filter(
|
||||
$this->item_factory->from_wc_order( $order ),
|
||||
function ( Item $item ): bool {
|
||||
return $item->unit_amount()->value() >= 0;
|
||||
}
|
||||
);
|
||||
$shipping = $this->shipping_factory->from_wc_order( $order );
|
||||
if (
|
||||
! $this->shipping_needed( ... array_values( $items ) ) ||
|
||||
empty( $shipping->address()->country_code() ) ||
|
||||
( ! $shipping->address()->postal_code() && ! $this->country_without_postal_code( $shipping->address()->country_code() ) )
|
||||
) {
|
||||
|
||||
$shipping = $this->shipping_factory->from_wc_order( $order );
|
||||
$shipping_address = $shipping->address();
|
||||
if ( $this->should_disable_shipping( $items, $shipping_address ) ) {
|
||||
$shipping = null;
|
||||
}
|
||||
|
||||
$reference_id = 'default';
|
||||
$description = '';
|
||||
$custom_id = (string) $order->get_id();
|
||||
|
@ -176,10 +176,12 @@ class PurchaseUnitFactory {
|
|||
$shipping = null;
|
||||
$customer = \WC()->customer;
|
||||
if ( $this->shipping_needed( ... array_values( $items ) ) && is_a( $customer, \WC_Customer::class ) ) {
|
||||
$shipping = $this->shipping_factory->from_wc_customer( \WC()->customer, $with_shipping_options );
|
||||
$shipping = $this->shipping_factory->from_wc_customer( \WC()->customer, $with_shipping_options );
|
||||
$shipping_address = $shipping->address();
|
||||
if (
|
||||
2 !== strlen( $shipping->address()->country_code() ) ||
|
||||
( ! $shipping->address()->postal_code() && ! $this->country_without_postal_code( $shipping->address()->country_code() ) )
|
||||
! $shipping_address ||
|
||||
2 !== strlen( $shipping_address->country_code() ) ||
|
||||
( ! $shipping_address->postal_code() && ! $this->country_without_postal_code( $shipping_address->country_code() ) )
|
||||
) {
|
||||
$shipping = null;
|
||||
}
|
||||
|
@ -219,17 +221,20 @@ class PurchaseUnitFactory {
|
|||
*
|
||||
* @param \stdClass $data The JSON object.
|
||||
*
|
||||
* @return PurchaseUnit
|
||||
* @return ?PurchaseUnit
|
||||
* @throws RuntimeException When JSON object is malformed.
|
||||
*/
|
||||
public function from_paypal_response( \stdClass $data ): PurchaseUnit {
|
||||
public function from_paypal_response( \stdClass $data ): ?PurchaseUnit {
|
||||
if ( ! isset( $data->reference_id ) || ! is_string( $data->reference_id ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No reference ID given.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No reference ID given.' );
|
||||
}
|
||||
|
||||
$amount_data = $data->amount ?? null;
|
||||
$amount = $this->amount_factory->from_paypal_response( $amount_data );
|
||||
if ( null === $amount ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$amount = $this->amount_factory->from_paypal_response( $data->amount );
|
||||
$description = ( isset( $data->description ) ) ? $data->description : '';
|
||||
$custom_id = ( isset( $data->custom_id ) ) ? $data->custom_id : '';
|
||||
$invoice_id = ( isset( $data->invoice_id ) ) ? $data->invoice_id : '';
|
||||
|
@ -245,7 +250,7 @@ class PurchaseUnitFactory {
|
|||
}
|
||||
$shipping = null;
|
||||
try {
|
||||
if ( isset( $data->shipping ) ) {
|
||||
if ( isset( $data->shipping ) && ! empty( (array) $data->shipping ) ) {
|
||||
$shipping = $this->shipping_factory->from_paypal_response( $data->shipping );
|
||||
}
|
||||
} catch ( RuntimeException $error ) {
|
||||
|
@ -334,4 +339,19 @@ class PurchaseUnitFactory {
|
|||
|
||||
return substr( $sanitized, 0, 22 ) ?: '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether shipping should be disabled for a purchase unit.
|
||||
*
|
||||
* @param array $items Purchase unit items.
|
||||
* @param Address|null $shipping_address The shipping address to validate.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function should_disable_shipping( array $items, ?Address $shipping_address ): bool {
|
||||
return ! $this->shipping_needed( ... array_values( $items ) ) ||
|
||||
! $shipping_address ||
|
||||
empty( $shipping_address->country_code() ) ||
|
||||
( ! $shipping_address->postal_code() && ! $this->country_without_postal_code( $shipping_address->country_code() ) );
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
|||
use WooCommerce\PayPalCommerce\ApiClient\Entity\Refund;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatus;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Entity\RefundStatusDetails;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Class RefundFactory
|
||||
|
@ -62,6 +63,7 @@ class RefundFactory {
|
|||
* @param \stdClass $data The PayPal response.
|
||||
*
|
||||
* @return Refund
|
||||
* @throws RuntimeException When refund amount data is invalid.
|
||||
*/
|
||||
public function from_paypal_response( \stdClass $data ) : Refund {
|
||||
$reason = $data->status_details->reason ?? null;
|
||||
|
@ -73,13 +75,18 @@ class RefundFactory {
|
|||
$this->refund_payer_factory->from_paypal_response( $data->payer )
|
||||
: null;
|
||||
|
||||
$amount = $this->amount_factory->from_paypal_response( $data->amount );
|
||||
if ( null === $amount ) {
|
||||
throw new RuntimeException( 'Invalid refund amount data.' );
|
||||
}
|
||||
|
||||
return new Refund(
|
||||
(string) $data->id,
|
||||
new RefundStatus(
|
||||
(string) $data->status,
|
||||
$reason ? new RefundStatusDetails( $reason ) : null
|
||||
),
|
||||
$this->amount_factory->from_paypal_response( $data->amount ),
|
||||
$amount,
|
||||
(string) ( $data->invoice_id ?? '' ),
|
||||
(string) ( $data->custom_id ?? '' ),
|
||||
$seller_payable_breakdown,
|
||||
|
|
53
modules/ppcp-api-client/src/Factory/ReturnUrlFactory.php
Normal file
53
modules/ppcp-api-client/src/Factory/ReturnUrlFactory.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
/**
|
||||
* Factory for determining the appropriate return URL based on context.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Factory
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Factory;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Class ReturnUrlFactory
|
||||
*/
|
||||
class ReturnUrlFactory {
|
||||
|
||||
/**
|
||||
* @throws RuntimeException When required data is missing for the context.
|
||||
*/
|
||||
public function from_context( string $context, array $request_data = array() ): string {
|
||||
switch ( $context ) {
|
||||
case 'cart':
|
||||
case 'cart-block':
|
||||
case 'mini-cart':
|
||||
return wc_get_cart_url();
|
||||
case 'product':
|
||||
if ( ! empty( $request_data['purchase_units'] ) && is_array( $request_data['purchase_units'] ) ) {
|
||||
$first_unit = reset( $request_data['purchase_units'] );
|
||||
if ( ! empty( $first_unit['items'] ) && is_array( $first_unit['items'] ) ) {
|
||||
$first_item = reset( $first_unit['items'] );
|
||||
if ( ! empty( $first_item['url'] ) ) {
|
||||
return $first_item['url'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException( 'Product URL is required but not provided in the request data.' );
|
||||
case 'pay-now':
|
||||
if ( ! empty( $request_data['order_id'] ) ) {
|
||||
$order = wc_get_order( $request_data['order_id'] );
|
||||
if ( $order instanceof \WC_Order ) {
|
||||
return $order->get_checkout_payment_url();
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException( 'The order ID is invalid.' );
|
||||
default:
|
||||
return wc_get_checkout_url();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -96,34 +96,25 @@ class ShippingFactory {
|
|||
* @throws RuntimeException When JSON object is malformed.
|
||||
*/
|
||||
public function from_paypal_response( \stdClass $data ): Shipping {
|
||||
if ( ! isset( $data->name->full_name ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No name was given for shipping.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
}
|
||||
if ( ! isset( $data->address ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No address was given for shipping.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
}
|
||||
$contact_phone = null;
|
||||
$contact_email = null;
|
||||
$address = $this->address_factory->from_paypal_response( $data->address );
|
||||
$name = $data->name->full_name ?? null;
|
||||
|
||||
$address = isset( $data->address )
|
||||
? $this->address_factory->from_paypal_response( $data->address )
|
||||
: null;
|
||||
|
||||
$contact_email = $data->email_address ?? null;
|
||||
|
||||
$contact_phone = isset( $data->phone_number->national_number )
|
||||
? new Phone( $data->phone_number->national_number )
|
||||
: null;
|
||||
|
||||
$options = array_map(
|
||||
array( $this->shipping_option_factory, 'from_paypal_response' ),
|
||||
$data->options ?? array()
|
||||
);
|
||||
|
||||
if ( isset( $data->phone_number->national_number ) ) {
|
||||
$contact_phone = new Phone( $data->phone_number->national_number );
|
||||
}
|
||||
if ( isset( $data->email_address ) ) {
|
||||
$contact_email = $data->email_address;
|
||||
}
|
||||
|
||||
return new Shipping(
|
||||
$data->name->full_name,
|
||||
$name,
|
||||
$address,
|
||||
$contact_email,
|
||||
$contact_phone,
|
||||
|
|
|
@ -40,14 +40,10 @@ class WebhookEventFactory {
|
|||
*/
|
||||
public function from_paypal_response( $data ): WebhookEvent {
|
||||
if ( ! isset( $data->id ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'ID for webhook event not found.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'ID for webhook event not found.' );
|
||||
}
|
||||
if ( ! isset( $data->event_type ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'Event type for webhook event not found.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'Event type for webhook event not found.' );
|
||||
}
|
||||
|
||||
$create_time = ( isset( $data->create_time ) ) ?
|
||||
|
|
|
@ -59,19 +59,13 @@ class WebhookFactory {
|
|||
*/
|
||||
public function from_paypal_response( $data ): Webhook {
|
||||
if ( ! isset( $data->id ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No id for webhook given.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No id for webhook given.' );
|
||||
}
|
||||
if ( ! isset( $data->url ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No URL for webhook given.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No URL for webhook given.' );
|
||||
}
|
||||
if ( ! isset( $data->event_types ) ) {
|
||||
throw new RuntimeException(
|
||||
__( 'No event types for webhook given.', 'woocommerce-paypal-payments' )
|
||||
);
|
||||
throw new RuntimeException( 'No event types for webhook given.' );
|
||||
}
|
||||
|
||||
return new Webhook(
|
||||
|
|
|
@ -114,7 +114,7 @@ abstract class ProductStatus {
|
|||
* @param Settings|null $settings See description in {@see self::clear()}.
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function clear_state( Settings $settings = null ) : void;
|
||||
abstract protected function clear_state( ?Settings $settings = null ) : void;
|
||||
|
||||
/**
|
||||
* Whether the merchant has access to the feature.
|
||||
|
@ -199,7 +199,7 @@ abstract class ProductStatus {
|
|||
* @param Settings|null $settings The settings object.
|
||||
* @return void
|
||||
*/
|
||||
public function clear( Settings $settings = null ) : void {
|
||||
public function clear( ?Settings $settings = null ) : void {
|
||||
$this->is_eligible = null;
|
||||
$this->has_request_failure = false;
|
||||
|
||||
|
|
|
@ -81,7 +81,7 @@ class PurchaseUnitSanitizer {
|
|||
* @param string|null $mode The mismatch handling mode, ditch or extra_line.
|
||||
* @param string|null $extra_line_name The name of the extra line.
|
||||
*/
|
||||
public function __construct( string $mode = null, string $extra_line_name = null ) {
|
||||
public function __construct( ?string $mode = null, ?string $extra_line_name = null ) {
|
||||
|
||||
if ( ! in_array( $mode, self::VALID_MODES, true ) ) {
|
||||
$mode = self::MODE_DITCH;
|
||||
|
@ -193,10 +193,16 @@ class PurchaseUnitSanitizer {
|
|||
$item_mismatch = $this->calculate_item_mismatch();
|
||||
|
||||
if ( $item_mismatch > 0 ) {
|
||||
// Use appropriate category to preserve purely digital or physical goods baskets.
|
||||
$rounding_item_category = $this->determine_rounding_item_category();
|
||||
|
||||
// Add extra line item with roundings.
|
||||
$line_name = $this->extra_line_name;
|
||||
$roundings_money = new Money( $item_mismatch, $this->currency_code() );
|
||||
$this->purchase_unit['items'][] = ( new Item( $line_name, $roundings_money, 1 ) )->to_array();
|
||||
$line_name = $this->extra_line_name;
|
||||
$roundings_money = new Money( $item_mismatch, $this->currency_code() );
|
||||
|
||||
$this->purchase_unit['items'][] = (
|
||||
new Item( $line_name, $roundings_money, 1, '', null, '', $rounding_item_category )
|
||||
)->to_array();
|
||||
|
||||
$this->set_last_message(
|
||||
__( 'Item amount mismatch. Extra line added.', 'woocommerce-paypal-payments' )
|
||||
|
@ -217,6 +223,24 @@ class PurchaseUnitSanitizer {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the appropriate category for rounding items based on existing items.
|
||||
*
|
||||
* @return string The category (Item::DIGITAL_GOODS or Item::PHYSICAL_GOODS)
|
||||
*/
|
||||
private function determine_rounding_item_category(): string {
|
||||
// Check if all items are digital goods.
|
||||
foreach ( $this->items() as $item ) {
|
||||
$category = $item['category'] ?? Item::PHYSICAL_GOODS;
|
||||
if ( $category !== Item::DIGITAL_GOODS ) {
|
||||
return Item::PHYSICAL_GOODS;
|
||||
}
|
||||
}
|
||||
|
||||
// All items are digital goods.
|
||||
return Item::DIGITAL_GOODS;
|
||||
}
|
||||
|
||||
/**
|
||||
* The sanitizes the purchase_unit items tax.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
/**
|
||||
* Reference transaction status helper class.
|
||||
*
|
||||
* @package WooCommerce\PayPalCommerce\ApiClient\Helper
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace WooCommerce\PayPalCommerce\ApiClient\Helper;
|
||||
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Endpoint\PartnersEndpoint;
|
||||
use WooCommerce\PayPalCommerce\ApiClient\Exception\RuntimeException;
|
||||
|
||||
/**
|
||||
* Class ReferenceTransactionStatus
|
||||
*
|
||||
* Helper class to check reference transaction capabilities for PayPal merchant accounts.
|
||||
*/
|
||||
class ReferenceTransactionStatus {
|
||||
|
||||
public const CACHE_KEY = 'ppcp_reference_transaction_enabled';
|
||||
|
||||
|
||||
protected PartnersEndpoint $partners_endpoint;
|
||||
protected Cache $cache;
|
||||
|
||||
public function __construct( PartnersEndpoint $partners_endpoint, Cache $cache ) {
|
||||
$this->partners_endpoint = $partners_endpoint;
|
||||
$this->cache = $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if reference transactions are enabled in the merchant account.
|
||||
*
|
||||
* This method verifies if the merchant has the PAYPAL_WALLET_VAULTING_ADVANCED
|
||||
* capability active, which is required for processing reference transactions.
|
||||
*
|
||||
* @return bool True if reference transactions are enabled, false otherwise.
|
||||
*/
|
||||
public function reference_transaction_enabled(): bool {
|
||||
if ( $this->cache->has( self::CACHE_KEY ) ) {
|
||||
return (bool) $this->cache->get( self::CACHE_KEY );
|
||||
}
|
||||
|
||||
try {
|
||||
foreach ( $this->partners_endpoint->seller_status()->capabilities() as $capability ) {
|
||||
if (
|
||||
$capability->name() === 'PAYPAL_WALLET_VAULTING_ADVANCED' &&
|
||||
$capability->status() === 'ACTIVE'
|
||||
) {
|
||||
$this->cache->set( self::CACHE_KEY, true, MONTH_IN_SECONDS );
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} catch ( RuntimeException $exception ) {
|
||||
$this->cache->set( self::CACHE_KEY, false, HOUR_IN_SECONDS );
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->cache->set( self::CACHE_KEY, false, HOUR_IN_SECONDS );
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -54,7 +54,12 @@ class PartnerReferralsData {
|
|||
* @param bool $use_card_payments If the merchant wants to process credit card payments.
|
||||
* @return array
|
||||
*/
|
||||
public function data( array $products = array(), string $onboarding_token = '', bool $use_subscriptions = null, bool $use_card_payments = true ) : array {
|
||||
public function data(
|
||||
array $products = array(),
|
||||
string $onboarding_token = '',
|
||||
?bool $use_subscriptions = null,
|
||||
bool $use_card_payments = true
|
||||
) : array {
|
||||
$in_acdc_country = $this->dcc_applies->for_country_currency();
|
||||
|
||||
if ( ! $products ) {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -94,20 +94,23 @@ if (
|
|||
features.push( 'subscriptions' );
|
||||
}
|
||||
|
||||
registerExpressPaymentMethod( {
|
||||
name: buttonData.id,
|
||||
title: `PayPal - ${ buttonData.title }`,
|
||||
description: __(
|
||||
'Eligible users will see the PayPal button.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
label: <div dangerouslySetInnerHTML={ { __html: buttonData.title } } />,
|
||||
content: <ApplePayComponent isEditing={ false } />,
|
||||
edit: <ApplePayComponent isEditing={ true } />,
|
||||
ariaLabel: buttonData.title,
|
||||
canMakePayment: () => buttonData.enabled,
|
||||
supports: {
|
||||
features,
|
||||
style: [ 'height', 'borderRadius' ],
|
||||
},
|
||||
} );
|
||||
if ( buttonConfig?.is_enabled ) {
|
||||
registerExpressPaymentMethod( {
|
||||
name: buttonData.id,
|
||||
title: `PayPal - ${ buttonData.title }`,
|
||||
description: __(
|
||||
'Eligible users will see the PayPal button.',
|
||||
'woocommerce-paypal-payments'
|
||||
),
|
||||
label: <div dangerouslySetInnerHTML={ { __html: buttonData.title } } />,
|
||||
content: <ApplePayComponent isEditing={ false } />,
|
||||
edit: <ApplePayComponent isEditing={ true } />,
|
||||
ariaLabel: buttonData.title,
|
||||
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';
|
||||
|
|
|
@ -18,6 +18,7 @@ use WooCommerce\PayPalCommerce\Applepay\Assets\PropertiesDictionary;
|
|||
use WooCommerce\PayPalCommerce\Button\Assets\ButtonInterface;
|
||||
use WooCommerce\PayPalCommerce\Button\Assets\SmartButtonInterface;
|
||||
use WooCommerce\PayPalCommerce\Applepay\Helper\AvailabilityNotice;
|
||||
use WooCommerce\PayPalCommerce\Settings\SettingsModule;
|
||||
use WooCommerce\PayPalCommerce\WcGateway\Helper\Environment;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExecutableModule;
|
||||
use WooCommerce\PayPalCommerce\Vendor\Inpsyde\Modularity\Module\ExtendingModule;
|
||||
|
@ -55,7 +56,7 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
// Clears product status when appropriate.
|
||||
add_action(
|
||||
'woocommerce_paypal_payments_clear_apm_product_status',
|
||||
function( Settings $settings = null ) use ( $c ): void {
|
||||
function( ?Settings $settings = null ) use ( $c ): void {
|
||||
$apm_status = $c->get( 'applepay.apple-product-status' );
|
||||
assert( $apm_status instanceof AppleProductStatus );
|
||||
$apm_status->clear( $settings );
|
||||
|
@ -93,15 +94,17 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
return;
|
||||
}
|
||||
|
||||
if ( $apple_payment_method->is_enabled() ) {
|
||||
$module->load_assets( $c, $apple_payment_method );
|
||||
$module->handle_validation_file( $c, $apple_payment_method );
|
||||
$module->render_buttons( $c, $apple_payment_method );
|
||||
$apple_payment_method->bootstrap_ajax_request();
|
||||
}
|
||||
|
||||
$module->load_admin_assets( $c, $apple_payment_method );
|
||||
$module->load_block_editor_assets( $c, $apple_payment_method );
|
||||
|
||||
if ( SettingsModule::should_use_the_old_ui() && ! $apple_payment_method->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$module->load_assets( $c, $apple_payment_method );
|
||||
$module->handle_validation_file( $c, $apple_payment_method );
|
||||
$module->render_buttons( $c, $apple_payment_method );
|
||||
$apple_payment_method->bootstrap_ajax_request();
|
||||
},
|
||||
1
|
||||
);
|
||||
|
@ -241,7 +244,8 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
$validation_string = $this->validation_string( $is_sandbox );
|
||||
nocache_headers();
|
||||
header( 'Content-Type: text/plain', true, 200 );
|
||||
echo $validation_string;// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
echo $validation_string;
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
@ -254,13 +258,13 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
* @return void
|
||||
*/
|
||||
public function load_assets( ContainerInterface $c, ApplePayButton $button ): void {
|
||||
if ( ! $button->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action(
|
||||
'wp_enqueue_scripts',
|
||||
function () use ( $c, $button ) {
|
||||
if ( ! $button->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
$smart_button = $c->get( 'button.smart-button' );
|
||||
assert( $smart_button instanceof SmartButtonInterface );
|
||||
if ( $smart_button->should_load_ppcp_script() ) {
|
||||
|
@ -281,6 +285,9 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
add_action(
|
||||
'enqueue_block_editor_assets',
|
||||
function () use ( $c, $button ) {
|
||||
if ( ! $button->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
$button->enqueue_admin_styles();
|
||||
}
|
||||
);
|
||||
|
@ -361,9 +368,6 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
* @return void
|
||||
*/
|
||||
public function render_buttons( ContainerInterface $c, ApplePayButton $button ): void {
|
||||
if ( ! $button->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
add_action(
|
||||
'wp',
|
||||
|
@ -371,8 +375,13 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
if ( is_admin() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$button = $c->get( 'applepay.button' );
|
||||
|
||||
if ( ! $button->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Button.
|
||||
*
|
||||
|
@ -391,9 +400,6 @@ class ApplepayModule implements ServiceModule, ExtendingModule, ExecutableModule
|
|||
* @return void
|
||||
*/
|
||||
public function handle_validation_file( ContainerInterface $c, ApplePayButton $button ): void {
|
||||
if ( ! $button->is_enabled() ) {
|
||||
return;
|
||||
}
|
||||
$env = $c->get( 'settings.environment' );
|
||||
assert( $env instanceof Environment );
|
||||
$is_sandbox = $env->current_environment_is( Environment::SANDBOX );
|
||||
|
|
|
@ -160,7 +160,7 @@ class ApplePayDataObjectHttp {
|
|||
* @return void
|
||||
*/
|
||||
public function validation_data(): void {
|
||||
$data = filter_input( INPUT_POST, 'validation', FILTER_VALIDATE_BOOL );
|
||||
$data = filter_input( INPUT_POST, 'validation', FILTER_VALIDATE_BOOLEAN );
|
||||
if ( ! $data ) {
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -101,7 +101,7 @@ class AppleProductStatus extends ProductStatus {
|
|||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
protected function clear_state( Settings $settings = null ) : void {
|
||||
protected function clear_state( ?Settings $settings = null ) : void {
|
||||
if ( null === $settings ) {
|
||||
$settings = $this->settings;
|
||||
}
|
||||
|
|
|
@ -42,40 +42,56 @@ $fast-transition-duration: 0.5s;
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 300px;
|
||||
max-width: 340px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__content {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
aspect-ratio: 1.586;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
border: 1px solid $border-color;
|
||||
font-size: 0.875em;
|
||||
font-family: monospace;
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
border-radius: 4px;
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
box-shadow: 0 3px 10px -3px rgba(0, 0, 0, .2666666667);
|
||||
background-image: linear-gradient(60deg, rgba(0, 0, 0, 0.0666666667), rgba(204, 204, 204, 0.0666666667) 65%, rgba(255, 255, 255, 0.4) 68%, rgba(255, 255, 255, 0));
|
||||
border: 2px solid #ccc;
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
@include flex-space-between;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
|
||||
&-digits {
|
||||
letter-spacing: 2px;
|
||||
margin-top: 76px;
|
||||
font-size: 24px;
|
||||
text-shadow: 0 -1px 1px #fff, 0 1px 1px rgba(0, 0, 0, .2666666667);
|
||||
color: #666;
|
||||
text-align: center;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
align-self: flex-end;
|
||||
&-logo {
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
top: 32px;
|
||||
height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
&__watermark {
|
||||
align-self: flex-end;
|
||||
&-expiry {
|
||||
text-align: right;
|
||||
font-size: 14px;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
&-name {
|
||||
text-transform: uppercase;
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
bottom: 20px;
|
||||
line-height: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
&__edit {
|
||||
|
|
|
@ -45,24 +45,19 @@ const Card = ( { fastlaneSdk, showWatermark = true } ) => {
|
|||
<div className="wc-block-checkout-axo-block-card__inner">
|
||||
<div className="wc-block-checkout-axo-block-card__content">
|
||||
<div className="wc-block-checkout-axo-block-card__meta">
|
||||
<span className="wc-block-checkout-axo-block-card__meta-digits">
|
||||
<div className="wc-block-checkout-axo-block-card__meta-logo">
|
||||
{ cardLogo }
|
||||
</div>
|
||||
<div className="wc-block-checkout-axo-block-card__meta-digits">
|
||||
{ `**** **** **** ${ lastDigits }` }
|
||||
</span>
|
||||
{ cardLogo }
|
||||
</div>
|
||||
<div className="wc-block-checkout-axo-block-card__meta-expiry">
|
||||
{ formattedExpiry }
|
||||
</div>
|
||||
<div className="wc-block-checkout-axo-block-card__meta-name">
|
||||
{ name }
|
||||
</div>
|
||||
</div>
|
||||
<div className="wc-block-checkout-axo-block-card__meta">
|
||||
<span>{ name }</span>
|
||||
<span>{ formattedExpiry }</span>{ ' ' }
|
||||
</div>
|
||||
</div>
|
||||
<div className="wc-block-checkout-axo-block-card__watermark">
|
||||
{ showWatermark && (
|
||||
<Watermark
|
||||
fastlaneSdk={ fastlaneSdk }
|
||||
name="wc-block-checkout-axo-card-watermark"
|
||||
includeAdditionalInfo={ false }
|
||||
/>
|
||||
) }
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -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,
|
||||
} );
|
||||
|
|
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