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() 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 5d2f4eea..610a38d8 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,150 +31,145 @@ 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 -1. Require the **payplug/sylius-payplug-plugin** : +### With Symfony Flex - ```bash - composer config extra.symfony.allow-contrib true - composer require payplug/sylius-payplug-plugin - ``` +#### 1. Allow contrib recipes and require the plugin -2. Apply migrations to your database: +```bash +composer config extra.symfony.allow-contrib true +composer require payplug/sylius-payplug-plugin +``` - ```shell - bin/console doctrine:migrations:migrate - ``` +#### 2. Install the Flex recipe + +```bash +composer recipes:install payplug/sylius-payplug-plugin --force +``` -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/ - ``` +This automatically registers the bundle, copies configuration files, and sets up assets (on Sylius 2.1+). -4. Add Payplug to refundable payment method for Sylius Refund Plugin in `config/services.yaml` +#### 3. 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 +``` -5. Add Payplug routes in `config/routes.yaml` +#### 4. Add Payplug to refundable payment methods for Sylius Refund Plugin in `config/services.yaml` - ```yaml - sylius_payplug: - resource: "@PayPlugSyliusPayPlugPlugin/config/routing.yaml" - ``` +```yaml +parameters: + locale: fr_FR + sylius_refund.supported_gateways: + - payplug + - payplug_oney + - payplug_bancontact + - payplug_apple_pay + - payplug_american_express +``` -8. Add Traits for Customer and PaymentMethod entities +#### 5. Add Traits for Customer and PaymentMethod entities * App\Entity\Customer\Customer - ```php - Payment methods`, then click on `Create` and choose "**Payplug**". @@ -199,30 +199,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). diff --git a/assets/shop/controllers/apple-pay_controller.js b/assets/shop/controllers/apple-pay_controller.js index e9d99adc..fe977eb6 100644 --- a/assets/shop/controllers/apple-pay_controller.js +++ b/assets/shop/controllers/apple-pay_controller.js @@ -1,166 +1,181 @@ import { Controller } from '@hotwired/stimulus'; -import $ from 'jquery'; /* stimulusFetch: 'lazy' */ export default class extends Controller { - connect() { - this.applePayHandler(); + static values = { + paymentInputId: String, + settings: Object, + notice: String, } - 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 { + this.disablePaymentMethod(); } } - applePayHandler() { - const applePayChoice = $(".payment-item .checkbox-applepay input:radio"); - if (applePayChoice) { - if (applePayChoice.is(':checked')) { - this.disableNextStepButton(); - this.showApplePayButton(); + + checkSupport() { + try { + const isSupported = window.ApplePaySession.canMakePayments(); + + if (isSupported && this.applePayButton) { + this.applePayButton.classList.add('enabled'); } else { - this.enableNextStepButton(); - this.hideApplePayButton(); + this.disablePaymentMethod(); } + } catch (e) { + this.disablePaymentMethod(); } - $(".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(); + + disablePaymentMethod() { + if (!this.paymentInputIdValue) return; + + const input = document.getElementById(this.paymentInputIdValue); + if (!input) return; + + const container = input.closest('.card, .item, .form-check, .field'); + if (container) { + 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; + input.dispatchEvent(new Event('change', { bubbles: true })); + } + } + + input.disabled = true; + + if (this.element) { + this.element.style.display = 'none'; } } - 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; + } - 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); + // Prepare settings + const settings = { ...this.settingsValue }; + if (settings.applePayDomain) { + settings.applicationData = btoa(JSON.stringify({ + 'apple_pay_domain': settings.applePayDomain + })); + delete settings.applePayDomain; + } + + 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 response = await fetch(applePayButton.dataset.paymentAuthorizedRoute, { + method: 'POST', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + token: event.payment.token, + }), + }); + 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() - }) - ); - } - enableNextStepButton() { - const nextStepButton = $('form[name*="checkout_select_payment"] [data-test-next-step]'); - nextStepButton.replaceWith( - $("