From 42eeedbc0fc8f7e6ed4349cd75eae8adadb6c00a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Mon, 16 Feb 2026 14:15:16 +0100 Subject: [PATCH 01/36] PPSYL-170 - Avoid critical when no payment id yet --- src/Command/Handler/StatusPaymentRequestHandler.php | 12 +++++++++++- src/PaymentProcessing/AbortPaymentProcessor.php | 8 +++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/Command/Handler/StatusPaymentRequestHandler.php b/src/Command/Handler/StatusPaymentRequestHandler.php index 4757f2ae..2134beb8 100644 --- a/src/Command/Handler/StatusPaymentRequestHandler.php +++ b/src/Command/Handler/StatusPaymentRequestHandler.php @@ -8,6 +8,7 @@ use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; use PayPlug\SyliusPayPlugPlugin\Command\StatusPaymentRequest; use PayPlug\SyliusPayPlugPlugin\Handler\PaymentNotificationHandler; +use Psr\Log\LoggerInterface; use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\PaymentBundle\Provider\PaymentRequestProviderInterface; use Sylius\Component\Payment\Model\PaymentInterface; @@ -24,6 +25,7 @@ public function __construct( private StateMachineInterface $stateMachine, private PayPlugApiClientFactoryInterface $apiClientFactory, private PaymentNotificationHandler $paymentNotificationHandler, + private LoggerInterface $logger, ) {} public function __invoke(StatusPaymentRequest $statusPaymentRequest): void @@ -44,7 +46,15 @@ public function __invoke(StatusPaymentRequest $statusPaymentRequest): void // We don't have a forced status, so we retrieve the payment status from PayPlug $client = $this->apiClientFactory->createForPaymentMethod($method); // @phpstan-ignore-next-line - getDetails() return mixed - $payplugPayment = $client->retrieve($payment->getDetails()['payment_id'] ?? throw new \LogicException('No PayPlug payment ID found in payment details.')); + $payplugPaymentId = $payment->getDetails()['payment_id'] ?? null; + if (null === $payplugPaymentId) { + $this->logger->warning('No PayPlug payment ID found in payment details.', ['payment_id' => $payment->getId(), 'order_id' => $payment->getOrder()?->getId()]); + $payment->setDetails(['status' => PayPlugApiClientInterface::FAILED]); + $this->updatePaymentState($payment); + return; + } + + $payplugPayment = $client->retrieve($payplugPaymentId); $paymentRequest->setResponseData((array) $payplugPayment); $details = new \ArrayObject($payment->getDetails()); diff --git a/src/PaymentProcessing/AbortPaymentProcessor.php b/src/PaymentProcessing/AbortPaymentProcessor.php index 60cf8d48..e03fceef 100644 --- a/src/PaymentProcessing/AbortPaymentProcessor.php +++ b/src/PaymentProcessing/AbortPaymentProcessor.php @@ -35,12 +35,18 @@ public function onFailedCompleteTransitionEvent(CompletedEvent $event): void public function process(PaymentInterface $payment): void { + $paymentId = $payment->getDetails()['payment_id'] ?? null; + if (null === $paymentId) { + // Payment not even started on payplug + return; + } + try { // When a payment is failed on Sylius, also abort it on PayPlug. // This should prevent the case that if we are already on PayPlug payment page // and go to the order history in another tab to click on pay again, then fail the transaction // and go back on the first PayPlug payment page and succeed it, it stays failed as its first payment model is already failed - $this->payPlugApiClient->abortPayment($payment->getDetails()['payment_id']); + $this->payPlugApiClient->abortPayment($paymentId); } catch (HttpException) { } } From cdfa1d2b1c2cd3b30f3240d223a22a07da7441c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Mon, 16 Feb 2026 14:18:40 +0100 Subject: [PATCH 02/36] PPSYL-170 - Avoid send details from mollie to payplug --- src/Creator/PayPlugPaymentDataCreator.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Creator/PayPlugPaymentDataCreator.php b/src/Creator/PayPlugPaymentDataCreator.php index 940fe558..b4bf2ead 100644 --- a/src/Creator/PayPlugPaymentDataCreator.php +++ b/src/Creator/PayPlugPaymentDataCreator.php @@ -54,7 +54,7 @@ public function create( /** @var CustomerInterface $customer */ $customer = $order->getCustomer(); - $details = new ArrayObject($payment->getDetails()); + $details = new ArrayObject(); $details['amount'] = $payment->getAmount(); $details['currency'] = $payment->getCurrencyCode(); From bf9adfb1e227425fdb1e5510406e2405ac734bdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Mon, 16 Feb 2026 14:44:05 +0100 Subject: [PATCH 03/36] PPSYL-171 - Update readme instruction / fix links --- README.md | 69 +++++++++++++++---------------------------------------- 1 file changed, 18 insertions(+), 51 deletions(-) diff --git a/README.md b/README.md index 5d2f4eea..3560a9bc 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ -[![License](https://img.shields.io/packagist/l/payplug/payplug-sylius.svg)](https://github.com/payplug/SyliusPayPlugPlugin/blob/master/LICENSE) -![CI](https://github.com/payplug/SyliusPayPlugPlugin/workflows/CI/badge.svg?branch=master) -[![Version](https://img.shields.io/packagist/v/payplug/payplug-sylius.svg)](https://packagist.org/packages/payplug/payplug-sylius) -[![Total Downloads](https://poser.pugx.org/payplug/payplug-sylius/downloads)](https://packagist.org/packages/payplug/payplug-sylius) +[![License](https://img.shields.io/packagist/l/payplug/sylius-payplug-plugin.svg)](https://github.com/payplug/SyliusPayPlugPlugin/blob/master/LICENSE) +[![CI - Analysis](https://github.com/payplug/SyliusPayPlugPlugin/actions/workflows/analysis.yaml/badge.svg?branch=master)](https://github.com/payplug/SyliusPayPlugPlugin/actions/workflows/analysis.yaml) +[![CI - Sylius](https://github.com/payplug/SyliusPayPlugPlugin/actions/workflows/sylius.yaml/badge.svg?branch=master)](https://github.com/payplug/SyliusPayPlugPlugin/actions/workflows/sylius.yaml) +[![Version](https://img.shields.io/packagist/v/payplug/sylius-payplug-plugin.svg)](https://packagist.org/packages/payplug/sylius-payplug-plugin) +[![Total Downloads](https://poser.pugx.org/payplug/sylius-payplug-plugin/downloads)](https://packagist.org/packages/payplug/sylius-payplug-plugin)

- + + + + Sylius Logo. +

@@ -26,10 +31,10 @@ In local environment, the plugin will not work properly because you will not be ## Compatibility -| | Version | -| :--- |:----------------------| -| PHP | 7.4, 8.0, 8.1 | -| Sylius | 1.9, 1.10, 1.11, 1.12 | +| | Version | +| :--- |:--------| +| PHP | ^8.2 | +| Sylius | ^2.0 | ## Installation @@ -46,14 +51,7 @@ In local environment, the plugin will not work properly because you will not be bin/console doctrine:migrations:migrate ``` -3. Copy templates that are overridden by Sylius into `templates/bundles/SyliusAdminBundle` - - ```shell - mkdir -p templates/bundles/SyliusAdminBundle/ - cp -R vendor/payplug/sylius-payplug-plugin/templates/SyliusAdminBundle/* templates/bundles/SyliusAdminBundle/ - ``` - -4. Add Payplug to refundable payment method for Sylius Refund Plugin in `config/services.yaml` +3. Add Payplug to refundable payment method for Sylius Refund Plugin in `config/services.yaml` ```yaml parameters: @@ -66,14 +64,7 @@ In local environment, the plugin will not work properly because you will not be - payplug_american_express ``` -5. Add Payplug routes in `config/routes.yaml` - - ```yaml - sylius_payplug: - resource: "@PayPlugSyliusPayPlugPlugin/config/routing.yaml" - ``` - -8. Add Traits for Customer and PaymentMethod entities +4. Add Traits for Customer and PaymentMethod entities * App\Entity\Customer\Customer @@ -158,14 +149,14 @@ In local environment, the plugin will not work properly because you will not be } ``` -9. Process translations +5. Process translations ```bash php bin/console translation:extract en PayPlugSyliusPayPlugPlugin --dump-messages php bin/console translation:extract fr PayPlugSyliusPayPlugPlugin --dump-messages ``` -10. Clear cache: +6. Clear cache: ```shell php bin/console cache:clear @@ -199,30 +190,6 @@ Run the below command to see what Symfony services are shared with this plugin: $ bin/console debug:container payplug_sylius_payplug_plugin ``` -### Template overriding - -This plugin override some sylius templates. -If you plan override them also, you should retrieve them in your application. - -Copy Sylius templates overridden in plugin to your templates directory (e.g templates/bundles/) - -```shell -mkdir -p templates/bundles/SyliusAdminBundle/ -mkdir -p templates/bundles/SyliusShopBundle/ -mkdir -p templates/bundles/SyliusUiBundle/ -cp -R vendor/payplug/sylius-payplug-plugin/templates/SyliusAdminBundle/* templates/bundles/SyliusAdminBundle/ -cp -R vendor/payplug/sylius-payplug-plugin/templates/SyliusShopBundle/* templates/bundles/SyliusShopBundle/ -cp -R vendor/payplug/sylius-payplug-plugin/templates/SyliusUiBundle/* templates/bundles/SyliusUiBundle/ -``` - -You also need to edit your twig config to add your path to avoid our configuration to be prepended : -```yaml -twig: - paths: - '%kernel.project_dir%/templates/bundles/SyliusAdminBundle': SyliusAdmin - '%kernel.project_dir%/templates/bundles/SyliusShopBundle': SyliusShop - '%kernel.project_dir%/templates/bundles/SyliusUiBundle': SyliusUi -``` ## Development See [How to contribute](CONTRIBUTING.md). From 55be2d2eb0cfa57dc66670f8d379f4ad48d4ef7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Mon, 16 Feb 2026 15:12:04 +0100 Subject: [PATCH 04/36] Hotfix - Bump phpunit --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 079ccdb9..f7d338e1 100755 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "phpstan/phpstan-doctrine": "2.0.1", "phpstan/phpstan-strict-rules": "2.0.1", "phpstan/phpstan-webmozart-assert": "2.0.0", - "phpunit/phpunit": "9.6.22", + "phpunit/phpunit": "^9.6", "rector/rector": "2.0.4", "sylius-labs/coding-standard": "4.4.0", "sylius/test-application": "^2.1.0@alpha", From bf3c59c1d3ae8a3c7e344ef3c89678b60e8cfc87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Mon, 16 Feb 2026 15:31:27 +0100 Subject: [PATCH 05/36] Fix Phpstan --- src/Command/Handler/StatusPaymentRequestHandler.php | 2 +- src/Creator/PayPlugPaymentDataCreator.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Command/Handler/StatusPaymentRequestHandler.php b/src/Command/Handler/StatusPaymentRequestHandler.php index 2134beb8..2515ed95 100644 --- a/src/Command/Handler/StatusPaymentRequestHandler.php +++ b/src/Command/Handler/StatusPaymentRequestHandler.php @@ -45,7 +45,7 @@ public function __invoke(StatusPaymentRequest $statusPaymentRequest): void // We don't have a forced status, so we retrieve the payment status from PayPlug $client = $this->apiClientFactory->createForPaymentMethod($method); - // @phpstan-ignore-next-line - getDetails() return mixed + /** @var null|string $payplugPaymentId */ $payplugPaymentId = $payment->getDetails()['payment_id'] ?? null; if (null === $payplugPaymentId) { $this->logger->warning('No PayPlug payment ID found in payment details.', ['payment_id' => $payment->getId(), 'order_id' => $payment->getOrder()?->getId()]); diff --git a/src/Creator/PayPlugPaymentDataCreator.php b/src/Creator/PayPlugPaymentDataCreator.php index b4bf2ead..e9e32322 100644 --- a/src/Creator/PayPlugPaymentDataCreator.php +++ b/src/Creator/PayPlugPaymentDataCreator.php @@ -54,6 +54,7 @@ public function create( /** @var CustomerInterface $customer */ $customer = $order->getCustomer(); + /** @var ArrayObject $details */ $details = new ArrayObject(); $details['amount'] = $payment->getAmount(); $details['currency'] = $payment->getCurrencyCode(); From 95d39bf557502416f6a848dc05c4f671fd1f5b56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Mon, 16 Feb 2026 15:41:28 +0100 Subject: [PATCH 06/36] Remove install ca certificate, not used anymore --- .github/workflows/sylius.yaml | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/.github/workflows/sylius.yaml b/.github/workflows/sylius.yaml index 15e307f9..33dc6c31 100644 --- a/.github/workflows/sylius.yaml +++ b/.github/workflows/sylius.yaml @@ -81,15 +81,6 @@ jobs: - name: 'Install Sylius-Standard and Plugin' run: 'make install -e SYLIUS_VERSION=${{ matrix.sylius }} SYMFONY_VERSION=${{ matrix.symfony }}' - - - name: 'Install certificates' - run: 'symfony server:ca:install' - - - name: 'Run Chrome headless' - run: 'google-chrome-stable --enable-automation --disable-background-networking --no-default-browser-check --no-first-run --disable-popup-blocking --disable-default-apps --allow-insecure-localhost --disable-translate --disable-extensions --no-sandbox --enable-features=Metal --headless --remote-debugging-port=9222 --window-size=2880,1800 --proxy-server=''direct://'' --proxy-bypass-list=''*'' https://127.0.0.1 > /dev/null 2>&1 &' - - - name: 'Run webserver' - run: 'symfony server:start --port=8080 --daemon' id: end-of-setup-sylius - name: 'Doctrine Schema Validate - Run' @@ -98,14 +89,6 @@ jobs: name: 'Run PHPUnit' run: 'make phpunit' if: 'always() && steps.end-of-setup-sylius.outcome == ''success''' -# - -# name: 'Configure Behat' -# run: 'make behat-configure' -# if: 'always() && steps.end-of-setup-sylius.outcome == ''success''' -# - -# name: 'Run behat' -# run: 'vendor/bin/behat --strict --no-interaction -f progress || vendor/bin/behat --strict -vvv --no-interaction --rerun' -# if: 'always() && steps.end-of-setup-sylius.outcome == ''success''' - uses: actions/upload-artifact@v4 if: failure() From 43abd9a2b7b5adde16c779982fd567544a694821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Tue, 3 Mar 2026 15:55:46 +0100 Subject: [PATCH 07/36] PPSYL-174 - Add note for assets on 2.0.x --- README.md | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/README.md b/README.md index 3560a9bc..5fe94a5c 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,60 @@ In local environment, the plugin will not work properly because you will not be 🎉 You are now ready to add Payplug Payment method. In your back-office, go to `Configuration > Payment methods`, then click on `Create` and choose "**Payplug**". +### Assets installation (only for Sylius 2.0.x) + +On sylius 2.0.x, there is no automatic load of assets. +You need to add the following lines in `assets/shop/controllers.json` to allow Sylius to use our assets: + +```json +{ + "controllers": { + "@payplug/sylius-payplug-plugin": { + "oney-popin": { + "enabled": true, + "fetch": "lazy", + "autoimport": { + "@payplug/sylius-payplug-plugin/shop/dist/oney_common/index.css": true, + "@payplug/sylius-payplug-plugin/shop/dist/oney_popin/index.css": true + } + }, + "integrated-payment": { + "enabled": true, + "fetch": "lazy", + "autoimport": { + "@payplug/sylius-payplug-plugin/shop/dist/payment/integrated.css": true + } + }, + "oney-payment": { + "enabled": true, + "fetch": "lazy" + }, + "payment-logo": { + "enabled": true, + "fetch": "lazy" + }, + "checkout-select-payment": { + "enabled": true, + "fetch": "lazy", + "autoimport": { + "@payplug/sylius-payplug-plugin/shop/dist/payment/index.css": true + } + }, + "apple-pay": { + "enabled": true, + "fetch": "lazy" + } + } + }, + "entrypoints": [] +} +``` + +> [!NOTE] +> On Sylius Standard >= 2.1, assets are automatically loaded when you install the plugin with flex. +> If you are upgrading from a 2.0.x version, read the [upgrade guide](https://github.com/Sylius/Sylius/blob/2.1/UPGRADE-2.1.md#assets) + + ## Logs If you want to follow the logs in the production environment, you need to add the configuration in `config/packages/prod/monolog.yaml`, logs should be in `var/log/prod.log` which can be searched after the phrase `[Payum]` or `[Payplug]`: From 06d1a4f67f98b4612ff4e5d8a155974922cd0d30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Wed, 4 Mar 2026 16:21:58 +0100 Subject: [PATCH 08/36] PPSYL-177 - handle canceled by oney status --- src/Command/Handler/NotifyPaymentRequestHandler.php | 2 +- src/Command/Handler/StatusPaymentRequestHandler.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Command/Handler/NotifyPaymentRequestHandler.php b/src/Command/Handler/NotifyPaymentRequestHandler.php index b510bdc4..c0a139dc 100644 --- a/src/Command/Handler/NotifyPaymentRequestHandler.php +++ b/src/Command/Handler/NotifyPaymentRequestHandler.php @@ -70,7 +70,7 @@ public function __invoke(NotifyPaymentRequest $notifyPaymentRequest): void private function updatePaymentState(PaymentInterface $payment): void { match ($payment->getDetails()['status'] ?? '') { - PayPlugApiClientInterface::STATUS_ABORTED, PayPlugApiClientInterface::STATUS_CANCELED => $this->stateMachine + PayPlugApiClientInterface::STATUS_ABORTED, PayPlugApiClientInterface::STATUS_CANCELED, PayPlugApiClientInterface::STATUS_CANCELED_BY_ONEY => $this->stateMachine ->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL), PayPlugApiClientInterface::STATUS_AUTHORIZED => $this->stateMachine ->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_AUTHORIZE), diff --git a/src/Command/Handler/StatusPaymentRequestHandler.php b/src/Command/Handler/StatusPaymentRequestHandler.php index 2515ed95..6563d281 100644 --- a/src/Command/Handler/StatusPaymentRequestHandler.php +++ b/src/Command/Handler/StatusPaymentRequestHandler.php @@ -95,7 +95,7 @@ private function handleForcedStatus( private function updatePaymentState(PaymentInterface $payment): void { match ($payment->getDetails()['status'] ?? '') { - PayPlugApiClientInterface::STATUS_ABORTED, PayPlugApiClientInterface::STATUS_CANCELED => $this->stateMachine + PayPlugApiClientInterface::STATUS_ABORTED, PayPlugApiClientInterface::STATUS_CANCELED, PayPlugApiClientInterface::STATUS_CANCELED_BY_ONEY => $this->stateMachine ->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_CANCEL), PayPlugApiClientInterface::STATUS_AUTHORIZED => $this->stateMachine ->apply($payment, PaymentTransitions::GRAPH, PaymentTransitions::TRANSITION_AUTHORIZE), From c40e6e8cab15e35045fbc6587543a9a8762a4140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Tue, 3 Mar 2026 17:36:27 +0100 Subject: [PATCH 09/36] PPSYL-173 - Update namespace for gatewayConfigInterface --- src/Controller/IpnAction.php | 2 +- src/Controller/OneClickAction.php | 2 +- src/Creator/RefundUnitsCommandCreatorDecorator.php | 2 +- src/Form/Extension/PaymentTypeExtension.php | 2 +- src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php | 4 +--- src/MessageHandler/RefundPaymentGeneratedHandler.php | 2 +- src/PaymentProcessing/RefundPaymentProcessor.php | 5 +++-- .../AbstractSupportedRefundPaymentMethodsProvider.php | 2 +- src/Provider/OneySupportedPaymentChoiceProvider.php | 2 +- src/Provider/PaymentTokenProvider.php | 2 +- src/Provider/SupportedMethodsProvider.php | 2 +- .../SupportedRefundPaymentMethodsProviderDecorator.php | 2 +- src/Resolver/ApplePayPaymentMethodsResolverDecorator.php | 2 +- src/Resolver/OneyPaymentMethodsResolverDecorator.php | 2 +- src/Resolver/PaymentStateResolver.php | 2 +- 15 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/Controller/IpnAction.php b/src/Controller/IpnAction.php index 96fe35d6..04ec1fbe 100644 --- a/src/Controller/IpnAction.php +++ b/src/Controller/IpnAction.php @@ -17,9 +17,9 @@ use PayPlug\SyliusPayPlugPlugin\Handler\RefundNotificationHandler; use PayPlug\SyliusPayPlugPlugin\Repository\PaymentRepositoryInterface; use Payum\Core\Bridge\Spl\ArrayObject; -use Payum\Core\Model\GatewayConfigInterface; use Psr\Log\LoggerInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Controller/OneClickAction.php b/src/Controller/OneClickAction.php index 6ce2764a..f892db9a 100644 --- a/src/Controller/OneClickAction.php +++ b/src/Controller/OneClickAction.php @@ -10,7 +10,7 @@ use Payum\Core\ApiAwareInterface; use Payum\Core\GatewayAwareInterface; use Payum\Core\GatewayAwareTrait; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Payum\Core\Payum; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; diff --git a/src/Creator/RefundUnitsCommandCreatorDecorator.php b/src/Creator/RefundUnitsCommandCreatorDecorator.php index 45b67600..ab2e0d31 100644 --- a/src/Creator/RefundUnitsCommandCreatorDecorator.php +++ b/src/Creator/RefundUnitsCommandCreatorDecorator.php @@ -7,7 +7,7 @@ use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; diff --git a/src/Form/Extension/PaymentTypeExtension.php b/src/Form/Extension/PaymentTypeExtension.php index 5c0e96cf..9fce7b3c 100644 --- a/src/Form/Extension/PaymentTypeExtension.php +++ b/src/Form/Extension/PaymentTypeExtension.php @@ -8,7 +8,7 @@ use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Provider\OneySupportedPaymentChoiceProvider; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Bundle\CoreBundle\Form\Type\Checkout\PaymentType; use Sylius\Component\Core\Model\OrderInterface; use Symfony\Component\Form\AbstractTypeExtension; diff --git a/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php b/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php index fe5c8afb..de92698e 100644 --- a/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php +++ b/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php @@ -8,13 +8,11 @@ use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory; use PayPlug\SyliusPayPlugPlugin\Checker\OneyChecker; use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; -use Symfony\Component\Form\Form; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\UnexpectedTypeException; -use Symfony\Component\Validator\Exception\UnexpectedValueException; use Webmozart\Assert\Assert; final class IsOneyEnabledValidator extends ConstraintValidator diff --git a/src/MessageHandler/RefundPaymentGeneratedHandler.php b/src/MessageHandler/RefundPaymentGeneratedHandler.php index 1a70a72d..e749af7b 100644 --- a/src/MessageHandler/RefundPaymentGeneratedHandler.php +++ b/src/MessageHandler/RefundPaymentGeneratedHandler.php @@ -16,7 +16,7 @@ use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; use PayPlug\SyliusPayPlugPlugin\PaymentProcessing\RefundPaymentProcessor; use PayPlug\SyliusPayPlugPlugin\Repository\RefundHistoryRepositoryInterface; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Psr\Log\LoggerInterface; use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Component\Core\Model\OrderInterface; diff --git a/src/PaymentProcessing/RefundPaymentProcessor.php b/src/PaymentProcessing/RefundPaymentProcessor.php index db250fb2..acf6fb16 100644 --- a/src/PaymentProcessing/RefundPaymentProcessor.php +++ b/src/PaymentProcessing/RefundPaymentProcessor.php @@ -6,6 +6,7 @@ use Exception; use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface; +use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; use PayPlug\SyliusPayPlugPlugin\Entity\RefundHistory; use PayPlug\SyliusPayPlugPlugin\Gateway\AmericanExpressGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Gateway\ApplePayGatewayFactory; @@ -13,7 +14,7 @@ use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Repository\RefundHistoryRepositoryInterface; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Psr\Log\LoggerInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; @@ -30,7 +31,7 @@ #[Autoconfigure(public: true)] final class RefundPaymentProcessor implements PaymentProcessorInterface { - public $payPlugApiClient; + private PayPlugApiClientInterface $payPlugApiClient; public function __construct( private RequestStack $requestStack, diff --git a/src/Provider/AbstractSupportedRefundPaymentMethodsProvider.php b/src/Provider/AbstractSupportedRefundPaymentMethodsProvider.php index 9e31969c..2336059d 100644 --- a/src/Provider/AbstractSupportedRefundPaymentMethodsProvider.php +++ b/src/Provider/AbstractSupportedRefundPaymentMethodsProvider.php @@ -4,7 +4,7 @@ namespace PayPlug\SyliusPayPlugPlugin\Provider; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; diff --git a/src/Provider/OneySupportedPaymentChoiceProvider.php b/src/Provider/OneySupportedPaymentChoiceProvider.php index 0c05947c..64309253 100644 --- a/src/Provider/OneySupportedPaymentChoiceProvider.php +++ b/src/Provider/OneySupportedPaymentChoiceProvider.php @@ -6,7 +6,7 @@ use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Repository\PaymentMethodRepositoryInterface; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; class OneySupportedPaymentChoiceProvider diff --git a/src/Provider/PaymentTokenProvider.php b/src/Provider/PaymentTokenProvider.php index 622cc87b..91bb5b4d 100644 --- a/src/Provider/PaymentTokenProvider.php +++ b/src/Provider/PaymentTokenProvider.php @@ -4,7 +4,7 @@ namespace PayPlug\SyliusPayPlugPlugin\Provider; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Payum\Core\Payum; use Payum\Core\Security\TokenInterface; use Sylius\Component\Core\Model\PaymentInterface; diff --git a/src/Provider/SupportedMethodsProvider.php b/src/Provider/SupportedMethodsProvider.php index 7c66fc16..b8793187 100644 --- a/src/Provider/SupportedMethodsProvider.php +++ b/src/Provider/SupportedMethodsProvider.php @@ -4,7 +4,7 @@ namespace PayPlug\SyliusPayPlugPlugin\Provider; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\Component\Currency\Context\CurrencyContextInterface; use Webmozart\Assert\Assert; diff --git a/src/Provider/SupportedRefundPaymentMethodsProviderDecorator.php b/src/Provider/SupportedRefundPaymentMethodsProviderDecorator.php index f57aee57..40095917 100644 --- a/src/Provider/SupportedRefundPaymentMethodsProviderDecorator.php +++ b/src/Provider/SupportedRefundPaymentMethodsProviderDecorator.php @@ -5,7 +5,7 @@ namespace PayPlug\SyliusPayPlugPlugin\Provider; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; diff --git a/src/Resolver/ApplePayPaymentMethodsResolverDecorator.php b/src/Resolver/ApplePayPaymentMethodsResolverDecorator.php index 83aaeb29..a86b6533 100644 --- a/src/Resolver/ApplePayPaymentMethodsResolverDecorator.php +++ b/src/Resolver/ApplePayPaymentMethodsResolverDecorator.php @@ -7,7 +7,7 @@ use PayPlug\SyliusPayPlugPlugin\Checker\ApplePayCheckerInterface; use PayPlug\SyliusPayPlugPlugin\Gateway\ApplePayGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Provider\SupportedMethodsProvider; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\Payment; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\Component\Payment\Model\PaymentInterface as BasePaymentInterface; diff --git a/src/Resolver/OneyPaymentMethodsResolverDecorator.php b/src/Resolver/OneyPaymentMethodsResolverDecorator.php index 8e4bee24..e555f96e 100644 --- a/src/Resolver/OneyPaymentMethodsResolverDecorator.php +++ b/src/Resolver/OneyPaymentMethodsResolverDecorator.php @@ -6,7 +6,7 @@ use PayPlug\SyliusPayPlugPlugin\Checker\OneyCheckerInterface; use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\Payment; use Sylius\Component\Core\Model\PaymentMethodInterface; diff --git a/src/Resolver/PaymentStateResolver.php b/src/Resolver/PaymentStateResolver.php index bf8ee0ef..1b338106 100644 --- a/src/Resolver/PaymentStateResolver.php +++ b/src/Resolver/PaymentStateResolver.php @@ -10,7 +10,7 @@ use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory; use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; -use Payum\Core\Model\GatewayConfigInterface; +use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; From f095637b02ecc4c0abaa596a1777625255afb5d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Wed, 4 Mar 2026 10:23:00 +0100 Subject: [PATCH 10/36] PPSYL-173 - Improve refundUnits command creator decorator --- .../RefundUnitsCommandCreatorDecorator.php | 65 ++++++------------- 1 file changed, 21 insertions(+), 44 deletions(-) diff --git a/src/Creator/RefundUnitsCommandCreatorDecorator.php b/src/Creator/RefundUnitsCommandCreatorDecorator.php index ab2e0d31..2d593d4e 100644 --- a/src/Creator/RefundUnitsCommandCreatorDecorator.php +++ b/src/Creator/RefundUnitsCommandCreatorDecorator.php @@ -7,18 +7,15 @@ use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; -use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\OrderInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; use Sylius\Component\Core\Repository\OrderRepositoryInterface; use Sylius\Component\Payment\Repository\PaymentMethodRepositoryInterface; use Sylius\RefundPlugin\Command\RefundUnits; -use Sylius\RefundPlugin\Converter\RefundUnitsConverterInterface; use Sylius\RefundPlugin\Converter\Request\RequestToRefundUnitsConverterInterface; use Sylius\RefundPlugin\Creator\RequestCommandCreatorInterface; use Sylius\RefundPlugin\Exception\InvalidRefundAmount; -use Sylius\RefundPlugin\Model\RefundType; use Sylius\RefundPlugin\Model\UnitRefundInterface; use Symfony\Component\DependencyInjection\Attribute\AsDecorator; use Symfony\Component\DependencyInjection\Attribute\Autowire; @@ -31,12 +28,16 @@ class RefundUnitsCommandCreatorDecorator implements RequestCommandCreatorInterface { private const MINIMUM_REFUND_AMOUNT = 10; + private const SUPPORTED_METHODS = [ + PayPlugGatewayFactory::FACTORY_NAME, + OneyGatewayFactory::FACTORY_NAME + ]; public function __construct( #[AutowireDecorated] private RequestCommandCreatorInterface $decorated, #[Autowire('@sylius_refund.converter.request_to_refund_units')] - private RequestToRefundUnitsConverterInterface | RefundUnitsConverterInterface $requestToRefundUnitsConverter, + private RequestToRefundUnitsConverterInterface $requestToRefundUnitsConverter, private PaymentMethodRepositoryInterface $paymentMethodRepository, private OrderRepositoryInterface $orderRepository, private TranslatorInterface $translator, @@ -45,52 +46,31 @@ public function __construct( ) { } - public function fromRequest(Request $request): RefundUnits + /** + * @return RefundUnits + */ + public function fromRequest(Request $request): object { Assert::true($request->attributes->has('orderNumber'), 'Refunded order number not provided'); - - if ($this->requestToRefundUnitsConverter instanceof RefundUnitsConverterInterface) { - /** @phpstan-ignore-next-line */ - $units = $this->requestToRefundUnitsConverter->convert( - $request->request->has('sylius_refund_units') ? $request->request->all()['sylius_refund_units'] : [], - /* @phpstan-ignore-next-line */ - RefundType::orderItemUnit(), - ); - - /** @phpstan-ignore-next-line */ - $shipments = $this->requestToRefundUnitsConverter->convert( - $request->request->has('sylius_refund_shipments') ? $request->request->all()['sylius_refund_shipments'] : [], - /* @phpstan-ignore-next-line */ - RefundType::shipment(), - ); - - $units = array_merge($units, $shipments); - } else { - $units = $this->requestToRefundUnitsConverter->convert($request); - } - + $units = $this->requestToRefundUnitsConverter->convert($request); if ([] === $units) { throw InvalidRefundAmount::withValidationConstraint('sylius_refund.at_least_one_unit_should_be_selected_to_refund'); } - /** @var int $paymentMethodId */ $paymentMethodId = $request->request->get('sylius_refund_payment_method'); + if (null === $paymentMethodId) { + return $this->decorated->fromRequest($request); + } /** @var PaymentMethodInterface $paymentMethod */ $paymentMethod = $this->paymentMethodRepository->find($paymentMethodId); - - /** @var GatewayConfigInterface $gateway */ $gateway = $paymentMethod->getGatewayConfig(); - - if ( - PayPlugGatewayFactory::FACTORY_NAME !== $gateway->getFactoryName() && - OneyGatewayFactory::FACTORY_NAME !== $gateway->getFactoryName() - ) { - return $this->decorated->fromRequest($request); /** @phpstan-ignore-line */ + if (!\in_array($gateway?->getFactoryName(), self::SUPPORTED_METHODS, true)) { + return $this->decorated->fromRequest($request); } if (OneyGatewayFactory::FACTORY_NAME === $gateway->getFactoryName()) { - $orderNumber = $request->get('orderNumber'); + $orderNumber = $request->attributes->get('orderNumber'); Assert::string($orderNumber); /** @var OrderInterface|null $order */ @@ -101,30 +81,27 @@ public function fromRequest(Request $request): RefundUnits } $totalRefundRequest = $this->getTotalRefundAmount($units); - if ($totalRefundRequest < self::MINIMUM_REFUND_AMOUNT) { throw InvalidRefundAmount::withValidationConstraint($this->translator->trans('payplug_sylius_payplug_plugin.ui.refund_minimum_amount_requirement_not_met')); } - return $this->decorated->fromRequest($request); /** @phpstan-ignore-line */ + return $this->decorated->fromRequest($request); } + /** + * @param array $units + */ private function getTotalRefundAmount(array $units): int { $total = 0; foreach ($units as $unit) { - $total += $this->getAmount($unit); + $total += $unit->total(); } return $total; } - private function getAmount(UnitRefundInterface $unit): int - { - return $unit->total(); - } - private function canOneyRefundBeMade(OrderInterface $order): void { $lastPayment = $order->getLastPayment(PaymentInterface::STATE_COMPLETED); From 9209026f41b9acd0066dbea6056b0e32e3ceb761 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Wed, 4 Mar 2026 11:45:42 +0100 Subject: [PATCH 11/36] PPSYL-173 - Mark RefundPaymentGeneratedHandler as MessageHandler --- src/MessageHandler/RefundPaymentGeneratedHandler.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/MessageHandler/RefundPaymentGeneratedHandler.php b/src/MessageHandler/RefundPaymentGeneratedHandler.php index e749af7b..9bd4bc27 100644 --- a/src/MessageHandler/RefundPaymentGeneratedHandler.php +++ b/src/MessageHandler/RefundPaymentGeneratedHandler.php @@ -30,10 +30,12 @@ use Sylius\RefundPlugin\Exception\InvalidRefundAmount; use Sylius\RefundPlugin\StateResolver\RefundPaymentTransitions; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\Messenger\Attribute\AsMessageHandler; use Symfony\Contracts\Translation\TranslatorInterface; use Throwable; use Webmozart\Assert\Assert; +#[AsMessageHandler] final class RefundPaymentGeneratedHandler { public function __construct( From 0a3d9de8597c6610cca7a43ff28109958e226061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Wed, 4 Mar 2026 11:46:04 +0100 Subject: [PATCH 12/36] PPSYL-173 - Remove useless controllers for refund --- .../Admin/CompleteRefundPaymentAction.php | 73 ------------------- src/Action/Admin/RefundUnitsAction.php | 60 --------------- 2 files changed, 133 deletions(-) delete mode 100644 src/Action/Admin/CompleteRefundPaymentAction.php delete mode 100644 src/Action/Admin/RefundUnitsAction.php diff --git a/src/Action/Admin/CompleteRefundPaymentAction.php b/src/Action/Admin/CompleteRefundPaymentAction.php deleted file mode 100644 index 548d0029..00000000 --- a/src/Action/Admin/CompleteRefundPaymentAction.php +++ /dev/null @@ -1,73 +0,0 @@ -refundPaymentRepository->find($id); - - /** @var OrderInterface $order */ - $order = $this->orderRepository->findOneByNumber($orderNumber); - - try { - $this->messageBus->dispatch(new RefundPaymentGenerated( - $refundPayment->getId(), - $refundPayment->getOrder()->getNumber() ?? '', - $refundPayment->getAmount(), - $refundPayment->getCurrencyCode(), - $refundPayment->getPaymentMethod()->getId(), - $this->relatedPaymentIdProvider->getForRefundPayment($refundPayment), - )); - - if (self::COMPLETED_STATE !== $refundPayment->getState()) { - $this->refundPaymentCompletedStateApplier->apply($refundPayment); - } - $this->requestStack->getSession()->getFlashBag()->add('success', 'sylius_refund.refund_payment_completed'); - } catch (Throwable) { - $this->requestStack->getSession()->getFlashBag()->add('error', $this->translator->trans('payplug_sylius_payplug_plugin.ui.impossible_to_refund_this_payment')); - } - - return new RedirectResponse($this->router->generate( - 'sylius_admin_order_show', - ['id' => $order->getId()], - )); - } -} diff --git a/src/Action/Admin/RefundUnitsAction.php b/src/Action/Admin/RefundUnitsAction.php deleted file mode 100644 index 33d8e933..00000000 --- a/src/Action/Admin/RefundUnitsAction.php +++ /dev/null @@ -1,60 +0,0 @@ -commandBus->dispatch($this->commandCreator->fromRequest($request)); - - $this->requestStack->getSession()->getFlashBag()->add('success', 'sylius_refund.units_successfully_refunded'); - } catch (InvalidRefundAmount $exception) { - $this->requestStack->getSession()->getFlashBag()->add('error', $exception->getMessage()); - - $this->logger->error($exception->getMessage()); - } catch (HandlerFailedException $exception) { - /** @var Exception $previousException */ - $previousException = $exception->getPrevious(); - - $this->requestStack->getSession()->getFlashBag()->add('error', $previousException->getMessage()); - - $this->logger->error($previousException->getMessage()); - } - - return new RedirectResponse($this->router->generate( - 'sylius_refund_order_refunds_list', - ['orderNumber' => $request->attributes->get('orderNumber')], - )); - } -} From bdd1fdcd89d9dd0fcff67e700c2bcbe036703d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Wed, 4 Mar 2026 15:55:43 +0100 Subject: [PATCH 13/36] PPSYL-173 - Handle refund from notification --- .../Handler/NotifyPaymentRequestHandler.php | 49 +++++++++++++------ .../Handler/StatusPaymentRequestHandler.php | 5 +- .../Provider/NotifyPaymentProvider.php | 4 +- .../Provider/NotifyRefundPaymentProvider.php | 46 +++++++++++++++++ .../RefundPaymentHandler.php | 6 ++- 5 files changed, 91 insertions(+), 19 deletions(-) create mode 100644 src/OrderPay/Provider/NotifyRefundPaymentProvider.php diff --git a/src/Command/Handler/NotifyPaymentRequestHandler.php b/src/Command/Handler/NotifyPaymentRequestHandler.php index c0a139dc..441b431a 100644 --- a/src/Command/Handler/NotifyPaymentRequestHandler.php +++ b/src/Command/Handler/NotifyPaymentRequestHandler.php @@ -5,9 +5,11 @@ namespace PayPlug\SyliusPayPlugPlugin\Command\Handler; use Payplug\Resource\Payment; +use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactoryInterface; use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; use PayPlug\SyliusPayPlugPlugin\Command\NotifyPaymentRequest; use PayPlug\SyliusPayPlugPlugin\Handler\PaymentNotificationHandler; +use PayPlug\SyliusPayPlugPlugin\Handler\RefundNotificationHandler; use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\PaymentBundle\Provider\PaymentRequestProviderInterface; use Sylius\Component\Core\Model\PaymentInterface; @@ -21,7 +23,9 @@ class NotifyPaymentRequestHandler public function __construct( private PaymentRequestProviderInterface $paymentRequestProvider, private StateMachineInterface $stateMachine, + private PayPlugApiClientFactoryInterface $apiClientFactory, private PaymentNotificationHandler $paymentNotificationHandler, + private RefundNotificationHandler $refundNotificationHandler, ) {} public function __invoke(NotifyPaymentRequest $notifyPaymentRequest): void @@ -29,26 +33,41 @@ public function __invoke(NotifyPaymentRequest $notifyPaymentRequest): void $paymentRequest = $this->paymentRequestProvider->provide($notifyPaymentRequest); /** @var PaymentInterface $payment */ $payment = $paymentRequest->getPayment(); - if ($payment->getState() !== PaymentInterface::STATE_COMPLETED) { - // If the payment is already completed, we do not need to notify again - $this->stateMachine->apply( - $paymentRequest, - PaymentRequestTransitions::GRAPH, - PaymentRequestTransitions::TRANSITION_COMPLETE, - ); - - return; - } try { - // Payload contains what's send by payplug, no need to retrieve it from PayPlug - // @phpstan-ignore-next-line - cannot access offset content / http_request on mixed - $payplugPayment = Payment::fromAttributes(json_decode($paymentRequest->getPayload()['http_request']['content'] ?? '{}', true, 512, \JSON_THROW_ON_ERROR)); + $payload = $paymentRequest->getPayload(); + $content = $payload['http_request']['content'] ?? null; // @phpstan-ignore-line + if (!is_string($content) || '' === $content) { + throw new \LogicException('Invalid PayPlug notification payload.'); + } + + $method = $payment->getMethod(); + if (null === $method) { + throw new \LogicException('Payment method is not set for the payment.'); + } + + $client = $this->apiClientFactory->createForPaymentMethod($method); + $resource = $client->treat($content); + + if ($resource instanceof Payment && $payment->getState() === PaymentInterface::STATE_COMPLETED) { + // If the payment is already completed, we do not need to update it again + $this->stateMachine->apply( + $paymentRequest, + PaymentRequestTransitions::GRAPH, + PaymentRequestTransitions::TRANSITION_COMPLETE, + ); + + return; + } + $details = new \ArrayObject($payment->getDetails()); - $this->paymentNotificationHandler->treat($payment, $payplugPayment, $details); + $this->paymentNotificationHandler->treat($payment, $resource, $details); + $this->refundNotificationHandler->treat($payment, $resource, $details); $payment->setDetails($details->getArrayCopy()); - $this->updatePaymentState($payment); + if ($resource instanceof Payment) { + $this->updatePaymentState($payment); + } $this->stateMachine->apply( $paymentRequest, diff --git a/src/Command/Handler/StatusPaymentRequestHandler.php b/src/Command/Handler/StatusPaymentRequestHandler.php index 6563d281..1f2c47fe 100644 --- a/src/Command/Handler/StatusPaymentRequestHandler.php +++ b/src/Command/Handler/StatusPaymentRequestHandler.php @@ -61,7 +61,10 @@ public function __invoke(StatusPaymentRequest $statusPaymentRequest): void $this->paymentNotificationHandler->treat($payment, $payplugPayment, $details); $payment->setDetails($details->getArrayCopy()); - $this->updatePaymentState($payment); + if ($payment->getState() !== PaymentInterface::STATE_COMPLETED) { + // If is already completed, do not try to update it again (updated by notification) + $this->updatePaymentState($payment); + } // Mark the PaymentRequest as completed $this->stateMachine->apply( diff --git a/src/OrderPay/Provider/NotifyPaymentProvider.php b/src/OrderPay/Provider/NotifyPaymentProvider.php index 5bae04a0..eb84a9aa 100644 --- a/src/OrderPay/Provider/NotifyPaymentProvider.php +++ b/src/OrderPay/Provider/NotifyPaymentProvider.php @@ -49,7 +49,9 @@ public function supports(Request $request, PaymentMethodInterface $paymentMethod { return \str_contains($paymentMethod->getGatewayConfig()?->getFactoryName() ?? '', 'payplug') && $request->getPayload()->has('id') && - $request->getPayload()->has('metadata'); + $request->getPayload()->has('metadata') && + $request->getPayload()->has('object') && + $request->getPayload()->get('object') === 'payment'; } private function getOrderFromReference(string $orderReference): OrderInterface diff --git a/src/OrderPay/Provider/NotifyRefundPaymentProvider.php b/src/OrderPay/Provider/NotifyRefundPaymentProvider.php new file mode 100644 index 00000000..50cdbee6 --- /dev/null +++ b/src/OrderPay/Provider/NotifyRefundPaymentProvider.php @@ -0,0 +1,46 @@ +getPayload()->get('payment_id'); + if (null === $paymentId) { + throw new \InvalidArgumentException('Order number not found in request payload'); + } + + $payment = $this->paymentRepository->findOneByPayPlugPaymentId($paymentId); + if (null === $payment || $payment->getMethod() !== $paymentMethod) { + throw new \InvalidArgumentException(sprintf('Payment with ID "%s" not found', $paymentId)); + } + + return $payment; + } + + public function supports(Request $request, PaymentMethodInterface $paymentMethod): bool + { + return \str_contains($paymentMethod->getGatewayConfig()?->getFactoryName() ?? '', 'payplug') && + $request->getPayload()->has('id') && + $request->getPayload()->has('metadata') && + $request->getPayload()->has('object') && + $request->getPayload()->get('object') === 'refund'; + } +} diff --git a/src/PaymentProcessing/RefundPaymentHandler.php b/src/PaymentProcessing/RefundPaymentHandler.php index 4a8875d0..fa6ef795 100644 --- a/src/PaymentProcessing/RefundPaymentHandler.php +++ b/src/PaymentProcessing/RefundPaymentHandler.php @@ -54,8 +54,10 @@ public function fromRequest(Refund $refund, PaymentInterface $payment): RefundUn return new RefundUnits( $payment->getOrder()->getNumber(), - $this->parseIdsToUnitRefunds($items, RefundType::orderItemUnit(), OrderItemUnitRefund::class), - $this->parseIdsToUnitRefunds($shipments, RefundType::shipment(), ShipmentRefund::class), + array_merge( + $this->parseIdsToUnitRefunds($items, RefundType::orderItemUnit(), OrderItemUnitRefund::class), + $this->parseIdsToUnitRefunds($shipments, RefundType::shipment(), ShipmentRefund::class) + ), $payment->getMethod()->getId(), '', ); From 2ed6ac60e141583f905f3fcfad2b743f2120cf49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Wed, 4 Mar 2026 15:58:00 +0100 Subject: [PATCH 14/36] Mark payum classes as deprecated --- src/Action/CaptureAction.php | 1 + src/Action/ConvertPaymentAction.php | 1 + src/Action/NotifyAction.php | 1 + src/Action/StatusAction.php | 1 + src/Controller/IpnAction.php | 1 + src/Controller/OneClickAction.php | 2 +- 6 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Action/CaptureAction.php b/src/Action/CaptureAction.php index 1fbb8125..8030246c 100644 --- a/src/Action/CaptureAction.php +++ b/src/Action/CaptureAction.php @@ -42,6 +42,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; use Webmozart\Assert\Assert; +/** @deprecated */ #[AutoconfigureTag( name: 'payum.action', attributes: [ diff --git a/src/Action/ConvertPaymentAction.php b/src/Action/ConvertPaymentAction.php index 4b00ebe3..663b9b1a 100644 --- a/src/Action/ConvertPaymentAction.php +++ b/src/Action/ConvertPaymentAction.php @@ -18,6 +18,7 @@ use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; +/** @deprecated */ #[AutoconfigureTag( name: 'payum.action', attributes: [ diff --git a/src/Action/NotifyAction.php b/src/Action/NotifyAction.php index 3f5fc36a..e90dac54 100644 --- a/src/Action/NotifyAction.php +++ b/src/Action/NotifyAction.php @@ -26,6 +26,7 @@ use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; use Symfony\Component\DependencyInjection\Attribute\Autowire; +/** @deprecated */ #[AutoconfigureTag( name: 'payum.action', attributes: [ diff --git a/src/Action/StatusAction.php b/src/Action/StatusAction.php index 6e356921..135af4a6 100644 --- a/src/Action/StatusAction.php +++ b/src/Action/StatusAction.php @@ -28,6 +28,7 @@ use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; use Symfony\Component\HttpFoundation\RequestStack; +/** @deprecated */ #[AutoconfigureTag( name: 'payum.action', attributes: [ diff --git a/src/Controller/IpnAction.php b/src/Controller/IpnAction.php index 04ec1fbe..ea42aa13 100644 --- a/src/Controller/IpnAction.php +++ b/src/Controller/IpnAction.php @@ -27,6 +27,7 @@ use Symfony\Component\Routing\Attribute\Route; use Webmozart\Assert\Assert; +/** @deprecated */ #[AsController] class IpnAction { diff --git a/src/Controller/OneClickAction.php b/src/Controller/OneClickAction.php index f892db9a..46f4011d 100644 --- a/src/Controller/OneClickAction.php +++ b/src/Controller/OneClickAction.php @@ -23,7 +23,7 @@ use Symfony\Component\Routing\Attribute\Route; /** - * TODO : !!! Check if this controller is still needed + * @deprecated - Used by payum */ #[AsController] class OneClickAction extends AbstractController implements GatewayAwareInterface, ApiAwareInterface From d83bee698db32abd3fff7dab0f59f66ffdc9c371 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Mon, 9 Mar 2026 14:26:27 +0100 Subject: [PATCH 15/36] PPSYL-173 - Fix phpstan issues --- src/Controller/IpnAction.php | 5 +++++ src/Controller/OneClickAction.php | 6 +++++- src/Creator/RefundUnitsCommandCreatorDecorator.php | 12 ++++++------ src/PaymentProcessing/RefundPaymentHandler.php | 2 +- src/PaymentProcessing/RefundPaymentProcessor.php | 3 +++ src/Provider/PaymentTokenProvider.php | 2 +- src/Repository/PaymentRepository.php | 5 +++-- src/Repository/PaymentRepositoryInterface.php | 2 +- 8 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Controller/IpnAction.php b/src/Controller/IpnAction.php index ea42aa13..ee7ca7c5 100644 --- a/src/Controller/IpnAction.php +++ b/src/Controller/IpnAction.php @@ -61,6 +61,11 @@ public function __invoke(Request $request): JsonResponse } $payment = $this->paymentRepository->findOneByPayPlugPaymentId($details['id']); + + if (null === $payment) { + return new JsonResponse(null, Response::HTTP_UNAUTHORIZED); + } + $paymentMethod = $payment->getMethod(); Assert::isInstanceOf($paymentMethod, PaymentMethodInterface::class); diff --git a/src/Controller/OneClickAction.php b/src/Controller/OneClickAction.php index 46f4011d..166fc383 100644 --- a/src/Controller/OneClickAction.php +++ b/src/Controller/OneClickAction.php @@ -51,9 +51,13 @@ public function __invoke(Request $request): Response /** @var GatewayConfigInterface $paymentGateway */ $paymentGateway = $paymentMethod->getGatewayConfig(); + $gatewayName = $paymentGateway->getGatewayName(); + if (null === $gatewayName) { + throw new \InvalidArgumentException('Payment gateway name cannot be null'); + } $captureToken = $this->payum->getTokenFactory()->createCaptureToken( - $paymentGateway->getGatewayName(), + $gatewayName, $payment, 'sylius_shop_order_thank_you', [], diff --git a/src/Creator/RefundUnitsCommandCreatorDecorator.php b/src/Creator/RefundUnitsCommandCreatorDecorator.php index 2d593d4e..396ac3ac 100644 --- a/src/Creator/RefundUnitsCommandCreatorDecorator.php +++ b/src/Creator/RefundUnitsCommandCreatorDecorator.php @@ -56,17 +56,17 @@ public function fromRequest(Request $request): object if ([] === $units) { throw InvalidRefundAmount::withValidationConstraint('sylius_refund.at_least_one_unit_should_be_selected_to_refund'); } - /** @var int $paymentMethodId */ - $paymentMethodId = $request->request->get('sylius_refund_payment_method'); - if (null === $paymentMethodId) { - return $this->decorated->fromRequest($request); + + $paymentMethodId = $request->request->getInt('sylius_refund_payment_method'); + if (0 === $paymentMethodId) { + return $this->decorated->fromRequest($request); // @phpstan-ignore-line } /** @var PaymentMethodInterface $paymentMethod */ $paymentMethod = $this->paymentMethodRepository->find($paymentMethodId); $gateway = $paymentMethod->getGatewayConfig(); if (!\in_array($gateway?->getFactoryName(), self::SUPPORTED_METHODS, true)) { - return $this->decorated->fromRequest($request); + return $this->decorated->fromRequest($request); // @phpstan-ignore-line } if (OneyGatewayFactory::FACTORY_NAME === $gateway->getFactoryName()) { @@ -85,7 +85,7 @@ public function fromRequest(Request $request): object throw InvalidRefundAmount::withValidationConstraint($this->translator->trans('payplug_sylius_payplug_plugin.ui.refund_minimum_amount_requirement_not_met')); } - return $this->decorated->fromRequest($request); + return $this->decorated->fromRequest($request); // @phpstan-ignore-line } /** diff --git a/src/PaymentProcessing/RefundPaymentHandler.php b/src/PaymentProcessing/RefundPaymentHandler.php index fa6ef795..70d80e53 100644 --- a/src/PaymentProcessing/RefundPaymentHandler.php +++ b/src/PaymentProcessing/RefundPaymentHandler.php @@ -58,7 +58,7 @@ public function fromRequest(Refund $refund, PaymentInterface $payment): RefundUn $this->parseIdsToUnitRefunds($items, RefundType::orderItemUnit(), OrderItemUnitRefund::class), $this->parseIdsToUnitRefunds($shipments, RefundType::shipment(), ShipmentRefund::class) ), - $payment->getMethod()->getId(), + $payment->getMethod()->getId(), // @phpstan-ignore-line '', ); } diff --git a/src/PaymentProcessing/RefundPaymentProcessor.php b/src/PaymentProcessing/RefundPaymentProcessor.php index acf6fb16..d9c62382 100644 --- a/src/PaymentProcessing/RefundPaymentProcessor.php +++ b/src/PaymentProcessing/RefundPaymentProcessor.php @@ -15,6 +15,7 @@ use PayPlug\SyliusPayPlugPlugin\Gateway\PayPlugGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Repository\RefundHistoryRepositoryInterface; use Sylius\Component\Payment\Model\GatewayConfigInterface; +use Webmozart\Assert\Assert; use Psr\Log\LoggerInterface; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; @@ -58,6 +59,7 @@ public function process(PaymentInterface $payment): void { $this->prepare($payment); $details = $payment->getDetails(); + Assert::string($details['payment_id']); try { $this->payPlugApiClient->refundPayment($details['payment_id']); @@ -74,6 +76,7 @@ public function processWithAmount(PaymentInterface $payment, int $amount, int $r { $this->prepare($payment); $details = $payment->getDetails(); + Assert::string($details['payment_id']); try { $refund = $this->payPlugApiClient->refundPaymentWithAmount($details['payment_id'], $amount, $refundId); diff --git a/src/Provider/PaymentTokenProvider.php b/src/Provider/PaymentTokenProvider.php index 91bb5b4d..622cc87b 100644 --- a/src/Provider/PaymentTokenProvider.php +++ b/src/Provider/PaymentTokenProvider.php @@ -4,7 +4,7 @@ namespace PayPlug\SyliusPayPlugPlugin\Provider; -use Sylius\Component\Payment\Model\GatewayConfigInterface; +use Payum\Core\Model\GatewayConfigInterface; use Payum\Core\Payum; use Payum\Core\Security\TokenInterface; use Sylius\Component\Core\Model\PaymentInterface; diff --git a/src/Repository/PaymentRepository.php b/src/Repository/PaymentRepository.php index c55fcacc..d3df9471 100644 --- a/src/Repository/PaymentRepository.php +++ b/src/Repository/PaymentRepository.php @@ -25,14 +25,15 @@ public function findAllActiveByGatewayFactoryName(string $gatewayFactoryName): a ; } - public function findOneByPayPlugPaymentId(string $payplugPaymentId): PaymentInterface + public function findOneByPayPlugPaymentId(string $payplugPaymentId): ?PaymentInterface { + /** @var PaymentInterface|null */ return $this->createQueryBuilder('o') ->where('o.details LIKE :payplugPaymentId') ->setParameter('payplugPaymentId', '%' . $payplugPaymentId . '%') ->getQuery() ->setMaxResults(1) - ->getSingleResult() + ->getOneOrNullResult() ; } diff --git a/src/Repository/PaymentRepositoryInterface.php b/src/Repository/PaymentRepositoryInterface.php index 6f35d267..b8b783e6 100644 --- a/src/Repository/PaymentRepositoryInterface.php +++ b/src/Repository/PaymentRepositoryInterface.php @@ -11,7 +11,7 @@ interface PaymentRepositoryInterface extends BasePaymentRepositoryInterface { public function findAllActiveByGatewayFactoryName(string $gatewayFactoryName): array; - public function findOneByPayPlugPaymentId(string $payplugPaymentId): PaymentInterface; + public function findOneByPayPlugPaymentId(string $payplugPaymentId): ?PaymentInterface; /** * @return array From 33583f686397cbc909c08ee5e0d82f7c214645e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Wed, 18 Mar 2026 11:38:18 +0100 Subject: [PATCH 16/36] [Checkout] Refact switch methods --- .../checkout-select-payment_controller.js | 120 ++++++++++++++---- assets/shop/dist/payment/index.css | 44 ++----- templates/shop/integrated/index.html.twig | 1 + templates/shop/select_payment/_oney.html.twig | 42 +++--- .../shop/select_payment/_payplug.html.twig | 6 +- .../shop/select_payment/choice.html.twig | 14 +- 6 files changed, 142 insertions(+), 85 deletions(-) diff --git a/assets/shop/controllers/checkout-select-payment_controller.js b/assets/shop/controllers/checkout-select-payment_controller.js index e2a21c74..852bb954 100644 --- a/assets/shop/controllers/checkout-select-payment_controller.js +++ b/assets/shop/controllers/checkout-select-payment_controller.js @@ -4,46 +4,120 @@ import $ from 'jquery'; /* stimulusFetch: 'lazy' */ export default class extends Controller { static targets = ['trigger']; - form= null; + form = null; connect() { + this.form = this.element.closest('form') || document.querySelector('form[name*="checkout_select_payment"]'); + this.findNextStepButton(); + + this.handleStateChange = this.handleStateChange.bind(this); + this.element.addEventListener('payment-method-state-change', this.handleStateChange); + this.toggleGateway(); - this.form = document.querySelector('form[name*="checkout_select_payment"]'); - this.form.addEventListener('submit', (event) => { - this.handleForm(); - }); + if (this.form) { + this.form.addEventListener('submit', (event) => { + this.handleForm(); + }); + } + } + + disconnect() { + this.element.removeEventListener('payment-method-state-change', this.handleStateChange); + } + + handleStateChange(event) { + this.updateNextStepButtonVisibility(event.target); } + + findNextStepButton() { + if (!this.form) return; + this.nextStepButton = this.form.querySelector('button[type="submit"], button:not([type])'); + } + toggleGateway() { - let checkedPaymentMethodInput; + const $inputs = $(this.element).find('input[id*="checkout_select_payment_payments"]'); + const checkedInput = $inputs.filter(':checked')[0]; - $(this.triggerTargets).each((k, v) => { - const paymentMethodInputId = $(v).data('payment-input-id'); - checkedPaymentMethodInput = $(`#${paymentMethodInputId}:checked`); - if (checkedPaymentMethodInput.length) { - return false; - } - }) + // Hide all initially + this.triggerTargets.forEach(target => $(target).hide()); - if (checkedPaymentMethodInput?.length) { - $(`.payment-method-choice[data-payment-input-id="${$(checkedPaymentMethodInput).attr('id')}"]`).show(); + if (checkedInput) { + const currentTarget = this.triggerTargets.find(target => target.dataset.paymentInputId === checkedInput.id); + if (currentTarget) { + $(currentTarget).show(); + this.updateNextStepButtonVisibility(currentTarget); + } else { + this.toggleNextStepButton(true); + } } - const $inputs = $('input[id*="checkout_select_payment_payments"]'); $inputs.on('change', (event) => { - const clickedPaymentMethodId = $(event.currentTarget).attr('id'); - $('.payment-method-choice').slideUp(); // Hide others - $(`.payment-method-choice[data-payment-input-id="${clickedPaymentMethodId}"]`).slideDown(); // Show current + const clickedPaymentMethodId = event.currentTarget.id; + + // Hide all choices + this.triggerTargets.forEach(target => $(target).slideUp()); + + const currentTarget = this.triggerTargets.find(target => target.dataset.paymentInputId === clickedPaymentMethodId); + if (currentTarget) { + $(currentTarget).slideDown(() => { + this.updateNextStepButtonVisibility(currentTarget); + }); + } else { + this.toggleNextStepButton(true); + } }); } + + updateNextStepButtonVisibility(container) { + let targetContainer = container; + + if (!targetContainer || !targetContainer.dataset.paymentInputId) { + // Fallback to currently checked input + const checkedInput = this.element.querySelector('input[id*="checkout_select_payment_payments"]:checked'); + if (checkedInput) { + targetContainer = this.triggerTargets.find(target => target.dataset.paymentInputId === checkedInput.id); + } + } + + if (!targetContainer) { + this.toggleNextStepButton(true); + return; + } + + const handlesSubmit = targetContainer.dataset.paymentHandlesSubmit === 'true' || + targetContainer.querySelector('[data-payment-handles-submit="true"]') !== null; + + this.toggleNextStepButton(!handlesSubmit); + } + + toggleNextStepButton(show) { + if (!this.nextStepButton) { + this.findNextStepButton(); + } + if (!this.nextStepButton) return; + + if (show) { + this.nextStepButton.classList.remove('d-none'); + this.nextStepButton.disabled = false; + this.nextStepButton.style.display = ''; + } else { + this.nextStepButton.classList.add('d-none'); + this.nextStepButton.disabled = true; + this.nextStepButton.style.display = 'none'; + } + } + handleForm() { if ($('.checkbox-oney :radio:checked').length) { - $('.checkbox-payplug').closest('.payment-item').find('.payment-choice__input:checked').prop('checked', false); + $('.checkbox-payplug').closest('.oney-payment-choice__item, .payplug-payment-choice__item, .form-check, .payment-item').find('.payment-choice__input:checked').prop('checked', false); } else if ($('.checkbox-payplug :radio:checked').length) { - $('.checkbox-oney').closest('.payment-item').find('.payment-choice__input:checked').prop('checked', false); + $('.checkbox-oney').closest('.oney-payment-choice__item, .payplug-payment-choice__item, .form-check, .payment-item').find('.payment-choice__input:checked').prop('checked', false); } - $('input#payplug_choice_card_other').attr('disabled', true); - this.form.submit(); + const otherCardOther = document.querySelector('input#payplug_choice_card_other'); + if (otherCardOther) { + otherCardOther.disabled = true; + } } } diff --git a/assets/shop/dist/payment/index.css b/assets/shop/dist/payment/index.css index bb6bfa52..95e0bf4b 100644 --- a/assets/shop/dist/payment/index.css +++ b/assets/shop/dist/payment/index.css @@ -4,10 +4,6 @@ display: flex !important } -.oney-payment-choice, .payplug-payment-choice { - display: none -} - .oney-payment-choice__container, .payplug-payment-choice__container { margin: 1rem 0; display: flex @@ -103,11 +99,11 @@ padding: 0 !important } -.payment-item .oney-logo[src*=without-fees] { +.oney-logo[src*=without-fees] { max-width: 200px } -.payment-item apple-pay-button { +apple-pay-button { --apple-pay-button-width: 222px; --apple-pay-button-height: 40px; --apple-pay-button-border-radius: 4px; @@ -115,29 +111,12 @@ --apple-pay-button-box-sizing: border-box; min-width: 140px; max-width: 100%; - transition: background-color .3s ease-in-out; - display: none -} - -.payment-item apple-pay-button.enabled { - display: block } -.payment-item label { +label { cursor: pointer } -.payment-item .bancontact-method label, .payment-item .apple-pay-method label, .payment-item .american-express-method label { - position: relative -} - -.payment-item .bancontact-method label img, .payment-item .apple-pay-method label img, .payment-item .american-express-method label img { - margin-left: .5em; - position: absolute; - top: -12px; - left: 100% -} - .payplug-payment-choice__container { flex-direction: column } @@ -174,16 +153,17 @@ content: ""; background-image: var(--logo); display: inline-block; - width: 60px; - height: 25px; + width: 45px; + height: 40px; + margin-top: -6px; + margin-left: 10px; background-repeat: no-repeat; - background-position: 0 -11px; - background-size: 60px 45px; - float: right + background-position: center; + background-size: contain; + float: right; } - -.bancontact-label::after { - margin-left: 5px +.payment-label-with-image.oney-label::after { + width: 130px; } @media screen and (max-width: 991px) { diff --git a/templates/shop/integrated/index.html.twig b/templates/shop/integrated/index.html.twig index 1329c28e..9cd1c26e 100644 --- a/templates/shop/integrated/index.html.twig +++ b/templates/shop/integrated/index.html.twig @@ -26,6 +26,7 @@
{{ 'sylius.ui.loading'|trans }} diff --git a/templates/shop/select_payment/_oney.html.twig b/templates/shop/select_payment/_oney.html.twig index fc5d2cd8..1a6696c4 100644 --- a/templates/shop/select_payment/_oney.html.twig +++ b/templates/shop/select_payment/_oney.html.twig @@ -22,30 +22,36 @@ {% set values = values|merge(completeInfoRoute) %} {% endif %} +{% set logo_path = 'bundles/payplugsyliuspayplugplugin/assets/oney/3x4x-without-fees.svg' %} +{% if ((form.parent.parent.oney_payment_choice.vars.choices|last).value|split('oney_')|last) not in constant('PayPlug\\SyliusPayPlugPlugin\\Gateway\\OneyGatewayFactory::ONEY_WITHOUT_FEES_CHOICES') %} + {% set logo_path = 'bundles/payplugsyliuspayplugplugin/assets/oney/3x4x.svg' %} +{% endif %}
- - {% if factoryName == oneyFactoryName %} - - {% endif %} - +{# #} +{# {% if factoryName == oneyFactoryName %}#} +{# #} +{# {% endif %}#} +{# #} {% if showOney %} -
+
{{ form_row(form.parent.parent.oney_payment_choice) }}
{% endif %}
- diff --git a/templates/shop/select_payment/_payplug.html.twig b/templates/shop/select_payment/_payplug.html.twig index 98d2b3e1..32b60ed1 100644 --- a/templates/shop/select_payment/_payplug.html.twig +++ b/templates/shop/select_payment/_payplug.html.twig @@ -27,11 +27,7 @@ {% if hasSavedCards %} -
+
{{ form_row(form.parent.parent.payplug_card_choice) }}
{% endif %} diff --git a/templates/shop/select_payment/choice.html.twig b/templates/shop/select_payment/choice.html.twig index dbda00d3..9e39f4f6 100644 --- a/templates/shop/select_payment/choice.html.twig +++ b/templates/shop/select_payment/choice.html.twig @@ -3,10 +3,10 @@ {% set order = hookable_metadata.context.order %} {% set factoryName = method.gatewayConfig.factoryName %} -{% set oneyFactoryName = constant('PayPlug\\SyliusPayPlugPlugin\\Gateway\\OneyGatewayFactory::FACTORY_NAME') %} -{% set payplugFactoryName = constant('PayPlug\\SyliusPayPlugPlugin\\Gateway\\PayplugGatewayFactory::FACTORY_NAME') %} -{% set bancontactFactoryName = constant('PayPlug\\SyliusPayPlugPlugin\\Gateway\\BancontactGatewayFactory::FACTORY_NAME') %} -{% set applePayFactoryName = constant('PayPlug\\SyliusPayPlugPlugin\\Gateway\\ApplePayGatewayFactory::FACTORY_NAME') %} -{% set americanExpressFactoryName = constant('PayPlug\\SyliusPayPlugPlugin\\Gateway\\AmericanExpressGatewayFactory::FACTORY_NAME') %} - -{% hook '#' ~ factoryName %} +
+ {% hook '#' ~ factoryName %} +
From cb18a9d178cc8b5db8d919a68f70a0182418c142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Wed, 18 Mar 2026 11:38:51 +0100 Subject: [PATCH 17/36] [Checkout] Update logos --- public/assets/american-express/logo.svg | 26 +------------------------ public/assets/apple-pay/logo.svg | 6 +----- public/assets/bancontact/logo.svg | 2 +- 3 files changed, 3 insertions(+), 31 deletions(-) diff --git a/public/assets/american-express/logo.svg b/public/assets/american-express/logo.svg index 5a587766..b6dfc5b8 100644 --- a/public/assets/american-express/logo.svg +++ b/public/assets/american-express/logo.svg @@ -1,25 +1 @@ - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/public/assets/apple-pay/logo.svg b/public/assets/apple-pay/logo.svg index 1240e7d4..5a86310e 100644 --- a/public/assets/apple-pay/logo.svg +++ b/public/assets/apple-pay/logo.svg @@ -1,5 +1 @@ - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/public/assets/bancontact/logo.svg b/public/assets/bancontact/logo.svg index 99d4989c..1ab4d0f9 100644 --- a/public/assets/bancontact/logo.svg +++ b/public/assets/bancontact/logo.svg @@ -1 +1 @@ - + \ No newline at end of file From 538738ee375137cb2eceb52fe36fb901abeae2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Wed, 18 Mar 2026 11:39:54 +0100 Subject: [PATCH 18/36] [Checkout] Unminify integrated payment.css --- assets/shop/dist/payment/integrated.css | 303 +++++++++++++++++++++++- 1 file changed, 302 insertions(+), 1 deletion(-) diff --git a/assets/shop/dist/payment/integrated.css b/assets/shop/dist/payment/integrated.css index 9a2805f7..382da93b 100644 --- a/assets/shop/dist/payment/integrated.css +++ b/assets/shop/dist/payment/integrated.css @@ -1 +1,302 @@ -.payplugIntegratedPayment{justify-self:center;display:none}.payplugIntegratedPayment *{font-family:Poppins,Arial,sans-serif!important}.payplugIntegratedPayment--loaded{width:100%;max-width:400px;flex-wrap:wrap;justify-content:space-between;margin-top:20px;margin-bottom:0;display:flex;position:relative}.payplugIntegratedPayment__select{height:36px;width:100%;border:1px solid #ccc;border-radius:5px;margin:0 0 10px;padding:0 8px}.payplugIntegratedPayment__container{width:100%;margin:0 0 10px;padding:0;display:flex;position:relative}.payplugIntegratedPayment__container--cardHolder,.payplugIntegratedPayment__container--pan,.payplugIntegratedPayment__container--exp,.payplugIntegratedPayment__container--cvv{height:40px;cursor:text;border:1px solid #d5d6d8;border-radius:2px;padding:0 16px 0 50px;line-height:40px}.payplugIntegratedPayment__container--cardHolder:before,.payplugIntegratedPayment__container--pan:before,.payplugIntegratedPayment__container--exp:before,.payplugIntegratedPayment__container--cvv:before{content:"";width:24px;height:24px;background:#95999e 50%/100% no-repeat;position:absolute;top:20%;left:16px}.payplugIntegratedPayment__container--cardHolder:focus,.payplugIntegratedPayment__container--pan:focus,.payplugIntegratedPayment__container--exp:focus,.payplugIntegratedPayment__container--cvv:focus{border-color:#2b343d}.payplugIntegratedPayment__container--cardHolder--invalid,.payplugIntegratedPayment__container--pan--invalid,.payplugIntegratedPayment__container--exp--invalid,.payplugIntegratedPayment__container--cvv--invalid{border-color:#e91932}.payplugIntegratedPayment__container--cardHolder:before{-webkit-mask-image:url(account.039342ad.svg);mask-image:url(account.039342ad.svg)}.payplugIntegratedPayment__container--pan:before{-webkit-mask-image:url(card.0d2bd9bc.svg);mask-image:url(card.0d2bd9bc.svg)}.payplugIntegratedPayment__container--exp:before{-webkit-mask-image:url(calendar.3c23bb16.svg);mask-image:url(calendar.3c23bb16.svg)}.payplugIntegratedPayment__container--cvv:before{-webkit-mask-image:url(lock.fe8a73cd.svg);mask-image:url(lock.fe8a73cd.svg)}.payplugIntegratedPayment__container--exp,.payplugIntegratedPayment__container--cvv{max-width:calc(50% - 2px);display:inline-block}.payplugIntegratedPayment__container--scheme{text-transform:uppercase;height:22px;justify-content:space-between;align-items:center;margin:10px 0;font-size:14px;font-weight:700}.payplugIntegratedPayment__container--saveCard{height:auto;align-items:center;padding:10px 0 0;display:flex}.payplugIntegratedPayment__container--saveCard input{display:none}.payplugIntegratedPayment__container--saveCard input:checked+label span:before{opacity:1}.payplugIntegratedPayment__container--saveCard label{cursor:pointer;color:#918f8f;margin:0!important;font-size:12px!important}.payplugIntegratedPayment__container--saveCard label span{cursor:pointer;height:16px;-o-transition:border .4s;width:16px;border:1px solid #d5d6d8;border-radius:2px;margin:0 10px -3px 0;transition:border .4s;display:inline-block;position:relative}.payplugIntegratedPayment__container--saveCard label span:before{content:"";height:5px;opacity:0;width:10px;border-top:none;border-bottom:2.5px solid #2b343d;border-left:2.5px solid #2b343d;border-right:none;border-radius:1px;transition:opacity .4s;display:block;position:absolute;top:50%;left:50%;transform:translate(-50%,-55%)rotate(-48deg)}.payplugIntegratedPayment__container--saveCard label:hover{color:#2b343d;transition:all .1s}.payplugIntegratedPayment__container--saveCard label:hover span{border-color:#2b343d;transition:all .1s}.payplugIntegratedPayment__container--transaction{align-items:center;margin-top:10px}.payplugIntegratedPayment__container--transaction .transaction-label{vertical-align:super;margin-left:5px;font-size:12px}.payplugIntegratedPayment__container img.lock-icon{width:18px;float:left!important}.payplugIntegratedPayment__container img.payplug-logo{width:80px;height:auto;vertical-align:text-top;margin-left:6px;display:inline-block;float:inherit!important}.payplugIntegratedPayment__container--privacy-policy{text-align:center;display:inline-block}.payplugIntegratedPayment__container--privacy-policy a{color:#918f8f;font-size:14px}.payplugIntegratedPayment__schemes{width:115px;flex-wrap:wrap;justify-content:space-between;align-items:center;display:flex}.payplugIntegratedPayment__schemes label{display:table-cell}.payplugIntegratedPayment__scheme{margin:0}.payplugIntegratedPayment__scheme span{cursor:pointer;width:33px;height:22px;background:50%/100% no-repeat;display:block}.payplugIntegratedPayment__scheme span:before{width:100%;height:100%;content:"";opacity:0;background:50%/100% no-repeat;display:block}.payplugIntegratedPayment__scheme input{display:none}.payplugIntegratedPayment__scheme input:checked+span:before{opacity:1}.payplugIntegratedPayment__scheme--visa span{background-image:url(visa-dark.87c34e0f.svg)}.payplugIntegratedPayment__scheme--visa span:before{background-image:url(visa.d11a46f6.svg)}.payplugIntegratedPayment__scheme--mastercard span{background-image:url(mastercard-dark.8977e440.svg)}.payplugIntegratedPayment__scheme--mastercard span:before{background-image:url(mastercard.7dd4ce0b.svg)}.payplugIntegratedPayment__scheme--cb span{background-image:url(./cb-dark.888aec45.svg)}.payplugIntegratedPayment__scheme--cb span:before{background-image:url(./cb.ccd964e9.svg)}.payplugIntegratedPayment__error{color:#e91932;width:100%;margin:-10px 0 10px;padding-left:4px;font-size:12px;line-height:18px}.payplugIntegratedPayment__error--cardHolder{margin:-10px 0 0}.payplugIntegratedPayment__error--cvv{justify-self:flex-end;margin:-10px 0 10px auto}.payplugIntegratedPayment__error--exp,.payplugIntegratedPayment__error--cvv{width:100%;max-width:49%}.payplugIntegratedPayment__error--hide{display:none} \ No newline at end of file +.payplugIntegratedPayment { + justify-self: center; + display: none +} + +.payplugIntegratedPayment * { + font-family: Poppins, Arial, sans-serif !important +} + +.payplugIntegratedPayment--loaded { + width: 100%; + max-width: 400px; + flex-wrap: wrap; + justify-content: space-between; + margin-top: 20px; + margin-bottom: 0; + display: flex; + position: relative +} + +.payplugIntegratedPayment__select { + height: 36px; + width: 100%; + border: 1px solid #ccc; + border-radius: 5px; + margin: 0 0 10px; + padding: 0 8px +} + +.payplugIntegratedPayment__container { + width: 100%; + margin: 0 0 10px; + padding: 0; + display: flex; + position: relative +} + +.payplugIntegratedPayment__container--cardHolder, +.payplugIntegratedPayment__container--pan, +.payplugIntegratedPayment__container--exp, +.payplugIntegratedPayment__container--cvv { + height: 40px; + cursor: text; + border: 1px solid #d5d6d8; + border-radius: 2px; + padding: 0 16px 0 50px; + line-height: 40px +} + +.payplugIntegratedPayment__container--cardHolder:before, +.payplugIntegratedPayment__container--pan:before, +.payplugIntegratedPayment__container--exp:before, +.payplugIntegratedPayment__container--cvv:before { + content: ""; + width: 24px; + height: 24px; + background: #95999e 50%/100% no-repeat; + position: absolute; + top: 20%; + left: 16px +} + +.payplugIntegratedPayment__container--cardHolder:focus, +.payplugIntegratedPayment__container--pan:focus, +.payplugIntegratedPayment__container--exp:focus, +.payplugIntegratedPayment__container--cvv:focus { + border-color: #2b343d +} + +.payplugIntegratedPayment__container--cardHolder--invalid, +.payplugIntegratedPayment__container--pan--invalid, +.payplugIntegratedPayment__container--exp--invalid, +.payplugIntegratedPayment__container--cvv--invalid { + border-color: #e91932 +} + +.payplugIntegratedPayment__container--cardHolder:before { + -webkit-mask-image: url(account.039342ad.svg); + mask-image: url(account.039342ad.svg) +} + +.payplugIntegratedPayment__container--pan:before { + -webkit-mask-image: url(card.0d2bd9bc.svg); + mask-image: url(card.0d2bd9bc.svg) +} + +.payplugIntegratedPayment__container--exp:before { + -webkit-mask-image: url(calendar.3c23bb16.svg); + mask-image: url(calendar.3c23bb16.svg) +} + +.payplugIntegratedPayment__container--cvv:before { + -webkit-mask-image: url(lock.fe8a73cd.svg); + mask-image: url(lock.fe8a73cd.svg) +} + +.payplugIntegratedPayment__container--exp, +.payplugIntegratedPayment__container--cvv { + max-width: calc(50% - 2px); + display: inline-block +} + +.payplugIntegratedPayment__container--scheme { + text-transform: uppercase; + height: 22px; + justify-content: space-between; + align-items: center; + margin: 10px 0; + font-size: 14px; + font-weight: 700 +} + +.payplugIntegratedPayment__container--saveCard { + height: auto; + align-items: center; + padding: 10px 0 0; + display: flex +} + +.payplugIntegratedPayment__container--saveCard input { + display: none +} + +.payplugIntegratedPayment__container--saveCard input:checked+label span:before { + opacity: 1 +} + +.payplugIntegratedPayment__container--saveCard label { + cursor: pointer; + color: #918f8f; + margin: 0 !important; + font-size: 12px !important +} + +.payplugIntegratedPayment__container--saveCard label span { + cursor: pointer; + height: 16px; + -o-transition: border .4s; + width: 16px; + border: 1px solid #d5d6d8; + border-radius: 2px; + margin: 0 10px -3px 0; + transition: border .4s; + display: inline-block; + position: relative +} + +.payplugIntegratedPayment__container--saveCard label span:before { + content: ""; + height: 5px; + opacity: 0; + width: 10px; + border-top: none; + border-bottom: 2.5px solid #2b343d; + border-left: 2.5px solid #2b343d; + border-right: none; + border-radius: 1px; + transition: opacity .4s; + display: block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -55%)rotate(-48deg) +} + +.payplugIntegratedPayment__container--saveCard label:hover { + color: #2b343d; + transition: all .1s +} + +.payplugIntegratedPayment__container--saveCard label:hover span { + border-color: #2b343d; + transition: all .1s +} + +.payplugIntegratedPayment__container--transaction { + align-items: center; + margin-top: 10px +} + +.payplugIntegratedPayment__container--transaction .transaction-label { + vertical-align: super; + margin-left: 5px; + font-size: 12px +} + +.payplugIntegratedPayment__container img.lock-icon { + width: 18px; + float: left !important +} + +.payplugIntegratedPayment__container img.payplug-logo { + width: 80px; + height: auto; + vertical-align: text-top; + margin-left: 6px; + display: inline-block; + float: inherit !important +} + +.payplugIntegratedPayment__container--privacy-policy { + text-align: center; + display: inline-block +} + +.payplugIntegratedPayment__container--privacy-policy a { + color: #918f8f; + font-size: 14px +} + +.payplugIntegratedPayment__schemes { + width: 115px; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + display: flex +} + +.payplugIntegratedPayment__schemes label { + display: table-cell +} + +.payplugIntegratedPayment__scheme { + margin: 0 +} + +.payplugIntegratedPayment__scheme span { + cursor: pointer; + width: 33px; + height: 22px; + background: 50%/100% no-repeat; + display: block +} + +.payplugIntegratedPayment__scheme span:before { + width: 100%; + height: 100%; + content: ""; + opacity: 0; + background: 50%/100% no-repeat; + display: block +} + +.payplugIntegratedPayment__scheme input { + display: none +} + +.payplugIntegratedPayment__scheme input:checked+span:before { + opacity: 1 +} + +.payplugIntegratedPayment__scheme--visa span { + background-image: url(visa-dark.87c34e0f.svg) +} + +.payplugIntegratedPayment__scheme--visa span:before { + background-image: url(visa.d11a46f6.svg) +} + +.payplugIntegratedPayment__scheme--mastercard span { + background-image: url(mastercard-dark.8977e440.svg) +} + +.payplugIntegratedPayment__scheme--mastercard span:before { + background-image: url(mastercard.7dd4ce0b.svg) +} + +.payplugIntegratedPayment__scheme--cb span { + background-image: url(./cb-dark.888aec45.svg) +} + +.payplugIntegratedPayment__scheme--cb span:before { + background-image: url(./cb.ccd964e9.svg) +} + +.payplugIntegratedPayment__error { + color: #e91932; + width: 100%; + margin: -10px 0 10px; + padding-left: 4px; + font-size: 12px; + line-height: 18px +} + +.payplugIntegratedPayment__error--cardHolder { + margin: -10px 0 0 +} + +.payplugIntegratedPayment__error--cvv { + justify-self: flex-end; + margin: -10px 0 10px auto +} + +.payplugIntegratedPayment__error--exp, +.payplugIntegratedPayment__error--cvv { + width: 100%; + max-width: 49% +} + +.payplugIntegratedPayment__error--hide { + display: none +} From 96fd4411509d2481933b1e5079effa86f9c4d36e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Wed, 18 Mar 2026 16:44:55 +0100 Subject: [PATCH 19/36] [Checkout] Improve enable/disable next step --- .../checkout-select-payment_controller.js | 18 ++- .../integrated-payment_controller.js | 123 ++++++++++++------ ...lius_checkout_select_payment_row.html.twig | 8 ++ templates/shop/integrated/index.html.twig | 4 +- .../shop/select_payment/_payplug.html.twig | 3 +- 5 files changed, 104 insertions(+), 52 deletions(-) diff --git a/assets/shop/controllers/checkout-select-payment_controller.js b/assets/shop/controllers/checkout-select-payment_controller.js index 852bb954..ead033d4 100644 --- a/assets/shop/controllers/checkout-select-payment_controller.js +++ b/assets/shop/controllers/checkout-select-payment_controller.js @@ -85,8 +85,8 @@ export default class extends Controller { return; } - const handlesSubmit = targetContainer.dataset.paymentHandlesSubmit === 'true' || - targetContainer.querySelector('[data-payment-handles-submit="true"]') !== null; + const handlesSubmit = targetContainer.dataset.paymentInlineSubmit === 'true' || + targetContainer.querySelector('[data-payment-inline-submit="true"]') !== null; this.toggleNextStepButton(!handlesSubmit); } @@ -98,16 +98,22 @@ export default class extends Controller { if (!this.nextStepButton) return; if (show) { - this.nextStepButton.classList.remove('d-none'); + this.nextStepButton.classList.remove('disabled'); this.nextStepButton.disabled = false; - this.nextStepButton.style.display = ''; } else { - this.nextStepButton.classList.add('d-none'); + this.nextStepButton.classList.add('disabled'); this.nextStepButton.disabled = true; - this.nextStepButton.style.display = 'none'; } } + enableNextStepButton() { + this.toggleNextStepButton(true); + } + + disableNextStepButton() { + this.toggleNextStepButton(false); + } + handleForm() { if ($('.checkbox-oney :radio:checked').length) { $('.checkbox-payplug').closest('.oney-payment-choice__item, .payplug-payment-choice__item, .form-check, .payment-item').find('.payment-choice__input:checked').prop('checked', false); diff --git a/assets/shop/controllers/integrated-payment_controller.js b/assets/shop/controllers/integrated-payment_controller.js index 4af78c21..40c99ca4 100644 --- a/assets/shop/controllers/integrated-payment_controller.js +++ b/assets/shop/controllers/integrated-payment_controller.js @@ -3,6 +3,7 @@ import WebFont from 'webfontloader'; /* stimulusFetch: 'lazy' */ export default class extends Controller { + static targets = ['container'] static values = { code: String, factoryName: String, @@ -55,23 +56,32 @@ export default class extends Controller { // Verify if we have required data // TODO: Pass as param - if (payplug_integrated_payment_params === undefined) { + if (typeof payplug_integrated_payment_params === 'undefined') { return; } + this.form = this.element.closest('form'); + if (payplug_integrated_payment_params.has_saved_cards) { - document.querySelectorAll('.payment-choice__input, .payment-item input[type=radio]:not([name=schemeOptions])').forEach((element) => { + const otherCardRadio = this.element.querySelector('#payplug_choice_card_other'); + const payplugRadio = document.querySelector(`[id*="checkout_select_payment_payments"][value="${payplug_integrated_payment_params.payment_method_code}"]`); + + // Initial check + if ( + (otherCardRadio && otherCardRadio.checked && payplugRadio && payplugRadio.checked) || + (otherCardRadio && otherCardRadio.checked && !payplugRadio && document.querySelector('.payplug-payment-choice__input:checked')) + ) { + this.openFields(); + } + + this.element.querySelectorAll('.payment-choice__input, [id*="checkout_select_payment_payments"]').forEach((element) => { element.addEventListener('change', (e) => { - if ( - 'payplug_choice_card_other' === e.currentTarget.id && - e.currentTarget.checked || - e.target.value === payplug_integrated_payment_params.payment_method_code && - document.querySelector('#payplug_choice_card_other').checked - ) { + const isOtherCardChecked = this.element.querySelector('#payplug_choice_card_other')?.checked; + const isPayplugSelected = document.querySelector(`[id*="checkout_select_payment_payments"][value="${payplug_integrated_payment_params.payment_method_code}"]`)?.checked; + + if (isOtherCardChecked && isPayplugSelected) { this.openFields(); - return; } - this.closeFields(); }) }) return; @@ -82,18 +92,37 @@ export default class extends Controller { this.openFields(); } - const selectPaymentMethodsField= this.getPaymentMethodSelectors(); + const selectPaymentMethodsField = this.getPaymentMethodSelectors(); selectPaymentMethodsField.forEach((element) => { - // On payment method select, open or close card fields + // On payment method select, open fields if payplug selected element.addEventListener('change', (e) => { if (payplug_integrated_payment_params.payment_method_code === e.currentTarget.value && e.currentTarget.checked) { this.openFields(); - return; } - this.closeFields(); }) }); } + + handleShow(event) { + if (this.hasContainerTarget) { + import('jquery').then(({ default: $ }) => { + $(this.containerTarget).slideDown(); + }); + this.openFields(); + this.containerTarget.dataset.paymentInlineSubmit = "true"; + } + } + + handleHide(event) { + if (this.hasContainerTarget) { + import('jquery').then(({ default: $ }) => { + $(this.containerTarget).slideUp(); + }); + this.closeFields(); + this.containerTarget.dataset.paymentInlineSubmit = "false"; + } + } + getPaymentMethodSelectors({ methodCode, checked } = {}) { const baseSelector = '[id*=checkout_select_payment_payments]'; @@ -105,45 +134,47 @@ export default class extends Controller { } return document.querySelectorAll(baseSelector); } + openFields() { - document.querySelector('.payplugIntegratedPayment').classList.add('payplugIntegratedPayment--loaded'); - document.querySelector('button[type=submit]').classList.add('disabled'); + this.containerTarget.classList.add('payplugIntegratedPayment--loaded'); if (null === this.options.api) { this.load(); } } + closeFields() { - document.querySelector('.payplugIntegratedPayment').classList.remove('payplugIntegratedPayment--loaded'); - document.querySelector('button[type=submit]').classList.remove('disabled'); + this.containerTarget.classList.remove('payplugIntegratedPayment--loaded'); } + load() { - this.options.api = new Payplug.IntegratedPayment(payplug_integrated_payment_params.is_test_mode); + this.options.api = new window.Payplug.IntegratedPayment(payplug_integrated_payment_params.is_test_mode); + this.options.api.setDisplayMode3ds(window.Payplug.DisplayMode3ds.LIGHTBOX); - this.options.api.setDisplayMode3ds(Payplug.DisplayMode3ds.LIGHTBOX); + const container = this.hasContainerTarget ? this.containerTarget : this.element; this.options.form.cardHolder = this.options.api.cardHolder( - document.querySelector('.cardHolder-input-container'), + container.querySelector('.cardHolder-input-container'), { default: this.options.inputStyle.default, placeholder: payplug_integrated_payment_params.cardholder } ); this.options.form.pan = this.options.api.cardNumber( - document.querySelector('.pan-input-container'), + container.querySelector('.pan-input-container'), { default: this.options.inputStyle.default, placeholder: payplug_integrated_payment_params.pan } ); this.options.form.cvv = this.options.api.cvv( - document.querySelector('.cvv-input-container'), + container.querySelector('.cvv-input-container'), { default: this.options.inputStyle.default, placeholder: payplug_integrated_payment_params.cvv } ); this.options.form.exp = this.options.api.expiration( - document.querySelector('.exp-input-container'), + container.querySelector('.exp-input-container'), { default: this.options.inputStyle.default, placeholder: payplug_integrated_payment_params.exp @@ -154,20 +185,24 @@ export default class extends Controller { this.fieldValidation(); } bindEvents() { - document.querySelector('#paid').addEventListener('click', (event) => { - event.preventDefault(); + const container = this.hasContainerTarget ? this.containerTarget : this.element; + const paidButton = container.querySelector('#paid'); + if (paidButton) { + paidButton.addEventListener('click', (event) => { + event.preventDefault(); + this.options.api.validateForm(); + }); + } - this.options.api.validateForm(); - }); this.options.api.onValidateForm(async ({ isFormValid }) => { if (isFormValid) { this.toggleLoader(); - const saveCardElement = document.querySelector('#savecard'); + const saveCardElement = this.element.querySelector('#savecard'); if (null !== saveCardElement) { this.options.save_card = saveCardElement.checked; } - const chosenScheme = document.querySelector('input.schemeOptions:checked'); - this.options.scheme = Payplug.Scheme.AUTO; + const chosenScheme = this.element.querySelector('input.schemeOptions:checked'); + this.options.scheme = window.Payplug.Scheme.AUTO; if (null !== chosenScheme) { this.options.scheme = chosenScheme.value; } @@ -186,28 +221,29 @@ export default class extends Controller { return; } document.querySelector('input[name=payplug_integrated_payment_token]').value = event.token; - document.querySelector('form[name*="checkout_select_payment"]').submit(); + this.form.submit(); }); } fieldValidation () { + const container = this.hasContainerTarget ? this.containerTarget : this.element; Object.keys(this.options.form).forEach((key) => { const field = this.options.form[key]; field.onChange((err) => { if (err.error) { - document.querySelector(`.payplugIntegratedPayment__error--${key}`).classList.remove('payplugIntegratedPayment__error--hide'); - document.querySelector(`.${key}-input-container`).classList.add('payplugIntegratedPayment__container--invalid'); + container.querySelector(`.payplugIntegratedPayment__error--${key}`).classList.remove('payplugIntegratedPayment__error--hide'); + container.querySelector(`.${key}-input-container`).classList.add('payplugIntegratedPayment__container--invalid'); if (err.error.name === "FIELD_EMPTY") { - document.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".emptyField").classList.remove('payplugIntegratedPayment__error--hide'); - document.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".invalidField").classList.add('payplugIntegratedPayment__error--hide'); + container.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".emptyField").classList.remove('payplugIntegratedPayment__error--hide'); + container.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".invalidField").classList.add('payplugIntegratedPayment__error--hide'); } else { - document.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".invalidField").classList.remove('payplugIntegratedPayment__error--hide'); - document.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".emptyField").classList.add('payplugIntegratedPayment__error--hide'); + container.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".invalidField").classList.remove('payplugIntegratedPayment__error--hide'); + container.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".emptyField").classList.add('payplugIntegratedPayment__error--hide'); } } else { - document.querySelector(`.payplugIntegratedPayment__error--${key}`).classList.add('payplugIntegratedPayment__error--hide'); - document.querySelector(`.${key}-input-container`).classList.remove('payplugIntegratedPayment__container--invalid'); - document.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".invalidField").classList.add('payplugIntegratedPayment__error--hide'); - document.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".emptyField").classList.add('payplugIntegratedPayment__error--hide'); + container.querySelector(`.payplugIntegratedPayment__error--${key}`).classList.add('payplugIntegratedPayment__error--hide'); + container.querySelector(`.${key}-input-container`).classList.remove('payplugIntegratedPayment__container--invalid'); + container.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".invalidField").classList.add('payplugIntegratedPayment__error--hide'); + container.querySelector(`.payplugIntegratedPayment__error--${key}`).querySelector(".emptyField").classList.add('payplugIntegratedPayment__error--hide'); this.options.fieldsValid[key] = true; this.options.fieldsEmpty[key] = false; } @@ -215,6 +251,7 @@ export default class extends Controller { }) } toggleLoader() { - document.querySelector('.payplugIntegratedPayment').querySelector('.sylius-shop-loader').classList.toggle('d-none'); + const container = this.hasContainerTarget ? this.containerTarget : this.element; + container.querySelector('.sylius-shop-loader').classList.toggle('d-none'); } } diff --git a/templates/form/sylius_checkout_select_payment_row.html.twig b/templates/form/sylius_checkout_select_payment_row.html.twig index f6b2ae7b..dc3ad140 100644 --- a/templates/form/sylius_checkout_select_payment_row.html.twig +++ b/templates/form/sylius_checkout_select_payment_row.html.twig @@ -78,6 +78,10 @@ id="payplug_choice_card_{{ card.id }}" name="{{ form.vars.full_name }}" class="payplug-payment-choice__input payment-choice__input" + {{ + stimulus_action('@payplug/sylius-payplug-plugin/checkout-select-payment', 'enableNextStepButton', 'change') | + stimulus_action('@payplug/sylius-payplug-plugin/integrated-payment', 'handleHide', 'change') + }} {% if form.vars.value is not empty %} {{ form.vars.value == card.id ? 'checked="checked"' : '' }} {% elseif loop.index is same as(1) %} @@ -99,6 +103,10 @@ id="payplug_choice_card_other" name="{{ form.vars.full_name }}" class="payplug-payment-choice__input payment-choice__input" + {{ + stimulus_action('@payplug/sylius-payplug-plugin/checkout-select-payment', 'disableNextStepButton', 'change') | + stimulus_action('@payplug/sylius-payplug-plugin/integrated-payment', 'handleShow', 'change') + }} {% if form.vars.value is not empty %} {{ form.vars.value == 'other' ? 'checked="checked"' : '' }} {% elseif sylius.customer.cards is empty %} diff --git a/templates/shop/integrated/index.html.twig b/templates/shop/integrated/index.html.twig index 9cd1c26e..9292eb37 100644 --- a/templates/shop/integrated/index.html.twig +++ b/templates/shop/integrated/index.html.twig @@ -25,8 +25,8 @@
{{ 'sylius.ui.loading'|trans }} diff --git a/templates/shop/select_payment/_payplug.html.twig b/templates/shop/select_payment/_payplug.html.twig index 32b60ed1..91cdbb80 100644 --- a/templates/shop/select_payment/_payplug.html.twig +++ b/templates/shop/select_payment/_payplug.html.twig @@ -21,7 +21,7 @@ {% set integratedPayment = true %} {% endif %} -
+
{{ form_label(form, null, {'label_attr': {'data-test-payment-method-label': '', 'data-gateway': 'payplug'}}) }} @@ -37,6 +37,7 @@ 'paymentMethod': method, 'payment': order.getLastPayment('cart'), 'hasSavedCards': hasSavedCards, + 'paymentInputId': form.vars.id, } %} {% endif %}
From 80c956ad074d3f51aa9824cd6b99499d7d9acd69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Mon, 30 Mar 2026 12:05:42 +0200 Subject: [PATCH 20/36] PPSYL-176 - Improve applepay stimulus controller and template --- .../shop/controllers/apple-pay_controller.js | 280 ++++++++++-------- .../shop/select_payment/_apple_pay.html.twig | 61 ++-- 2 files changed, 171 insertions(+), 170 deletions(-) diff --git a/assets/shop/controllers/apple-pay_controller.js b/assets/shop/controllers/apple-pay_controller.js index e9d99adc..4c6ab5a0 100644 --- a/assets/shop/controllers/apple-pay_controller.js +++ b/assets/shop/controllers/apple-pay_controller.js @@ -1,166 +1,184 @@ import { Controller } from '@hotwired/stimulus'; -import $ from 'jquery'; /* stimulusFetch: 'lazy' */ export default class extends Controller { - connect() { - this.applePayHandler(); + static values = { + paymentInputId: String, + settings: Object, } - showApplePayButton() { - const applePayButton = $(document).find("apple-pay-button"); - if (applePayButton.length) { - applePayButton.addClass('enabled'); + + connect() { + this.applePayButton = this.element.querySelector('apple-pay-button'); + this.onApplePayButtonClick = this.onApplePayButtonClick.bind(this); + + if (this.applePayButton) { + this.applePayButton.addEventListener('click', this.onApplePayButtonClick); } + + this.retryCount = 0; + this.initApplePay(); } - hideApplePayButton() { - const applePayButton = $(document).find("apple-pay-button.enabled"); - if (applePayButton.length) { - applePayButton.removeClass('enabled'); + + initApplePay() { + if (typeof window.ApplePaySession !== 'undefined') { + this.checkSupport(); + return; + } + + if (this.retryCount < 20) { // Try for 2 seconds + this.retryCount++; + setTimeout(() => this.initApplePay(), 100); + } else { + console.warn('Apple Pay SDK not fully initialized or not supported on this platform.'); } } - applePayHandler() { - const applePayChoice = $(".payment-item .checkbox-applepay input:radio"); - if (applePayChoice) { - if (applePayChoice.is(':checked')) { - this.disableNextStepButton(); - this.showApplePayButton(); + + checkSupport() { + try { + // Version 14 is the minimum for QR Code support on non-Safari browsers + const isSupported = window.ApplePaySession.canMakePayments() || window.ApplePaySession.supportsVersion(14); + + if (isSupported && this.applePayButton) { + this.applePayButton.classList.add('enabled'); } else { - this.enableNextStepButton(); - this.hideApplePayButton(); + this.hidePaymentMethod(); } + } catch (e) { + console.error('Error checking Apple Pay support:', e); + this.hidePaymentMethod(); } - $(".payment-item .checkbox input:radio").on('change', this.onPaymentMethodChoice); - $(document).find("apple-pay-button").on('click', this.onApplePayButtonClick); } - onPaymentMethodChoice(event) { - const isApplePay = $(event.currentTarget).closest('.checkbox-applepay').length; - if (isApplePay) { - this.showApplePayButton(); - this.disableNextStepButton(); - } else { - this.hideApplePayButton(); - this.enableNextStepButton(); + + hidePaymentMethod() { + if (!this.paymentInputIdValue) return; + + const input = document.getElementById(this.paymentInputIdValue); + if (!input) return; + + // Hide the entire payment item (usually a .item, .payment-item or .form-check) + const container = input.closest('.item, .payment-item, .form-check, [data-test-payment-item]'); + if (container) { + container.style.display = 'none'; + + // If the hidden input was checked, we should probably uncheck it + if (input.checked) { + input.checked = false; + // Trigger change to let other controllers (like checkout-select-payment) know + input.dispatchEvent(new Event('change', { bubbles: true })); + } } } - onApplePayButtonClick(event) { - const applePayButton = $(event.currentTarget); - if (applePaySessionRequestSettings === undefined) { + async onApplePayButtonClick(event) { + const applePayButton = event.currentTarget; + + if (!this.settingsValue) { console.error('Invalid Apple Pay settings!'); - return false; + return; } - // Create ApplePaySession - const session = new ApplePaySession(3, applePaySessionRequestSettings); + if (typeof window.ApplePaySession === 'undefined') { + return; + } + + // Prepare settings + const settings = { ...this.settingsValue }; + if (settings.applePayDomain) { + settings.applicationData = btoa(JSON.stringify({ + 'apple_pay_domain': settings.applePayDomain + })); + delete settings.applePayDomain; + } - session.onvalidatemerchant = async event => { - $.ajax({ - url: applePayButton.data('validate-merchant-route'), - method: 'POST', - cache: false, - data: {}, - success: (authorization) => { - let result = authorization.merchant_session; - console.log(result); + try { + const version = window.ApplePaySession.supportsVersion(14) ? 14 : 3; + const session = new window.ApplePaySession(version, settings); + + session.onvalidatemerchant = async (event) => { + try { + const response = await fetch(applePayButton.dataset.validateMerchantRoute, { + method: 'POST', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + const authorization = await response.json(); if (authorization.success === true) { - console.log(authorization.merchant_session); - session.completeMerchantValidation(result); + session.completeMerchantValidation(authorization.merchant_session); } else { session.abort(); } - }, - error: (XHR, status, error) => { - console.log(XHR, status, error); + } catch (error) { + console.error(error); session.abort(); window.location.reload(); - }, - }); - }; - - session.onpaymentauthorized = event => { - $.ajax({ - url: applePayButton.data('payment-authorized-route'), - method: 'POST', - cache: false, - data: { - token: event.payment.token - }, - success: (authorization) => { - try { - var apple_pay_Session_status = ApplePaySession.STATUS_SUCCESS; - - console.log(authorization); - console.log(authorization.data.responseToApple.status); - if (authorization.data.responseToApple.status != 1) { - apple_pay_Session_status = ApplePaySession.STATUS_FAILURE; - } - - const result = { - "status": apple_pay_Session_status - }; - - console.log(apple_pay_Session_status); - console.log(result); - - session.completePayment(result); - - console.log(authorization.data.returnUrl); - window.location.href = authorization.data.returnUrl; - } catch (err) { - console.error(err); - window.location.reload(); + } + }; + + session.onpaymentauthorized = async (event) => { + try { + const formData = this.toFormData({ + token: event.payment.token + }); + + const response = await fetch(applePayButton.dataset.paymentAuthorizedRoute, { + method: 'POST', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + body: formData, + }); + const authorization = await response.json(); + + let applePaySessionStatus = window.ApplePaySession.STATUS_SUCCESS; + + if (authorization.data.responseToApple.status !== 1) { + applePaySessionStatus = window.ApplePaySession.STATUS_FAILURE; } - }, - error: (XHR, status, error) => { - console.log(XHR, status, error); - session.abort(); + + session.completePayment({ "status": applePaySessionStatus }); + window.location.href = authorization.data.returnUrl; + } catch (error) { + console.error(error); window.location.reload(); - }, - }); - }; - - session.oncancel = event => { - console.log('Cancelling Apple Pay session!'); - - $.ajax({ - url: applePayButton.data('session-cancel-route'), - cache: false, - method: 'POST', - data: {}, - success: (authorization) => { - console.log('Cancelled!'); - console.log(authorization.data.returnUrl); + } + }; + + session.oncancel = async (event) => { + try { + const response = await fetch(applePayButton.dataset.sessionCancelRoute, { + method: 'POST', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + }, + }); + const authorization = await response.json(); window.location.href = authorization.data.returnUrl; - }, - error: (XHR, status, error) => { - console.log(XHR, status, error); + } catch (error) { + console.error(error); window.location.reload(); - }, - }); - }; + } + }; - session.begin(); - } - disableNextStepButton() { - const nextStepButton = $('form[name*="checkout_select_payment"] [data-test-next-step]'); - nextStepButton.replaceWith( - $("", { - id: 'next-step', - class: 'btn btn-primary btn-icon', - html: nextStepButton.html() - }) - ); + session.begin(); + } catch (e) { + console.error('Failed to create Apple Pay session:', e); + } } - enableNextStepButton() { - const nextStepButton = $('form[name*="checkout_select_payment"] [data-test-next-step]'); - nextStepButton.replaceWith( - $("
From d1c16c6349fe228ba438ad4f08cbe407ac2ce6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Mon, 30 Mar 2026 15:57:09 +0200 Subject: [PATCH 22/36] PPSYL-176 - Improve disabling payment method when not eligible --- .../shop/controllers/apple-pay_controller.js | 39 ++++++++++++------- assets/shop/dist/payment/integrated.css | 3 +- .../shop/select_payment/_apple_pay.html.twig | 3 +- translations/messages.en.yml | 1 + translations/messages.fr.yml | 1 + translations/messages.it.yml | 1 + 6 files changed, 32 insertions(+), 16 deletions(-) diff --git a/assets/shop/controllers/apple-pay_controller.js b/assets/shop/controllers/apple-pay_controller.js index 4c6ab5a0..53a069c5 100644 --- a/assets/shop/controllers/apple-pay_controller.js +++ b/assets/shop/controllers/apple-pay_controller.js @@ -5,6 +5,7 @@ export default class extends Controller { static values = { paymentInputId: String, settings: Object, + notice: String, } connect() { @@ -29,44 +30,56 @@ export default class extends Controller { this.retryCount++; setTimeout(() => this.initApplePay(), 100); } else { - console.warn('Apple Pay SDK not fully initialized or not supported on this platform.'); + this.disablePaymentMethod(); } } checkSupport() { try { - // Version 14 is the minimum for QR Code support on non-Safari browsers - const isSupported = window.ApplePaySession.canMakePayments() || window.ApplePaySession.supportsVersion(14); + const isSupported = window.ApplePaySession.canMakePayments(); if (isSupported && this.applePayButton) { this.applePayButton.classList.add('enabled'); } else { - this.hidePaymentMethod(); + this.disablePaymentMethod(); } } catch (e) { - console.error('Error checking Apple Pay support:', e); - this.hidePaymentMethod(); + this.disablePaymentMethod(); } } - hidePaymentMethod() { + disablePaymentMethod() { if (!this.paymentInputIdValue) return; const input = document.getElementById(this.paymentInputIdValue); if (!input) return; - // Hide the entire payment item (usually a .item, .payment-item or .form-check) - const container = input.closest('.item, .payment-item, .form-check, [data-test-payment-item]'); + const container = input.closest('.card, .item, .form-check, .field'); if (container) { - container.style.display = 'none'; - - // If the hidden input was checked, we should probably uncheck it + container.style.opacity = '0.5'; + container.style.pointerEvents = 'none'; + container.classList.add('apple-pay-ineligible'); + + // Add a small info message if not already present + if (!container.querySelector('.apple-pay-notice')) { + const notice = document.createElement('div'); + notice.className = 'apple-pay-notice small text-muted mt-2'; + notice.style.padding = '0 1rem 1rem'; + notice.innerText = this.noticeValue || 'Apple Pay is not available on this browser or device.'; + container.appendChild(notice); + } + if (input.checked) { input.checked = false; - // Trigger change to let other controllers (like checkout-select-payment) know input.dispatchEvent(new Event('change', { bubbles: true })); } } + + input.disabled = true; + + if (this.element) { + this.element.style.display = 'none'; + } } async onApplePayButtonClick(event) { diff --git a/assets/shop/dist/payment/integrated.css b/assets/shop/dist/payment/integrated.css index 382da93b..2bf34d51 100644 --- a/assets/shop/dist/payment/integrated.css +++ b/assets/shop/dist/payment/integrated.css @@ -12,8 +12,7 @@ max-width: 400px; flex-wrap: wrap; justify-content: space-between; - margin-top: 20px; - margin-bottom: 0; + margin: 20px auto 0; display: flex; position: relative } diff --git a/templates/shop/select_payment/_apple_pay.html.twig b/templates/shop/select_payment/_apple_pay.html.twig index 33e83b27..8f9f62c6 100644 --- a/templates/shop/select_payment/_apple_pay.html.twig +++ b/templates/shop/select_payment/_apple_pay.html.twig @@ -18,7 +18,8 @@
Date: Mon, 30 Mar 2026 16:05:13 +0200 Subject: [PATCH 23/36] PPSYL-176 - Latest ApplePay sdk version make it compatible with more device, do not check against --- src/Checker/ApplePayChecker.php | 42 ------------------- src/Checker/ApplePayCheckerInterface.php | 10 ----- ...pplePayPaymentMethodsResolverDecorator.php | 19 --------- 3 files changed, 71 deletions(-) delete mode 100644 src/Checker/ApplePayChecker.php delete mode 100644 src/Checker/ApplePayCheckerInterface.php diff --git a/src/Checker/ApplePayChecker.php b/src/Checker/ApplePayChecker.php deleted file mode 100644 index f3e681b1..00000000 --- a/src/Checker/ApplePayChecker.php +++ /dev/null @@ -1,42 +0,0 @@ -requestStack->getMainRequest(); - if (!$mainRequest instanceof Request) { - return false; - } - - $userAgent = $mainRequest->headers->get('User-Agent'); - - if (!is_string($userAgent)) { - return false; - } - - $browsers = ['Opera', 'Edg', 'Chrome', 'Firefox', 'MSIE', 'Trident']; - - foreach ($browsers as $browser) { - if (str_contains($userAgent, $browser)) { - return false; - } - } - - preg_match('/iPhone|Android|iPad|iPod|webOS|Mac/', $userAgent, $matches); - $os = current($matches); - - return \is_string($os); - } -} diff --git a/src/Checker/ApplePayCheckerInterface.php b/src/Checker/ApplePayCheckerInterface.php deleted file mode 100644 index 531d576b..00000000 --- a/src/Checker/ApplePayCheckerInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -decorated->getSupportedMethods($subject); - foreach ($supportedMethods as $key => $paymentMethod) { - Assert::isInstanceOf($paymentMethod, PaymentMethodInterface::class); - - /** @var GatewayConfigInterface $gatewayConfig */ - $gatewayConfig = $paymentMethod->getGatewayConfig(); - - if (ApplePayGatewayFactory::FACTORY_NAME !== $gatewayConfig->getFactoryName()) { - continue; - } - - if (!$this->applePayChecker->isDeviceReady()) { - unset($supportedMethods[$key]); - } - } - return $this->supportedMethodsProvider->provide( $supportedMethods, ApplePayGatewayFactory::FACTORY_NAME, From 0d40e00ddcbbae5faa695ba5f9854d931bc970c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Mon, 30 Mar 2026 16:17:01 +0200 Subject: [PATCH 24/36] PPSYL-176 - Remove useless header --- templates/shop/select_payment/_oney.html.twig | 12 ------------ templates/shop/select_payment/_payplug.html.twig | 4 ---- 2 files changed, 16 deletions(-) diff --git a/templates/shop/select_payment/_oney.html.twig b/templates/shop/select_payment/_oney.html.twig index 1a6696c4..97ae38c5 100644 --- a/templates/shop/select_payment/_oney.html.twig +++ b/templates/shop/select_payment/_oney.html.twig @@ -37,18 +37,6 @@ }) }} > -{# #} -{# {% if factoryName == oneyFactoryName %}#} -{# #} -{# {% endif %}#} -{# #} - {% if showOney %}
{{ form_row(form.parent.parent.oney_payment_choice) }} diff --git a/templates/shop/select_payment/_payplug.html.twig b/templates/shop/select_payment/_payplug.html.twig index 91cdbb80..cb5fa9e5 100644 --- a/templates/shop/select_payment/_payplug.html.twig +++ b/templates/shop/select_payment/_payplug.html.twig @@ -22,10 +22,6 @@ {% endif %}
- - {{ form_label(form, null, {'label_attr': {'data-test-payment-method-label': '', 'data-gateway': 'payplug'}}) }} - - {% if hasSavedCards %}
{{ form_row(form.parent.parent.payplug_card_choice) }} From 04f5ca1ed77ae4a6c4d4e3c9d79f16b0efbe7ea0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Mon, 30 Mar 2026 17:24:57 +0200 Subject: [PATCH 25/36] PPSYL-176 - Fix switching method enable next button or not --- templates/shop/integrated/index.html.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/shop/integrated/index.html.twig b/templates/shop/integrated/index.html.twig index 9292eb37..61a4b1d2 100644 --- a/templates/shop/integrated/index.html.twig +++ b/templates/shop/integrated/index.html.twig @@ -26,7 +26,7 @@
{{ 'sylius.ui.loading'|trans }} From 28590a075baab0f0e2d48ddc6b02a749db52b9bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Wed, 1 Apr 2026 15:08:04 +0200 Subject: [PATCH 26/36] Fix toggle next button when no integrated payment --- assets/shop/controllers/integrated-payment_controller.js | 2 ++ templates/form/sylius_checkout_select_payment_row.html.twig | 5 +---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/assets/shop/controllers/integrated-payment_controller.js b/assets/shop/controllers/integrated-payment_controller.js index 40c99ca4..5c85c16e 100644 --- a/assets/shop/controllers/integrated-payment_controller.js +++ b/assets/shop/controllers/integrated-payment_controller.js @@ -110,6 +110,7 @@ export default class extends Controller { }); this.openFields(); this.containerTarget.dataset.paymentInlineSubmit = "true"; + this.element.dispatchEvent(new CustomEvent('payment-method-state-change', { bubbles: true })); } } @@ -120,6 +121,7 @@ export default class extends Controller { }); this.closeFields(); this.containerTarget.dataset.paymentInlineSubmit = "false"; + this.element.dispatchEvent(new CustomEvent('payment-method-state-change', { bubbles: true })); } } diff --git a/templates/form/sylius_checkout_select_payment_row.html.twig b/templates/form/sylius_checkout_select_payment_row.html.twig index dc3ad140..6c27adff 100644 --- a/templates/form/sylius_checkout_select_payment_row.html.twig +++ b/templates/form/sylius_checkout_select_payment_row.html.twig @@ -103,10 +103,7 @@ id="payplug_choice_card_other" name="{{ form.vars.full_name }}" class="payplug-payment-choice__input payment-choice__input" - {{ - stimulus_action('@payplug/sylius-payplug-plugin/checkout-select-payment', 'disableNextStepButton', 'change') | - stimulus_action('@payplug/sylius-payplug-plugin/integrated-payment', 'handleShow', 'change') - }} + {{ stimulus_action('@payplug/sylius-payplug-plugin/integrated-payment', 'handleShow', 'change') }} {% if form.vars.value is not empty %} {{ form.vars.value == 'other' ? 'checked="checked"' : '' }} {% elseif sylius.customer.cards is empty %} From 79683acfea02be00425cbb36822cee726dae5cc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Thu, 2 Apr 2026 09:21:05 +0200 Subject: [PATCH 27/36] PPSYL-176 - Fix retrieving applepay token, add more log, unify next route when fail --- src/Controller/OrderController.php | 64 +++++++++++-------- .../Payment/ApplePayPaymentProvider.php | 24 +++++-- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/src/Controller/OrderController.php b/src/Controller/OrderController.php index dc4344d6..7fa0d206 100644 --- a/src/Controller/OrderController.php +++ b/src/Controller/OrderController.php @@ -18,6 +18,7 @@ use Sylius\Component\Resource\Exception\UpdateHandlingException; use Sylius\Component\Resource\ResourceActions; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Attribute\AsController; @@ -100,7 +101,6 @@ public function initiateApplePaySessionAction(Request $request): Response } $initializeEvent = $this->eventDispatcher->dispatchInitializeEvent(ResourceActions::UPDATE, $configuration, $resource); - $initializeEventResponse = $initializeEvent->getResponse(); if ($initializeEventResponse instanceof \Symfony\Component\HttpFoundation\Response) { @@ -113,6 +113,7 @@ public function initiateApplePaySessionAction(Request $request): Response $payment = $this->applePayPaymentProvider->provide($request, $resource); + $this->logger->info('[Payplug] ApplePay payment', ['payment' => $payment, 'details' => $payment->getDetails()]); $this->manager->flush(); return new JsonResponse([ @@ -142,7 +143,7 @@ public function initiateApplePaySessionAction(Request $request): Response $request->getSession()->getFlashBag()->add('error', 'sylius.payment.cancelled'); $dataResponse = []; - $redirect = $this->redirectToRoute('sylius_shop_checkout_select_payment'); + $redirect = $this->getApplePayNextRoute($resource); $dataResponse['returnUrl'] = $redirect->getTargetUrl(); $dataResponse['responseToApple'] = ['status' => self::APPLE_ERROR_RESPONSE_CODE]; @@ -174,35 +175,37 @@ public function initiateApplePaySessionAction(Request $request): Response public function confirmApplePayPaymentAction(Request $request): Response { $configuration = $this->requestConfigurationFactory->create($this->metadata, $request); - + $this->logger->info('[Payplug] Configuration', ['configuration' => $configuration->getParameters() ?? '']); $this->isGrantedOr403($configuration, ResourceActions::UPDATE); /** @var OrderInterface $resource */ $resource = $this->findOr404($configuration); - + $this->logger->info('[Payplug] Order', ['order' => $resource]); /** @var ResourceControllerEvent $event */ $event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::UPDATE, $configuration, $resource); - + $this->logger->info('[Payplug] Event', ['event' => $event]); if ($event->isStopped() && !$configuration->isHtmlRequest()) { throw new HttpException($event->getErrorCode(), $event->getMessage()); } - if ($event->isStopped()) { + $this->logger->info('[Payplug] Event stopped', ['event' => $event]); $eventResponse = $event->getResponse(); if (null !== $eventResponse) { return $eventResponse; } - + $this->logger->info('[Payplug] Event response', ['event_response' => $eventResponse]); return new JsonResponse([], Response::HTTP_BAD_REQUEST); } try { + $this->logger->info('[Payplug] Update resource', ['resource' => $resource]); $lastPayment = $this->applePayPaymentProvider->patch($request, $resource); - + $this->logger->info('[Payplug] Last payment', ['last_payment' => $lastPayment]); if (PaymentInterface::STATE_COMPLETED !== $lastPayment->getState()) { throw new PaymentNotCompletedException(); } } catch (\Exception | PaymentNotCompletedException $exception) { + $this->logger->error('Could not complete ApplePay payment', ['exception' => $exception]); try { $this->applePayPaymentProvider->cancel($resource); } catch (\Throwable $throwable) { @@ -215,13 +218,15 @@ public function confirmApplePayPaymentAction(Request $request): Response } $request->getSession()->getFlashBag()->add('error', 'sylius.payment.cancelled'); - $redirect = $this->redirectToRoute('sylius_shop_checkout_select_payment'); + $redirect = $this->getApplePayNextRoute($resource); + $dataResponse = []; $dataResponse['returnUrl'] = $redirect->getTargetUrl(); $dataResponse['responseToApple'] = ['status' => self::APPLE_ERROR_RESPONSE_CODE]; $dataResponse['errors'] = 'Payment not created'; $dataResponse['message'] = $exception->getMessage(); + $this->logger->error('Could not complete ApplePay payment', ['exception' => $exception, 'data_response' => $dataResponse]); return new JsonResponse($dataResponse, Response::HTTP_BAD_REQUEST); } @@ -347,14 +352,7 @@ public function cancelApplePaySessionAction(Request $request): Response $request->getSession()->getFlashBag()->add('error', 'sylius.payment.cancelled'); $dataResponse = []; - $redirect = $this->redirectToRoute('sylius_shop_checkout_select_payment', ['_locale' => $resource->getLocaleCode()]); - - if (OrderCheckoutStates::STATE_COMPLETED === $resource->getCheckoutState()) { - $redirect = $this->redirectToRoute('sylius_shop_order_show', [ - 'tokenValue' => $resource->getTokenValue(), - '_locale' => $resource->getLocaleCode(), - ]); - } + $redirect = $this->getApplePayNextRoute($resource); $dataResponse['returnUrl'] = $redirect->getTargetUrl(); $dataResponse['responseToApple'] = ['status' => self::APPLE_SUCCESS_RESPONSE_CODE]; @@ -388,14 +386,7 @@ public function cancelApplePaySessionAction(Request $request): Response $dataResponse = []; - $redirect = $this->redirectToRoute('sylius_shop_checkout_select_payment'); - - if (OrderInterface::STATE_NEW === $resource->getState()) { - $redirect = $this->redirectToRoute('sylius_shop_order_show', [ - 'tokenValue' => $resource->getTokenValue(), - '_locale' => $resource->getLocaleCode(), - ]); - } + $redirect = $this->getApplePayNextRoute($resource); $dataResponse['returnUrl'] = $redirect->getTargetUrl(); $dataResponse['responseToApple'] = ['status' => self::APPLE_ERROR_RESPONSE_CODE]; @@ -408,4 +399,27 @@ public function cancelApplePaySessionAction(Request $request): Response return new JsonResponse($response, Response::HTTP_OK); } } + + private function getApplePayNextRoute(OrderInterface $resource): RedirectResponse + { + $redirect = $this->redirectToRoute('sylius_shop_checkout_select_payment', ['_locale' => $resource->getLocaleCode()]); + + if (OrderCheckoutStates::STATE_COMPLETED === $resource->getCheckoutState()) { + $redirect = $this->redirectToRoute('sylius_shop_order_show', [ + 'tokenValue' => $resource->getTokenValue(), + '_locale' => $resource->getLocaleCode(), + ]); + } + + if (OrderInterface::STATE_NEW === $resource->getState()) { + $redirect = $this->redirectToRoute('sylius_shop_order_show', [ + 'tokenValue' => $resource->getTokenValue(), + '_locale' => $resource->getLocaleCode(), + ]); + } + + $this->logger->debug('[Payplug] Redirect', ['redirect' => $redirect, 'targetUrl' => $redirect->getTargetUrl()]); + + return $redirect; + } } diff --git a/src/Provider/Payment/ApplePayPaymentProvider.php b/src/Provider/Payment/ApplePayPaymentProvider.php index f592e026..ffb64cf6 100644 --- a/src/Provider/Payment/ApplePayPaymentProvider.php +++ b/src/Provider/Payment/ApplePayPaymentProvider.php @@ -15,6 +15,7 @@ use PayPlug\SyliusPayPlugPlugin\Exception\Payment\PaymentNotCompletedException; use PayPlug\SyliusPayPlugPlugin\Gateway\ApplePayGatewayFactory; use PayPlug\SyliusPayPlugPlugin\Repository\PaymentMethodRepositoryInterface; +use Psr\Log\LoggerInterface; use Sylius\Abstraction\StateMachine\StateMachineInterface; use Sylius\Bundle\PayumBundle\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\ChannelInterface; @@ -47,6 +48,7 @@ public function __construct( private EntityManagerInterface $entityManager, private OrderTokenAssignerInterface $orderTokenAssigner, private RouterInterface $router, + private LoggerInterface $logger, ) { } @@ -59,7 +61,6 @@ public function provide(Request $request, OrderInterface $order): PaymentInterfa } $state = PaymentInterface::STATE_CART; - /** @phpstan-ignore-next-line */ if ($order->getPayments()->filter(fn (PaymentInterface $payment): bool => PaymentInterface::STATE_FAILED === $payment->getState() || PaymentInterface::STATE_CANCELLED === $payment->getState())->count() > 0) { $state = PaymentInterface::STATE_NEW; @@ -102,6 +103,7 @@ public function provide(Request $request, OrderInterface $order): PaymentInterfa $this->applyRequiredPaymentTransition($payment, PaymentInterface::STATE_NEW); $this->applyRequiredOrderPaymentTransition($order, OrderPaymentStates::STATE_AWAITING_PAYMENT); $this->applyRequiredOrderCheckoutTransition($order, OrderCheckoutStates::STATE_COMPLETED); + $this->orderTokenAssigner->assignTokenValueIfNotSet($order); $this->entityManager->flush(); @@ -110,9 +112,11 @@ public function provide(Request $request, OrderInterface $order): PaymentInterfa public function patch(Request $request, OrderInterface $order): PaymentInterface { + $this->logger->debug('[Payplug] ApplePay payment patch', ['order' => $order]); $lastPayment = $order->getLastPayment(PaymentInterface::STATE_NEW); if (!$lastPayment instanceof PaymentInterface) { + $this->logger->error('[Payplug] No new payment found for order', ['order' => $order]); throw new LogicException(); } @@ -126,17 +130,19 @@ public function patch(Request $request, OrderInterface $order): PaymentInterface } $paymentResource = $this->applePayClient->retrieve($lastPayment->getDetails()['payment_id']); - + $this->logger->debug('[Payplug] ApplePay payment resource', ['payment' => (array) $paymentResource]); try { $applePay = []; - $applePay['payment_token'] = $request->get('token'); + $applePay['payment_token'] = $request->request->all('token'); $data = [ ApplePayGatewayFactory::PAYMENT_METHOD_APPLE_PAY => $applePay, ]; + $this->logger->debug('[Payplug] ApplePay payment update data', ['data' => $data]); /** @var Payment $response */ $response = $paymentResource->update($data); + $this->logger->info('[Payplug] ApplePay payment update response', ['response' => (array) $response]); $details = $lastPayment->getDetails(); if (!$response->is_paid) { @@ -145,14 +151,13 @@ public function patch(Request $request, OrderInterface $order): PaymentInterface throw new PaymentNotCompletedException(); } + $this->logger->debug('[Payplug] ApplePay payment update response is paid', ['response' => (array) $response]); $details['status'] = PaymentInterface::STATE_COMPLETED; $details['created_at'] = $response->created_at; $order = $lastPayment->getOrder(); Assert::isInstanceOf($order, OrderInterface::class); - $this->orderTokenAssigner->assignTokenValueIfNotSet($order); - $this->entityManager->flush(); $this->applyRequiredPaymentTransition($lastPayment, PaymentInterface::STATE_COMPLETED); @@ -164,9 +169,14 @@ public function patch(Request $request, OrderInterface $order): PaymentInterface $lastPayment->setDetails($details); return $lastPayment; - } catch (\Exception) { - $paymentResource->abort(); + } catch (\Exception $exception) { + $this->logger->error('[Payplug] ApplePay payment update failed', ['exception' => $exception, 'message' => $exception->getMessage()]); $this->applyRequiredPaymentTransition($lastPayment, PaymentInterface::STATE_FAILED); + try { + $paymentResource->abort(); + } catch (\Throwable $throwable) { + $this->logger->error('[Payplug] ApplePay payment abort failed', ['payment' => $lastPayment, 'exception' => $throwable]); + } throw new PaymentNotCompletedException(); } From 9ad895426e5fd2525eed23738c1a670ef77fcf9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Thu, 2 Apr 2026 09:44:03 +0200 Subject: [PATCH 28/36] PPSYL-176 - Fix returning data when fail to handle next route --- src/Controller/OrderController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controller/OrderController.php b/src/Controller/OrderController.php index 7fa0d206..cf20efcf 100644 --- a/src/Controller/OrderController.php +++ b/src/Controller/OrderController.php @@ -227,7 +227,7 @@ public function confirmApplePayPaymentAction(Request $request): Response $dataResponse['message'] = $exception->getMessage(); $this->logger->error('Could not complete ApplePay payment', ['exception' => $exception, 'data_response' => $dataResponse]); - return new JsonResponse($dataResponse, Response::HTTP_BAD_REQUEST); + return new JsonResponse(['data' => $dataResponse], Response::HTTP_BAD_REQUEST); } $this->manager->flush(); From f99e3f2946debc767c9b7cc1166b3c1165c7ca79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Thu, 9 Apr 2026 15:47:12 +0200 Subject: [PATCH 29/36] PPSYL-176 - Clean Provider, apple-pay-controller --- .../shop/controllers/apple-pay_controller.js | 24 ++------ src/Controller/OrderController.php | 51 ++++------------ src/Provider/ApplePayOrderProvider.php | 33 ---------- .../Payment/ApplePayPaymentProvider.php | 60 +++++++++---------- .../shop/select_payment/_apple_pay.html.twig | 4 +- 5 files changed, 44 insertions(+), 128 deletions(-) delete mode 100644 src/Provider/ApplePayOrderProvider.php diff --git a/assets/shop/controllers/apple-pay_controller.js b/assets/shop/controllers/apple-pay_controller.js index 53a069c5..fe977eb6 100644 --- a/assets/shop/controllers/apple-pay_controller.js +++ b/assets/shop/controllers/apple-pay_controller.js @@ -131,16 +131,15 @@ export default class extends Controller { session.onpaymentauthorized = async (event) => { try { - const formData = this.toFormData({ - token: event.payment.token - }); - const response = await fetch(applePayButton.dataset.paymentAuthorizedRoute, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/json', }, - body: formData, + body: JSON.stringify({ + token: event.payment.token, + }), }); const authorization = await response.json(); @@ -179,19 +178,4 @@ export default class extends Controller { console.error('Failed to create Apple Pay session:', e); } } - - toFormData(obj, formData = new FormData(), prefix = '') { - for (const key in obj) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - const value = obj[key]; - const name = prefix ? `${prefix}[${key}]` : key; - if (typeof value === 'object' && value !== null && !(value instanceof File) && !(value instanceof Blob)) { - this.toFormData(value, formData, name); - } else { - formData.append(name, value); - } - } - } - return formData; - } } diff --git a/src/Controller/OrderController.php b/src/Controller/OrderController.php index cf20efcf..2249f57a 100644 --- a/src/Controller/OrderController.php +++ b/src/Controller/OrderController.php @@ -53,12 +53,6 @@ final class OrderController extends BaseOrderController options: [ '_sylius' => [ 'flash' => false, - 'repository' => [ - 'method' => 'find', - 'arguments' => [ - 'expr:service("PayPlug\\SyliusPayPlugPlugin\\Provider\\ApplePayOrderProvider").getCurrentCart()', - ], - ], ], ], methods: ['GET', 'POST'], @@ -132,6 +126,7 @@ public function initiateApplePaySessionAction(Request $request): Response } catch (\Exception) { try { $this->applePayPaymentProvider->cancel($resource); + $this->manager->flush(); } catch (\Throwable $throwable) { $this->logger->error('Could not cancel ApplePay payment', [ 'order_id' => $resource->getId(), @@ -162,12 +157,6 @@ public function initiateApplePaySessionAction(Request $request): Response options: [ '_sylius' => [ 'flash' => false, - 'repository' => [ - 'method' => 'find', - 'arguments' => [ - 'expr:service("PayPlug\\SyliusPayPlugPlugin\\Provider\\ApplePayOrderProvider").getCurrentCart()', - ], - ], ], ], methods: ['GET', 'POST'], @@ -175,59 +164,46 @@ public function initiateApplePaySessionAction(Request $request): Response public function confirmApplePayPaymentAction(Request $request): Response { $configuration = $this->requestConfigurationFactory->create($this->metadata, $request); - $this->logger->info('[Payplug] Configuration', ['configuration' => $configuration->getParameters() ?? '']); $this->isGrantedOr403($configuration, ResourceActions::UPDATE); /** @var OrderInterface $resource */ $resource = $this->findOr404($configuration); - $this->logger->info('[Payplug] Order', ['order' => $resource]); /** @var ResourceControllerEvent $event */ $event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::UPDATE, $configuration, $resource); - $this->logger->info('[Payplug] Event', ['event' => $event]); if ($event->isStopped() && !$configuration->isHtmlRequest()) { throw new HttpException($event->getErrorCode(), $event->getMessage()); } if ($event->isStopped()) { - $this->logger->info('[Payplug] Event stopped', ['event' => $event]); $eventResponse = $event->getResponse(); if (null !== $eventResponse) { return $eventResponse; } - $this->logger->info('[Payplug] Event response', ['event_response' => $eventResponse]); return new JsonResponse([], Response::HTTP_BAD_REQUEST); } try { - $this->logger->info('[Payplug] Update resource', ['resource' => $resource]); $lastPayment = $this->applePayPaymentProvider->patch($request, $resource); - $this->logger->info('[Payplug] Last payment', ['last_payment' => $lastPayment]); if (PaymentInterface::STATE_COMPLETED !== $lastPayment->getState()) { throw new PaymentNotCompletedException(); } } catch (\Exception | PaymentNotCompletedException $exception) { - $this->logger->error('Could not complete ApplePay payment', ['exception' => $exception]); - try { - $this->applePayPaymentProvider->cancel($resource); - } catch (\Throwable $throwable) { - $this->logger->error('Could not cancel ApplePay payment', [ - 'order_id' => $resource->getId(), - 'code' => $throwable->getCode(), - 'message' => $throwable->getMessage(), - 'trace' => $throwable->getTraceAsString(), - ]); - } - $request->getSession()->getFlashBag()->add('error', 'sylius.payment.cancelled'); $redirect = $this->getApplePayNextRoute($resource); $dataResponse = []; $dataResponse['returnUrl'] = $redirect->getTargetUrl(); $dataResponse['responseToApple'] = ['status' => self::APPLE_ERROR_RESPONSE_CODE]; - $dataResponse['errors'] = 'Payment not created'; + $dataResponse['errors'] = 'Payment not completed'; $dataResponse['message'] = $exception->getMessage(); $this->logger->error('Could not complete ApplePay payment', ['exception' => $exception, 'data_response' => $dataResponse]); - return new JsonResponse(['data' => $dataResponse], Response::HTTP_BAD_REQUEST); + + $response = [ + 'success' => false, + 'data' => $dataResponse, + ]; + + return new JsonResponse($response, Response::HTTP_BAD_REQUEST); } $this->manager->flush(); @@ -248,7 +224,7 @@ public function confirmApplePayPaymentAction(Request $request): Response return $initializeEventResponse; } - $order = $lastPayment->getOrder(); + $order = $resource; Assert::isInstanceOf($order, OrderInterface::class); if ($this->stateMachineAbstraction->can($order, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_COMPLETE)) { @@ -277,12 +253,6 @@ public function confirmApplePayPaymentAction(Request $request): Response options: [ '_sylius' => [ 'flash' => false, - 'repository' => [ - 'method' => 'find', - 'arguments' => [ - 'expr:service("PayPlug\\SyliusPayPlugPlugin\\Provider\\ApplePayOrderProvider").getCurrentCart()', - ], - ], ], ], methods: ['GET', 'POST'], @@ -335,6 +305,7 @@ public function cancelApplePaySessionAction(Request $request): Response try { $this->applePayPaymentProvider->cancel($resource); + $this->manager->flush(); } catch (\Throwable $throwable) { $this->logger->error('Could not cancel ApplePay payment', [ 'order_id' => $resource->getId(), diff --git a/src/Provider/ApplePayOrderProvider.php b/src/Provider/ApplePayOrderProvider.php deleted file mode 100644 index abeef1c7..00000000 --- a/src/Provider/ApplePayOrderProvider.php +++ /dev/null @@ -1,33 +0,0 @@ -requestStack->getCurrentRequest(); - - if (!$request instanceof Request) { - throw new LogicException(); - } - - $orderId = $request->attributes->get('orderId'); - - if (null === $orderId || '' === $orderId) { - throw new LogicException(); - } - - return (int) $orderId; - } -} diff --git a/src/Provider/Payment/ApplePayPaymentProvider.php b/src/Provider/Payment/ApplePayPaymentProvider.php index ffb64cf6..c2c2ebac 100644 --- a/src/Provider/Payment/ApplePayPaymentProvider.php +++ b/src/Provider/Payment/ApplePayPaymentProvider.php @@ -32,7 +32,6 @@ use Sylius\Component\Payment\PaymentTransitions; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\Routing\Generator\UrlGenerator; use Symfony\Component\Routing\RouterInterface; use Webmozart\Assert\Assert; @@ -45,7 +44,6 @@ public function __construct( private PayPlugPaymentDataCreator $paymentDataCreator, #[Autowire('@payplug_sylius_payplug_plugin.api_client.apple_pay')] private PayPlugApiClientInterface $applePayClient, - private EntityManagerInterface $entityManager, private OrderTokenAssignerInterface $orderTokenAssigner, private RouterInterface $router, private LoggerInterface $logger, @@ -60,12 +58,6 @@ public function provide(Request $request, OrderInterface $order): PaymentInterfa throw new LogicException('Apple Pay is not enabled'); } - $state = PaymentInterface::STATE_CART; - /** @phpstan-ignore-next-line */ - if ($order->getPayments()->filter(fn (PaymentInterface $payment): bool => PaymentInterface::STATE_FAILED === $payment->getState() || PaymentInterface::STATE_CANCELLED === $payment->getState())->count() > 0) { - $state = PaymentInterface::STATE_NEW; - } - $payment = $this->initApplePaySyliusPaymentState($order); Assert::notNull($order->getBillingAddress()); @@ -89,9 +81,11 @@ public function provide(Request $request, OrderInterface $order): PaymentInterfa ); $paymentData = $paymentDataObject->getArrayCopy(); - $paymentData['notification_url'] = $this->router->generate('payplug_sylius_ipn', [], UrlGenerator::ABSOLUTE_URL); + $paymentData['notification_url'] = $this->router->generate('sylius_payment_method_notify', ['code' => $payment->getMethod()?->getCode()], RouterInterface::ABSOLUTE_URL); + $this->logger->notice('[Payplug] ApplePay payment data', ['data' => $paymentData]); $paymentResource = $this->applePayClient->createPayment($paymentData); + $this->logger->notice('[Payplug] ApplePay payment resource', ['payment' => (array) $paymentResource]); $details = $paymentData; $details['merchant_session'] = $paymentResource->payment_method['merchant_session']; @@ -105,14 +99,12 @@ public function provide(Request $request, OrderInterface $order): PaymentInterfa $this->applyRequiredOrderCheckoutTransition($order, OrderCheckoutStates::STATE_COMPLETED); $this->orderTokenAssigner->assignTokenValueIfNotSet($order); - $this->entityManager->flush(); - return $payment; } public function patch(Request $request, OrderInterface $order): PaymentInterface { - $this->logger->debug('[Payplug] ApplePay payment patch', ['order' => $order]); + $this->logger->notice('[Payplug] ApplePay payment patch', ['order' => $order]); $lastPayment = $order->getLastPayment(PaymentInterface::STATE_NEW); if (!$lastPayment instanceof PaymentInterface) { @@ -130,36 +122,39 @@ public function patch(Request $request, OrderInterface $order): PaymentInterface } $paymentResource = $this->applePayClient->retrieve($lastPayment->getDetails()['payment_id']); - $this->logger->debug('[Payplug] ApplePay payment resource', ['payment' => (array) $paymentResource]); + $this->logger->notice('[Payplug] ApplePay payment resource', ['payment' => (array) $paymentResource]); try { - $applePay = []; - $applePay['payment_token'] = $request->request->all('token'); + $token = $request->request->all('token'); + if ([] === $token) { + $token = json_decode($request->getContent(), true)['token'] ?? null; + } + if (null == $token) { + throw new InvalidRequestException('Missing token in request'); + } + + $this->logger->notice('[Payplug] ApplePay payment token', ['token' => $token]); $data = [ - ApplePayGatewayFactory::PAYMENT_METHOD_APPLE_PAY => $applePay, + 'apple_pay' => [ + 'payment_token' => $token, + ], + 'metadata' => (array) $paymentResource->metadata, ]; - $this->logger->debug('[Payplug] ApplePay payment update data', ['data' => $data]); + $this->logger->notice('[Payplug] ApplePay sending update to Payplug', ['data' => $data]); /** @var Payment $response */ - $response = $paymentResource->update($data); - $this->logger->info('[Payplug] ApplePay payment update response', ['response' => (array) $response]); - $details = $lastPayment->getDetails(); + $response = $paymentResource->update($data, $this->applePayClient->getConfiguration()); + $this->logger->notice('[Payplug] ApplePay updated response from Payplug', ['response' => (array) $response]); if (!$response->is_paid) { - $this->applyRequiredPaymentTransition($lastPayment, PaymentInterface::STATE_FAILED); - throw new PaymentNotCompletedException(); } + $this->logger->notice('[Payplug] ApplePay payment update response is paid', ['response' => (array) $response]); - $this->logger->debug('[Payplug] ApplePay payment update response is paid', ['response' => (array) $response]); + $details = $lastPayment->getDetails(); $details['status'] = PaymentInterface::STATE_COMPLETED; $details['created_at'] = $response->created_at; - $order = $lastPayment->getOrder(); - Assert::isInstanceOf($order, OrderInterface::class); - - $this->entityManager->flush(); - $this->applyRequiredPaymentTransition($lastPayment, PaymentInterface::STATE_COMPLETED); if ($this->isResourceIsAuthorized($response)) { @@ -173,7 +168,7 @@ public function patch(Request $request, OrderInterface $order): PaymentInterface $this->logger->error('[Payplug] ApplePay payment update failed', ['exception' => $exception, 'message' => $exception->getMessage()]); $this->applyRequiredPaymentTransition($lastPayment, PaymentInterface::STATE_FAILED); try { - $paymentResource->abort(); + $paymentResource->abort($this->applePayClient->getConfiguration()); } catch (\Throwable $throwable) { $this->logger->error('[Payplug] ApplePay payment abort failed', ['payment' => $lastPayment, 'exception' => $throwable]); } @@ -185,9 +180,10 @@ public function patch(Request $request, OrderInterface $order): PaymentInterface public function cancel(OrderInterface $order): void { $lastPayment = $order->getLastPayment(PaymentInterface::STATE_NEW); - if (!$lastPayment instanceof PaymentInterface) { - throw new LogicException(); + $this->logger->error('[Payplug] No new payment found for order during cancel', ['order' => $order]); + + return; } $paymentMethod = $lastPayment->getMethod(); @@ -200,7 +196,6 @@ public function cancel(OrderInterface $order): void } $this->applyRequiredPaymentTransition($lastPayment, PaymentInterface::STATE_CANCELLED); - $this->entityManager->flush(); } /** @@ -215,7 +210,6 @@ private function initApplePaySyliusPaymentState(OrderInterface $order): PaymentI $paymentMethod = $this->paymentMethodRepository->findOneByGatewayName(ApplePayGatewayFactory::FACTORY_NAME); $payment->setMethod($paymentMethod); $order->addPayment($payment); - $this->entityManager->flush(); return $payment; } diff --git a/templates/shop/select_payment/_apple_pay.html.twig b/templates/shop/select_payment/_apple_pay.html.twig index 8f9f62c6..fd8a56d2 100644 --- a/templates/shop/select_payment/_apple_pay.html.twig +++ b/templates/shop/select_payment/_apple_pay.html.twig @@ -6,9 +6,9 @@ 'countryCode': sylius.channel.defaultLocale.code|length == 2 ? sylius.channel.defaultLocale.code : sylius.channel.defaultLocale.code|slice(3, 2), 'currencyCode': sylius.channel.baseCurrency.code, 'merchantCapabilities': ['supports3DS'], - 'supportedNetworks': ['visa', 'mastercard'], + 'supportedNetworks': ['cartesBancaires', 'visa', 'masterCard'], 'total': { - 'label': '', + 'label': sylius.channel.name, 'type': 'final', 'amount': (order is not null ? order.total / 100 : 0) }, From 311e451e7df9557c1f970540f547809c683844aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Thu, 9 Apr 2026 15:48:23 +0200 Subject: [PATCH 30/36] PPSYL-176 - Improve retrieve order in notify --- src/OrderPay/Provider/NotifyPaymentProvider.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/OrderPay/Provider/NotifyPaymentProvider.php b/src/OrderPay/Provider/NotifyPaymentProvider.php index eb84a9aa..9748f47b 100644 --- a/src/OrderPay/Provider/NotifyPaymentProvider.php +++ b/src/OrderPay/Provider/NotifyPaymentProvider.php @@ -32,11 +32,11 @@ public function getPayment(Request $request, PaymentMethodInterface $paymentMeth if (null === $orderNumber) { throw new \InvalidArgumentException('Order number not found in request payload'); } - $order = $this->getOrderFromReference($orderNumber); + $order = $this->getOrderFromReference((string) $orderNumber); $payId = $request->getPayload()->getString('id'); $payment = $order->getPayments()->filter(function (PaymentInterface $payment) use ($payId) { - return $payment->getDetails()['payment_id'] === $payId; + return $payId === ($payment->getDetails()['payment_id'] ?? null); })->first(); if (false === $payment) { throw new \InvalidArgumentException(sprintf('Payment with ID "%s" not found in order "%s"', $payId, $orderNumber)); From 0a77cad304bc1f98d70123cdd2b3ef8feaf5e2c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Thu, 9 Apr 2026 15:49:16 +0200 Subject: [PATCH 31/36] PPSYL-176 - Use api client for payment method when abort, not default one --- src/PaymentProcessing/AbortPaymentProcessor.php | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/PaymentProcessing/AbortPaymentProcessor.php b/src/PaymentProcessing/AbortPaymentProcessor.php index e03fceef..5b08c5f1 100644 --- a/src/PaymentProcessing/AbortPaymentProcessor.php +++ b/src/PaymentProcessing/AbortPaymentProcessor.php @@ -5,11 +5,10 @@ namespace PayPlug\SyliusPayPlugPlugin\PaymentProcessing; use Payplug\Exception\HttpException; -use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientInterface; +use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory; use Sylius\Component\Core\Model\PaymentInterface; use Sylius\Component\Payment\PaymentTransitions; use Symfony\Component\DependencyInjection\Attribute\Autoconfigure; -use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Workflow\Attribute\AsCompletedListener; use Symfony\Component\Workflow\Event\CompletedEvent; @@ -17,8 +16,7 @@ class AbortPaymentProcessor { public function __construct( - #[Autowire('@payplug_sylius_payplug_plugin.api_client.payplug')] - private PayPlugApiClientInterface $payPlugApiClient, + private PayPlugApiClientFactory $payplugApiClientFactory, ) { } @@ -41,12 +39,13 @@ public function process(PaymentInterface $payment): void return; } + $client = $this->payplugApiClientFactory->createForPaymentMethod($payment->getMethod()); try { // When a payment is failed on Sylius, also abort it on PayPlug. // This should prevent the case that if we are already on PayPlug payment page // and go to the order history in another tab to click on pay again, then fail the transaction // and go back on the first PayPlug payment page and succeed it, it stays failed as its first payment model is already failed - $this->payPlugApiClient->abortPayment($paymentId); + $client->abortPayment($paymentId); } catch (HttpException) { } } From bee69bb48f82207223aa84c619cbf89cd7bc7370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Thu, 9 Apr 2026 16:15:51 +0200 Subject: [PATCH 32/36] PPSYL-176 - Phpstan --- ruleset/phpstan-baseline.neon | 6 ----- src/Controller/OrderController.php | 23 +++++-------------- .../Provider/NotifyPaymentProvider.php | 2 +- .../AbortPaymentProcessor.php | 6 ++++- .../Payment/ApplePayPaymentProvider.php | 6 ++--- 5 files changed, 15 insertions(+), 28 deletions(-) diff --git a/ruleset/phpstan-baseline.neon b/ruleset/phpstan-baseline.neon index 2ed4e7ab..1d16fed3 100644 --- a/ruleset/phpstan-baseline.neon +++ b/ruleset/phpstan-baseline.neon @@ -1156,12 +1156,6 @@ parameters: count: 1 path: ../src/Provider/AbstractSupportedRefundPaymentMethodsProvider.php - - - message: '#^Cannot cast mixed to int\.$#' - identifier: cast.int - count: 1 - path: ../src/Provider/ApplePayOrderProvider.php - - message: '#^Cannot access offset ''max_amounts'' on mixed\.$#' identifier: offsetAccess.nonOffsetAccessible diff --git a/src/Controller/OrderController.php b/src/Controller/OrderController.php index 2249f57a..7c70250e 100644 --- a/src/Controller/OrderController.php +++ b/src/Controller/OrderController.php @@ -166,10 +166,10 @@ public function confirmApplePayPaymentAction(Request $request): Response $configuration = $this->requestConfigurationFactory->create($this->metadata, $request); $this->isGrantedOr403($configuration, ResourceActions::UPDATE); - /** @var OrderInterface $resource */ - $resource = $this->findOr404($configuration); + /** @var OrderInterface $order */ + $order = $this->findOr404($configuration); /** @var ResourceControllerEvent $event */ - $event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::UPDATE, $configuration, $resource); + $event = $this->eventDispatcher->dispatchPreEvent(ResourceActions::UPDATE, $configuration, $order); if ($event->isStopped() && !$configuration->isHtmlRequest()) { throw new HttpException($event->getErrorCode(), $event->getMessage()); } @@ -182,13 +182,13 @@ public function confirmApplePayPaymentAction(Request $request): Response } try { - $lastPayment = $this->applePayPaymentProvider->patch($request, $resource); + $lastPayment = $this->applePayPaymentProvider->patch($request, $order); if (PaymentInterface::STATE_COMPLETED !== $lastPayment->getState()) { throw new PaymentNotCompletedException(); } } catch (\Exception | PaymentNotCompletedException $exception) { $request->getSession()->getFlashBag()->add('error', 'sylius.payment.cancelled'); - $redirect = $this->getApplePayNextRoute($resource); + $redirect = $this->getApplePayNextRoute($order); $dataResponse = []; $dataResponse['returnUrl'] = $redirect->getTargetUrl(); @@ -208,7 +208,7 @@ public function confirmApplePayPaymentAction(Request $request): Response $this->manager->flush(); - $postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::UPDATE, $configuration, $resource); + $postEvent = $this->eventDispatcher->dispatchPostEvent(ResourceActions::UPDATE, $configuration, $order); $postEventResponse = $postEvent->getResponse(); @@ -216,17 +216,6 @@ public function confirmApplePayPaymentAction(Request $request): Response return $postEventResponse; } - $initializeEvent = $this->eventDispatcher->dispatchInitializeEvent(ResourceActions::UPDATE, $configuration, $resource); - - $initializeEventResponse = $initializeEvent->getResponse(); - - if ($initializeEventResponse instanceof \Symfony\Component\HttpFoundation\Response) { - return $initializeEventResponse; - } - - $order = $resource; - Assert::isInstanceOf($order, OrderInterface::class); - if ($this->stateMachineAbstraction->can($order, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_COMPLETE)) { $this->stateMachineAbstraction->apply($order, OrderCheckoutTransitions::GRAPH, OrderCheckoutTransitions::TRANSITION_COMPLETE); } diff --git a/src/OrderPay/Provider/NotifyPaymentProvider.php b/src/OrderPay/Provider/NotifyPaymentProvider.php index 9748f47b..1f7818c3 100644 --- a/src/OrderPay/Provider/NotifyPaymentProvider.php +++ b/src/OrderPay/Provider/NotifyPaymentProvider.php @@ -27,7 +27,7 @@ public function __construct( public function getPayment(Request $request, PaymentMethodInterface $paymentMethod): PaymentInterface { - /** @var string|null $orderNumber */ + /** @var string|int|null $orderNumber */ $orderNumber = $request->getPayload()->all('metadata')['order_number'] ?? null; if (null === $orderNumber) { throw new \InvalidArgumentException('Order number not found in request payload'); diff --git a/src/PaymentProcessing/AbortPaymentProcessor.php b/src/PaymentProcessing/AbortPaymentProcessor.php index 5b08c5f1..a4f85d78 100644 --- a/src/PaymentProcessing/AbortPaymentProcessor.php +++ b/src/PaymentProcessing/AbortPaymentProcessor.php @@ -39,7 +39,11 @@ public function process(PaymentInterface $payment): void return; } - $client = $this->payplugApiClientFactory->createForPaymentMethod($payment->getMethod()); + $method = $payment->getMethod(); + if (null === $method) { + return; + } + $client = $this->payplugApiClientFactory->createForPaymentMethod($method); try { // When a payment is failed on Sylius, also abort it on PayPlug. // This should prevent the case that if we are already on PayPlug payment page diff --git a/src/Provider/Payment/ApplePayPaymentProvider.php b/src/Provider/Payment/ApplePayPaymentProvider.php index c2c2ebac..74f9b93c 100644 --- a/src/Provider/Payment/ApplePayPaymentProvider.php +++ b/src/Provider/Payment/ApplePayPaymentProvider.php @@ -126,11 +126,11 @@ public function patch(Request $request, OrderInterface $order): PaymentInterface try { $token = $request->request->all('token'); if ([] === $token) { - $token = json_decode($request->getContent(), true)['token'] ?? null; + $token = json_decode($request->getContent(), true)['token'] ?? null; // @phpstan-ignore-line } - if (null == $token) { - throw new InvalidRequestException('Missing token in request'); + if (null === $token) { + throw new \InvalidArgumentException('Missing token in request'); } $this->logger->notice('[Payplug] ApplePay payment token', ['token' => $token]); From 3019aecedea4a9da24d888bce0e88e165bcdb4f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Tue, 14 Apr 2026 14:58:50 +0200 Subject: [PATCH 33/36] Prevent 500 when payment method has never been unifiedauth --- src/ApiClient/PayPlugApiClientFactory.php | 7 ++++--- src/Exception/GatewayConfigurationException.php | 9 +++++++++ .../Constraints/IsCanSavePaymentMethodValidator.php | 10 +++++++++- .../Validator/Constraints/IsOneyEnabledValidator.php | 9 ++++++++- .../Constraints/PayplugPermissionValidator.php | 3 +++ 5 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 src/Exception/GatewayConfigurationException.php diff --git a/src/ApiClient/PayPlugApiClientFactory.php b/src/ApiClient/PayPlugApiClientFactory.php index 67c09ca7..4d12c900 100644 --- a/src/ApiClient/PayPlugApiClientFactory.php +++ b/src/ApiClient/PayPlugApiClientFactory.php @@ -5,6 +5,7 @@ namespace PayPlug\SyliusPayPlugPlugin\ApiClient; use Payplug\Authentication; +use PayPlug\SyliusPayPlugPlugin\Exception\GatewayConfigurationException; use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Payment\Model\PaymentMethodInterface; use Sylius\Component\Resource\Repository\RepositoryInterface; @@ -52,7 +53,7 @@ private function getTokenForGatewayConfig(GatewayConfigInterface $gatewayConfig) $clientConfig = $config['test_client']; } if (!\is_array($clientConfig)) { - throw new \LogicException('No client config found for ' . $gatewayConfig->getFactoryName() . '. Please renew your credentials in the PayPlug plugin configuration.'); + throw new GatewayConfigurationException('No client config found for ' . $gatewayConfig->getFactoryName() . '. Please renew your credentials in the PayPlug plugin configuration.'); } $cacheKey = sprintf('payplug_%s_api_key_%s', $gatewayConfig->getFactoryName(), $config['live'] === true ? 'live' : 'test'); @@ -61,12 +62,12 @@ private function getTokenForGatewayConfig(GatewayConfigInterface $gatewayConfig) return $this->cache->get($cacheKey, function (ItemInterface $item) use ($clientConfig) { $response = Authentication::generateJWT($clientConfig['client_id'] ?? '', $clientConfig['client_secret'] ?? ''); if ([] === $response || !is_array($response['httpResponse'])) { - throw new \LogicException('Unable to connect to PayPlug API. Please check your credentials in the PayPlug plugin configuration.'); + throw new GatewayConfigurationException('Unable to connect to PayPlug API. Please check your credentials in the PayPlug plugin configuration.'); } $accessToken = $response['httpResponse']['access_token']; if (!is_string($accessToken)) { - throw new \LogicException('Unable to connect to PayPlug API. Please check your credentials in the PayPlug plugin configuration.'); + throw new GatewayConfigurationException('Unable to connect to PayPlug API. Please check your credentials in the PayPlug plugin configuration.'); } $expiresIn = $response['httpResponse']['expires_in']; if (!is_int($expiresIn)) { diff --git a/src/Exception/GatewayConfigurationException.php b/src/Exception/GatewayConfigurationException.php new file mode 100644 index 00000000..69f21e43 --- /dev/null +++ b/src/Exception/GatewayConfigurationException.php @@ -0,0 +1,9 @@ +isEnabled() === false) { + return; + } + $factoryName = $value->getGatewayConfig()?->getFactoryName(); $channels = $value->getChannels(); @@ -45,8 +50,8 @@ public function validate($value, Constraint $constraint): void return; } - $checker = new CanSavePayplugPaymentMethodChecker($this->apiClientFactory->createForPaymentMethod($value)); try { + $checker = new CanSavePayplugPaymentMethodChecker($this->apiClientFactory->createForPaymentMethod($value)); if (!$checker->isLive()) { $this->context->buildViolation(sprintf($constraint->noTestKeyMessage, $factoryName))->addViolation(); @@ -58,6 +63,9 @@ public function validate($value, Constraint $constraint): void } return; + } catch (GatewayConfigurationException $exception) { + $this->context->buildViolation($exception->getMessage()) + ->addViolation(); } catch (UnauthorizedException | \LogicException) { return; } diff --git a/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php b/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php index de92698e..86aefd6e 100644 --- a/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php +++ b/src/Gateway/Validator/Constraints/IsOneyEnabledValidator.php @@ -7,6 +7,7 @@ use Payplug\Exception\UnauthorizedException; use PayPlug\SyliusPayPlugPlugin\ApiClient\PayPlugApiClientFactory; use PayPlug\SyliusPayPlugPlugin\Checker\OneyChecker; +use PayPlug\SyliusPayPlugPlugin\Exception\GatewayConfigurationException; use PayPlug\SyliusPayPlugPlugin\Gateway\OneyGatewayFactory; use Sylius\Component\Payment\Model\GatewayConfigInterface; use Sylius\Component\Core\Model\PaymentMethodInterface; @@ -33,6 +34,10 @@ public function validate($value, Constraint $constraint): void return; } + if ($value->isEnabled() === false) { + return; + } + $paymentMethod = $value; $gatewayConfig = $paymentMethod->getGatewayConfig(); if (!$gatewayConfig instanceof GatewayConfigInterface) { @@ -53,8 +58,10 @@ public function validate($value, Constraint $constraint): void ->addViolation(); } } catch (UnauthorizedException) { - // do nothing, this should be handle by IsPayPlugSecretKeyValid Constraint return; + } catch (GatewayConfigurationException $exception) { + $this->context->buildViolation($exception->getMessage()) + ->addViolation(); } } } diff --git a/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php b/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php index d59fae7b..328b47e8 100644 --- a/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php +++ b/src/Gateway/Validator/Constraints/PayplugPermissionValidator.php @@ -27,6 +27,9 @@ public function validate(mixed $value, Constraint $constraint): void return; } $paymentMethod = $value; + if ($paymentMethod->isEnabled() === false) { + return; + } try { $client = $this->apiClientFactory->createForPaymentMethod($paymentMethod); From d82b03162f20b9e1904b391d36f6a175542804a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jib=C3=A9=20Barth?= Date: Tue, 14 Apr 2026 15:25:04 +0200 Subject: [PATCH 34/36] Hotfix - Clean cache client config after new oauth --- src/Action/Admin/Auth/UnifiedAuthenticationController.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Action/Admin/Auth/UnifiedAuthenticationController.php b/src/Action/Admin/Auth/UnifiedAuthenticationController.php index 86d0f65a..603ad6aa 100644 --- a/src/Action/Admin/Auth/UnifiedAuthenticationController.php +++ b/src/Action/Admin/Auth/UnifiedAuthenticationController.php @@ -18,6 +18,7 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RouterInterface; +use Symfony\Contracts\Cache\CacheInterface; /** * This controller is used to authenticate the user with PayPlug @@ -37,6 +38,7 @@ public function __construct( private EntityManagerInterface $entityManager, private PaymentMethodValidator $paymentMethodValidator, private LoggerInterface $logger, + private CacheInterface $cache, ) { } @@ -114,6 +116,11 @@ public function oauthCallback(Request $request): Response $this->cleanSession($request); $request->getSession()->getFlashBag()->add('success', 'payplug_sylius_payplug_plugin.admin.oauth_callback_success'); + // Clean previous cached client config + $cacheKeyLive = sprintf('payplug_%s_api_key_live', $gatewayConfig->getFactoryName()); + $cacheKeyTest = sprintf('payplug_%s_api_key_test', $gatewayConfig->getFactoryName()); + $this->cache->delete($cacheKeyLive); + $this->cache->delete($cacheKeyTest); // Ensure that the payment method is well configured $this->paymentMethodValidator->process($paymentMethod); From e5537ec275bdbfee1ba43f74bdbe517d007af4a7 Mon Sep 17 00:00:00 2001 From: adumont-payplug Date: Fri, 17 Apr 2026 11:18:39 +0200 Subject: [PATCH 35/36] upgrade-v2: update README with complete instructions --- .gitignore | 7 ++ README.md | 226 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 136 insertions(+), 97 deletions(-) diff --git a/.gitignore b/.gitignore index 19975f9a..cde5dab0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,10 @@ /tests/TestApplication/.env.local /tests/TestApplication/.env.*.local /var/ + + +Audit.md +CLAUDE.md +.DS_Store +.claude +.review diff --git a/README.md b/README.md index 5fe94a5c..cea0998c 100644 --- a/README.md +++ b/README.md @@ -31,136 +31,168 @@ In local environment, the plugin will not work properly because you will not be ## Compatibility -| | Version | -| :--- |:--------| -| PHP | ^8.2 | +| | Version | +|:-------|:--------| +| PHP | ^8.2 | | Sylius | ^2.0 | ## Installation -1. Require the **payplug/sylius-payplug-plugin** : +### 1. Require the **payplug/sylius-payplug-plugin** : - ```bash - composer config extra.symfony.allow-contrib true - composer require payplug/sylius-payplug-plugin - ``` +```bash +composer config extra.symfony.allow-contrib true +composer require payplug/sylius-payplug-plugin +``` + +### 3. Register Sylius resources + +The plugin's extension does not prepend its `resources.yaml`, so the Sylius resource services for the Card and RefundHistory entities are never created. Add them manually in `config/packages/sylius_resource.yaml`: + +```yaml +sylius_resource: + resources: + payplug.payplug_card: + driver: doctrine/orm + classes: + model: PayPlug\SyliusPayPlugPlugin\Entity\Card + payplug.payplug_refund_history: + driver: doctrine/orm + classes: + model: PayPlug\SyliusPayPlugPlugin\Entity\RefundHistory + repository: PayPlug\SyliusPayPlugPlugin\Repository\RefundHistoryRepository +``` -2. Apply migrations to your database: +### 4. Fix service autowiring - ```shell - bin/console doctrine:migrations:migrate - ``` +The plugin uses `#[Autoconfigure]` on some actions and relies on named constructor arguments that Symfony cannot resolve automatically. Add the following to `config/services.yaml`: + +```yaml +services: + PayPlug\SyliusPayPlugPlugin\Action\CaptureAction: + arguments: + $payplugCardRepository: '@payplug.repository.payplug_card' + + PayPlug\SyliusPayPlugPlugin\Repository\RefundHistoryRepositoryInterface: + alias: payplug.repository.payplug_refund_history +``` -3. Add Payplug to refundable payment method for Sylius Refund Plugin in `config/services.yaml` +### 5. Apply migrations to your database: - ```yaml - parameters: - locale: fr_FR - sylius_refund.supported_gateways: - - payplug - - payplug_oney - - payplug_bancontact - - payplug_apple_pay - - payplug_american_express - ``` +```shell +bin/console doctrine:migrations:migrate + ``` -4. Add Traits for Customer and PaymentMethod entities +### 6. Add Payplug to refundable payment method for Sylius Refund Plugin in `config/services.yaml` + +```yaml +parameters: + locale: fr_FR + sylius_refund.supported_gateways: + - payplug + - payplug_oney + - payplug_bancontact + - payplug_apple_pay + - payplug_american_express +``` + +### 7. Add Traits for Customer and PaymentMethod entities * App\Entity\Customer\Customer - ```php - Payment methods`, then click on `Create` and choose "**Payplug**". From 3d4c86a5e3dc2031e8346c69f178902f14ec0be0 Mon Sep 17 00:00:00 2001 From: adumont-payplug Date: Mon, 20 Apr 2026 11:30:52 +0200 Subject: [PATCH 36/36] FIX: fixing dependency injection for installation --- README.md | 113 +++++++++--------------------------------------------- 1 file changed, 18 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index cea0998c..610a38d8 100644 --- a/README.md +++ b/README.md @@ -36,54 +36,31 @@ In local environment, the plugin will not work properly because you will not be | PHP | ^8.2 | | Sylius | ^2.0 | -## Installation -### 1. Require the **payplug/sylius-payplug-plugin** : +### With Symfony Flex + +#### 1. Allow contrib recipes and require the plugin ```bash composer config extra.symfony.allow-contrib true composer require payplug/sylius-payplug-plugin ``` -### 3. Register Sylius resources - -The plugin's extension does not prepend its `resources.yaml`, so the Sylius resource services for the Card and RefundHistory entities are never created. Add them manually in `config/packages/sylius_resource.yaml`: +#### 2. Install the Flex recipe -```yaml -sylius_resource: - resources: - payplug.payplug_card: - driver: doctrine/orm - classes: - model: PayPlug\SyliusPayPlugPlugin\Entity\Card - payplug.payplug_refund_history: - driver: doctrine/orm - classes: - model: PayPlug\SyliusPayPlugPlugin\Entity\RefundHistory - repository: PayPlug\SyliusPayPlugPlugin\Repository\RefundHistoryRepository +```bash +composer recipes:install payplug/sylius-payplug-plugin --force ``` -### 4. Fix service autowiring - -The plugin uses `#[Autoconfigure]` on some actions and relies on named constructor arguments that Symfony cannot resolve automatically. Add the following to `config/services.yaml`: +This automatically registers the bundle, copies configuration files, and sets up assets (on Sylius 2.1+). -```yaml -services: - PayPlug\SyliusPayPlugPlugin\Action\CaptureAction: - arguments: - $payplugCardRepository: '@payplug.repository.payplug_card' - - PayPlug\SyliusPayPlugPlugin\Repository\RefundHistoryRepositoryInterface: - alias: payplug.repository.payplug_refund_history -``` - -### 5. Apply migrations to your database: +#### 3. Apply migrations to your database ```shell bin/console doctrine:migrations:migrate - ``` +``` -### 6. Add Payplug to refundable payment method for Sylius Refund Plugin in `config/services.yaml` +#### 4. Add Payplug to refundable payment methods for Sylius Refund Plugin in `config/services.yaml` ```yaml parameters: @@ -96,7 +73,7 @@ parameters: - payplug_american_express ``` -### 7. Add Traits for Customer and PaymentMethod entities +#### 5. Add Traits for Customer and PaymentMethod entities * App\Entity\Customer\Customer @@ -153,7 +130,7 @@ class PaymentMethod extends BasePaymentMethod return new PaymentMethodTranslation(); } } -``` +``` * App\Entity\Payment\Payment @@ -179,78 +156,24 @@ class Payment extends BasePayment { use PaymentTrait; } -``` +``` -### 8. Process translations +#### 6. Process translations ```bash php bin/console translation:extract en PayPlugSyliusPayPlugPlugin --dump-messages php bin/console translation:extract fr PayPlugSyliusPayPlugPlugin --dump-messages ``` -### 9. Clear cache: +#### 7. Clear cache - ```bash - bin/console cache:clear - ``` +```bash +bin/console cache:clear +``` 🎉 You are now ready to add Payplug Payment method. In your back-office, go to `Configuration > Payment methods`, then click on `Create` and choose "**Payplug**". -### Assets installation (only for Sylius 2.0.x) - -On sylius 2.0.x, there is no automatic load of assets. -You need to add the following lines in `assets/shop/controllers.json` to allow Sylius to use our assets: - -```json -{ - "controllers": { - "@payplug/sylius-payplug-plugin": { - "oney-popin": { - "enabled": true, - "fetch": "lazy", - "autoimport": { - "@payplug/sylius-payplug-plugin/shop/dist/oney_common/index.css": true, - "@payplug/sylius-payplug-plugin/shop/dist/oney_popin/index.css": true - } - }, - "integrated-payment": { - "enabled": true, - "fetch": "lazy", - "autoimport": { - "@payplug/sylius-payplug-plugin/shop/dist/payment/integrated.css": true - } - }, - "oney-payment": { - "enabled": true, - "fetch": "lazy" - }, - "payment-logo": { - "enabled": true, - "fetch": "lazy" - }, - "checkout-select-payment": { - "enabled": true, - "fetch": "lazy", - "autoimport": { - "@payplug/sylius-payplug-plugin/shop/dist/payment/index.css": true - } - }, - "apple-pay": { - "enabled": true, - "fetch": "lazy" - } - } - }, - "entrypoints": [] -} -``` - -> [!NOTE] -> On Sylius Standard >= 2.1, assets are automatically loaded when you install the plugin with flex. -> If you are upgrading from a 2.0.x version, read the [upgrade guide](https://github.com/Sylius/Sylius/blob/2.1/UPGRADE-2.1.md#assets) - - ## Logs If you want to follow the logs in the production environment, you need to add the configuration in `config/packages/prod/monolog.yaml`, logs should be in `var/log/prod.log` which can be searched after the phrase `[Payum]` or `[Payplug]`: