From d0bd8324ca5b960526290ea5da0aeddc0eeefbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joachim=20L=C3=B8vgaard?= Date: Mon, 17 Aug 2026 11:36:37 +0200 Subject: [PATCH] Show payment method brand logos for Quickpay methods on the checkout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each Quickpay payment method on the checkout payment step now shows the brands its payment window will offer, derived from the gateway configuration's payment_methods option — nothing is configured twice and no API is called. PaymentMethodLogoProvider parses Quickpay's token grammar (exclusions, the 3d- prefix, regional/debit variants collapsing onto their brand, creditcard expanding to a configurable brand list) and maps tokens to bundled SVGs; a token without a logo renders as a text label. Rendered through a Twig function by a sylius_ui block on sylius.shop.checkout.select_payment.choice_item_content. The SVGs are Shopify's MIT-licensed activemerchant/payment_icons (attribution README next to them) and cover cards, Dankort, MobilePay, Apple/Google Pay, Klarna, Anyday, Vipps, Swish, PayPal, ViaBill, Trustly, iDEAL, Sofort and paysafecard. Shops add/override/hide logos through the new checkout.payment_method_logos and checkout.creditcard_brands configuration. Closes #108 --- CLAUDE.md | 14 ++ README.md | 44 ++++- composer.json | 1 + phpstan.neon | 11 ++ src/Checkout/PaymentMethodLogo.php | 20 +++ src/Checkout/PaymentMethodLogoProvider.php | 170 ++++++++++++++++++ .../PaymentMethodLogoProviderInterface.php | 18 ++ src/DependencyInjection/Configuration.php | 17 ++ .../SetonoSyliusQuickpayExtension.php | 12 +- src/Resources/config/services.xml | 19 ++ .../public/images/payment-methods/README.md | 26 +++ .../payment-methods/american-express.svg | 1 + .../public/images/payment-methods/anyday.svg | 1 + .../images/payment-methods/apple-pay.svg | 1 + .../public/images/payment-methods/dankort.svg | 1 + .../images/payment-methods/diners-club.svg | 1 + .../images/payment-methods/discover.svg | 1 + .../payment-methods/forbrugsforeningen.svg | 1 + .../images/payment-methods/google-pay.svg | 1 + .../public/images/payment-methods/ideal.svg | 1 + .../public/images/payment-methods/jcb.svg | 1 + .../public/images/payment-methods/klarna.svg | 1 + .../public/images/payment-methods/maestro.svg | 1 + .../images/payment-methods/mastercard.svg | 1 + .../images/payment-methods/mobilepay.svg | 1 + .../public/images/payment-methods/paypal.svg | 1 + .../images/payment-methods/paysafecard.svg | 1 + .../public/images/payment-methods/sofort.svg | 1 + .../public/images/payment-methods/swish.svg | 1 + .../public/images/payment-methods/trustly.svg | 1 + .../images/payment-methods/unionpay.svg | 1 + .../public/images/payment-methods/viabill.svg | 1 + .../public/images/payment-methods/vipps.svg | 1 + .../images/payment-methods/visa-electron.svg | 1 + .../public/images/payment-methods/visa.svg | 1 + .../_payment_method_logos.html.twig | 19 ++ src/Twig/PaymentMethodLogoExtension.php | 18 ++ src/Twig/PaymentMethodLogoRuntime.php | 25 +++ .../PaymentMethodLogoProviderTest.php | 141 +++++++++++++++ .../DependencyInjection/ConfigurationTest.php | 29 +++ .../SetonoSyliusQuickpayExtensionTest.php | 25 +++ .../payment_method_logos.test | 38 ++++ tests/Twig/PaymentMethodLogoExtensionTest.php | 51 ++++++ 43 files changed, 720 insertions(+), 2 deletions(-) create mode 100644 src/Checkout/PaymentMethodLogo.php create mode 100644 src/Checkout/PaymentMethodLogoProvider.php create mode 100644 src/Checkout/PaymentMethodLogoProviderInterface.php create mode 100644 src/Resources/public/images/payment-methods/README.md create mode 100644 src/Resources/public/images/payment-methods/american-express.svg create mode 100644 src/Resources/public/images/payment-methods/anyday.svg create mode 100644 src/Resources/public/images/payment-methods/apple-pay.svg create mode 100644 src/Resources/public/images/payment-methods/dankort.svg create mode 100644 src/Resources/public/images/payment-methods/diners-club.svg create mode 100644 src/Resources/public/images/payment-methods/discover.svg create mode 100644 src/Resources/public/images/payment-methods/forbrugsforeningen.svg create mode 100644 src/Resources/public/images/payment-methods/google-pay.svg create mode 100644 src/Resources/public/images/payment-methods/ideal.svg create mode 100644 src/Resources/public/images/payment-methods/jcb.svg create mode 100644 src/Resources/public/images/payment-methods/klarna.svg create mode 100644 src/Resources/public/images/payment-methods/maestro.svg create mode 100644 src/Resources/public/images/payment-methods/mastercard.svg create mode 100644 src/Resources/public/images/payment-methods/mobilepay.svg create mode 100644 src/Resources/public/images/payment-methods/paypal.svg create mode 100644 src/Resources/public/images/payment-methods/paysafecard.svg create mode 100644 src/Resources/public/images/payment-methods/sofort.svg create mode 100644 src/Resources/public/images/payment-methods/swish.svg create mode 100644 src/Resources/public/images/payment-methods/trustly.svg create mode 100644 src/Resources/public/images/payment-methods/unionpay.svg create mode 100644 src/Resources/public/images/payment-methods/viabill.svg create mode 100644 src/Resources/public/images/payment-methods/vipps.svg create mode 100644 src/Resources/public/images/payment-methods/visa-electron.svg create mode 100644 src/Resources/public/images/payment-methods/visa.svg create mode 100644 src/Resources/views/shop/checkout/select_payment/_payment_method_logos.html.twig create mode 100644 src/Twig/PaymentMethodLogoExtension.php create mode 100644 src/Twig/PaymentMethodLogoRuntime.php create mode 100644 tests/Checkout/PaymentMethodLogoProviderTest.php create mode 100644 tests/Twig/Fixtures/PaymentMethodLogo/payment_method_logos.test create mode 100644 tests/Twig/PaymentMethodLogoExtensionTest.php diff --git a/CLAUDE.md b/CLAUDE.md index 586ddc7..9bc0454 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -130,6 +130,20 @@ persisted into the details by the library's Status/Confirm/Sync actions. Each op `operations.capture` / `operations.refund` / `operations.cancel` (defined in `DependencyInjection/Configuration.php`, passed to the processor as container parameters). +### Checkout presentation +`Checkout/PaymentMethodLogoProvider` turns a Quickpay payment method's gateway `payment_methods` option into a +list of `PaymentMethodLogo` value objects (token, label, `asset()` path or null) — parsing Quickpay's token +grammar (`!` exclusions, `3d-` prefix, `-dk`/`-debet` variants collapsing onto the brand, `creditcard` +expanding to the configured `checkout.creditcard_brands`) and applying the `checkout.payment_method_logos` +config overrides (path adds/overrides, null hides). Exposed to Twig as +`setono_sylius_quickpay_payment_method_logos(method)` (`Twig/PaymentMethodLogoExtension` + `PaymentMethodLogoRuntime`, +the Twig runtime pattern; `phpstan.neon` ignores the one error Twig < 3.9's `callable|null` docblock raises for the +`[Runtime::class, 'method']` form on the lowest-deps job) and +rendered by `shop/checkout/select_payment/_payment_method_logos.html.twig`, a `sylius_ui` block prepended on +`sylius.shop.checkout.select_payment.choice_item_content`. The SVGs in +`Resources/public/images/payment-methods/` are Shopify's MIT-licensed payment_icons (attribution README +in that folder) and need `assets:install` in the host app. + ### Gateway config & language - `Form/Type/GatewayConfigurationType` is the admin form for the gateway (tagged `sylius.gateway_configuration_type` type `quickpay`). Every field carries a translated `help` text diff --git a/README.md b/README.md index 94760ff..4ca2e6c 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,17 @@ This registers the callback endpoint (`POST /payment/quickpay/notify`) that Quic store about payment state changes. **Quickpay only delivers capture, refund and cancel callbacks to the account-wide callback url, which is empty by default** — point it at this endpoint, see [Callbacks](#callbacks). -### 5. Import fixtures (optional, development only) +### 5. Install the assets + +```bash +bin/console assets:install +``` + +The plugin ships the payment method logos shown on the checkout (see +[Payment method logos on the checkout](#payment-method-logos-on-the-checkout)); like every bundle asset they are +published to `public/bundles/setonosyliusquickpayplugin/` by `assets:install`. + +### 6. Import fixtures (optional, development only) ```yaml # config/packages/setono_sylius_quickpay.yaml @@ -137,6 +147,38 @@ never blocks saving. programmatic partial operations; note that Sylius' payment state machine still treats the payment as a whole — the `refund` transition can only be applied once. +## Payment method logos on the checkout + +On the checkout payment step, each Quickpay payment method shows the brands its payment window will offer — derived +from the gateway configuration's **Payment methods** field, so nothing is configured twice and no API is called. +`creditcard, mobilepay` renders the Visa and Mastercard marks (what `creditcard` stands for is configurable) and the +MobilePay mark; a token without a bundled logo (e.g. `resurs`) renders as a small text label. Quickpay's token +grammar is understood: exclusions (`!diners`) are skipped, forced 3-D Secure (`3d-creditcard`) is ignored, and +regional/debit variants (`visa-dk`, `mastercard-debet-dk`, `mobilepay-subscriptions`) collapse onto their brand. + +Bundled logos cover cards (Visa, Visa Electron, Mastercard, Maestro, American Express, Diners Club, Discover, JCB, +UnionPay, Dankort, Forbrugsforeningen) and MobilePay, Apple Pay, Google Pay, Klarna, Anyday, Vipps, Swish, PayPal, +ViaBill, Trustly, iDEAL, Sofort and paysafecard. They come from Shopify's MIT-licensed +[payment_icons](https://github.com/activemerchant/payment_icons); the marks remain their owners' trademarks and are +shown only to indicate acceptance. + +To use your own images, add or override a token, or hide one, configure the plugin — the value is an asset path (or +URL) as `asset()` understands it, or `null` to hide the token: + +```yaml +setono_sylius_quickpay: + checkout: + payment_method_logos: + mobilepay: build/images/mobilepay.svg + resurs: https://cdn.example.com/resurs.png + apple-pay: ~ + creditcard_brands: [dankort, visa, mastercard] # what the `creditcard` token shows; default [visa, mastercard] +``` + +The markup lives in `@SetonoSyliusQuickpayPlugin/shop/checkout/select_payment/_payment_method_logos.html.twig` (a +`sylius_ui` block on `sylius.shop.checkout.select_payment.choice_item_content`) and can be overridden like any bundle +template. + ## Operation history in the admin Each Quickpay payment on the admin order view shows its **live operation history** — every diff --git a/composer.json b/composer.json index 57327ae..c3a925c 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,7 @@ "sylius/core-bundle": "^1.0", "sylius/locale": "^1.0", "sylius/payment": "^1.0", + "sylius/payum-bundle": "^1.0", "sylius/resource-bundle": "^1.10", "sylius/state-machine-abstraction": "^1.0", "symfony/config": "^6.4", diff --git a/phpstan.neon b/phpstan.neon index 506b9cf..914580d 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -22,3 +22,14 @@ parameters: reportUnmatchedIgnoredErrors: false treatPhpDocTypesAsCertain: false + + ignoreErrors: + # Twig functions are declared with the runtime form `[SomeRuntime::class, 'method']`, which is + # not a PHP callable and which Twig's TwigFunction constructor only documents from Twig 3.9 + # (`callable|array{class-string, string}|null`). The plugin still allows Twig 2.15/3.0, so on + # the lowest-dependency matrix job the docblock says `callable|null` and PHPStan reports the + # array. The runtime form is what Twig expects at every version. + - + identifier: argument.type + message: '#Parameter \#2 \$callable of class Twig\\TwigFunction constructor expects \(callable\(\): mixed\)\|null, array\{#' + path: src/Twig/* diff --git a/src/Checkout/PaymentMethodLogo.php b/src/Checkout/PaymentMethodLogo.php new file mode 100644 index 0000000..da5ed0e --- /dev/null +++ b/src/Checkout/PaymentMethodLogo.php @@ -0,0 +1,20 @@ + [label, bundled image stem or null] + */ + private const BUILT_IN = [ + 'visa' => ['Visa', 'visa'], + 'visa-electron' => ['Visa Electron', 'visa-electron'], + 'mastercard' => ['Mastercard', 'mastercard'], + 'maestro' => ['Maestro', 'maestro'], + 'american-express' => ['American Express', 'american-express'], + 'diners' => ['Diners Club', 'diners-club'], + 'discover' => ['Discover', 'discover'], + 'jcb' => ['JCB', 'jcb'], + 'unionpay' => ['UnionPay', 'unionpay'], + 'dankort' => ['Dankort', 'dankort'], + 'fbg1886' => ['Forbrugsforeningen', 'forbrugsforeningen'], + 'mobilepay' => ['MobilePay', 'mobilepay'], + 'apple-pay' => ['Apple Pay', 'apple-pay'], + 'google-pay' => ['Google Pay', 'google-pay'], + 'klarna-payments' => ['Klarna', 'klarna'], + 'klarna' => ['Klarna', 'klarna'], + 'anyday' => ['Anyday', 'anyday'], + 'vipps' => ['Vipps', 'vipps'], + 'swish' => ['Swish', 'swish'], + 'paypal' => ['PayPal', 'paypal'], + 'viabill' => ['ViaBill', 'viabill'], + 'trustly' => ['Trustly', 'trustly'], + 'ideal' => ['iDEAL', 'ideal'], + 'sofort' => ['Sofort', 'sofort'], + 'paysafecard' => ['paysafecard', 'paysafecard'], + 'resurs' => ['Resurs Bank', null], + ]; + + /** + * @param array $images token => image path (`asset()`-compatible) to add or + * override a logo, or null to hide the token entirely + * @param list $creditcardBrands the brands `creditcard` stands for + */ + public function __construct( + private readonly array $images = [], + private readonly array $creditcardBrands = ['visa', 'mastercard'], + ) { + } + + public function provide(PaymentMethodInterface $paymentMethod): array + { + $gatewayConfig = $paymentMethod->getGatewayConfig(); + if (!$gatewayConfig instanceof GatewayConfigInterface || QuickpayGatewayFactory::NAME !== $gatewayConfig->getFactoryName()) { + return []; + } + + $paymentMethods = $gatewayConfig->getConfig()['payment_methods'] ?? null; + if (!is_string($paymentMethods)) { + return []; + } + + $logos = []; + foreach ($this->tokens($paymentMethods) as $token) { + $logo = $this->logo($token); + if (null !== $logo && !isset($logos[$logo->token])) { + $logos[$logo->token] = $logo; + } + } + + return array_values($logos); + } + + /** + * @return list the base tokens the option resolves to, in order + */ + private function tokens(string $paymentMethods): array + { + $tokens = []; + foreach (explode(',', strtolower($paymentMethods)) as $raw) { + $token = trim($raw); + if ('' === $token || str_starts_with($token, '!')) { + continue; + } + + if (str_starts_with($token, '3d-')) { + $token = substr($token, 3); + } + + if ('creditcard' === $token) { + array_push($tokens, ...$this->creditcardBrands); + + continue; + } + + $tokens[] = $token; + } + + return $tokens; + } + + private function logo(string $token): ?PaymentMethodLogo + { + $base = $this->base($token); + + // An explicit configuration wins: an image adds/overrides, null hides + if (array_key_exists($base, $this->images)) { + $image = $this->images[$base]; + if (null === $image) { + return null; + } + + return new PaymentMethodLogo($base, self::BUILT_IN[$base][0] ?? self::humanize($base), $image); + } + + if (isset(self::BUILT_IN[$base])) { + [$label, $stem] = self::BUILT_IN[$base]; + + return new PaymentMethodLogo($base, $label, null === $stem ? null : sprintf(self::IMAGE_PATH, $stem)); + } + + return new PaymentMethodLogo($base, self::humanize($base), null); + } + + /** + * Reduces a token to the brand it stands for: `visa-dk` and `mastercard-debet-dk` are still + * Visa and Mastercard, `mobilepay-subscriptions` is still MobilePay. Suffixes are only stripped + * while the token is unknown, so `apple-pay` and `visa-electron` keep their own identity. + */ + private function base(string $token): string + { + $candidate = $token; + while (true) { + if (isset(self::BUILT_IN[$candidate]) || array_key_exists($candidate, $this->images)) { + return $candidate; + } + + $pos = strrpos($candidate, '-'); + if (false === $pos) { + // Nothing known at any length: keep the token as Quickpay spells it + return $token; + } + + $candidate = substr($candidate, 0, $pos); + } + } + + private static function humanize(string $token): string + { + return ucwords(str_replace('-', ' ', $token)); + } +} diff --git a/src/Checkout/PaymentMethodLogoProviderInterface.php b/src/Checkout/PaymentMethodLogoProviderInterface.php new file mode 100644 index 0000000..b9bf423 --- /dev/null +++ b/src/Checkout/PaymentMethodLogoProviderInterface.php @@ -0,0 +1,18 @@ + + */ + public function provide(PaymentMethodInterface $paymentMethod): array; +} diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index c91e80e..e3747fe 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -36,6 +36,23 @@ public function getConfigTreeBuilder(): TreeBuilder ->end() ->end() ->end() + ->arrayNode('checkout') + ->info('Presentation of Quickpay payment methods on the checkout payment step') + ->addDefaultsIfNotSet() + ->children() + ->arrayNode('payment_method_logos') + ->info('Add or override the logo shown for a Quickpay payment method token (e.g. "mobilepay"): the value is an asset path or URL for the image, or null to hide the token. Bundled logos exist for the common tokens; anything else renders as a text label') + ->useAttributeAsKey('token') + ->normalizeKeys(false) + ->scalarPrototype()->end() + ->end() + ->arrayNode('creditcard_brands') + ->info('Which card brands the "creditcard" token (every card enabled on the Quickpay agreement) shows on the checkout') + ->scalarPrototype()->end() + ->defaultValue(['visa', 'mastercard']) + ->end() + ->end() + ->end() ; return $treeBuilder; diff --git a/src/DependencyInjection/SetonoSyliusQuickpayExtension.php b/src/DependencyInjection/SetonoSyliusQuickpayExtension.php index 019142d..a93e461 100644 --- a/src/DependencyInjection/SetonoSyliusQuickpayExtension.php +++ b/src/DependencyInjection/SetonoSyliusQuickpayExtension.php @@ -14,13 +14,15 @@ final class SetonoSyliusQuickpayExtension extends Extension implements PrependEx { public function load(array $configs, ContainerBuilder $container): void { - /** @var array{operations: array{capture: bool, refund: bool, cancel: bool}} $config */ + /** @var array{operations: array{capture: bool, refund: bool, cancel: bool}, checkout: array{payment_method_logos: array, creditcard_brands: list}} $config */ $config = $this->processConfiguration($this->getConfiguration([], $container), $configs); $loader = new XmlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); $container->setParameter('setono_sylius_quickpay.operations.capture', $config['operations']['capture']); $container->setParameter('setono_sylius_quickpay.operations.refund', $config['operations']['refund']); $container->setParameter('setono_sylius_quickpay.operations.cancel', $config['operations']['cancel']); + $container->setParameter('setono_sylius_quickpay.checkout.payment_method_logos', $config['checkout']['payment_method_logos']); + $container->setParameter('setono_sylius_quickpay.checkout.creditcard_brands', $config['checkout']['creditcard_brands']); $loader->load('services.xml'); } @@ -44,6 +46,14 @@ public function prepend(ContainerBuilder $container): void ], ], ], + 'sylius.shop.checkout.select_payment.choice_item_content' => [ + 'blocks' => [ + 'setono_sylius_quickpay_payment_method_logos' => [ + 'template' => '@SetonoSyliusQuickpayPlugin/shop/checkout/select_payment/_payment_method_logos.html.twig', + 'priority' => -10, + ], + ], + ], ], ]); } diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml index 82a954c..4ed837f 100644 --- a/src/Resources/config/services.xml +++ b/src/Resources/config/services.xml @@ -109,6 +109,25 @@ + + + %setono_sylius_quickpay.checkout.payment_method_logos% + %setono_sylius_quickpay.checkout.creditcard_brands% + + + + + + + + + + + + + + diff --git a/src/Resources/public/images/payment-methods/README.md b/src/Resources/public/images/payment-methods/README.md new file mode 100644 index 0000000..9a2f52b --- /dev/null +++ b/src/Resources/public/images/payment-methods/README.md @@ -0,0 +1,26 @@ +# Payment method icons + +The SVG files in this directory come from Shopify's [activemerchant/payment_icons](https://github.com/activemerchant/payment_icons) project and are redistributed under its MIT license, reproduced below. The brand names and marks they depict remain trademarks of their respective owners; the plugin shows them only to indicate which payment methods the Quickpay payment window offers. + +``` +Copyright 2015 Shopify Inc + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +``` diff --git a/src/Resources/public/images/payment-methods/american-express.svg b/src/Resources/public/images/payment-methods/american-express.svg new file mode 100644 index 0000000..0447d7a --- /dev/null +++ b/src/Resources/public/images/payment-methods/american-express.svg @@ -0,0 +1 @@ +American Express diff --git a/src/Resources/public/images/payment-methods/anyday.svg b/src/Resources/public/images/payment-methods/anyday.svg new file mode 100644 index 0000000..d12b3ea --- /dev/null +++ b/src/Resources/public/images/payment-methods/anyday.svg @@ -0,0 +1 @@ +Anyday \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/apple-pay.svg b/src/Resources/public/images/payment-methods/apple-pay.svg new file mode 100644 index 0000000..3a566b9 --- /dev/null +++ b/src/Resources/public/images/payment-methods/apple-pay.svg @@ -0,0 +1 @@ +Apple Pay diff --git a/src/Resources/public/images/payment-methods/dankort.svg b/src/Resources/public/images/payment-methods/dankort.svg new file mode 100644 index 0000000..e097311 --- /dev/null +++ b/src/Resources/public/images/payment-methods/dankort.svg @@ -0,0 +1 @@ +Dankort diff --git a/src/Resources/public/images/payment-methods/diners-club.svg b/src/Resources/public/images/payment-methods/diners-club.svg new file mode 100644 index 0000000..acbc8ec --- /dev/null +++ b/src/Resources/public/images/payment-methods/diners-club.svg @@ -0,0 +1 @@ +Diners Club \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/discover.svg b/src/Resources/public/images/payment-methods/discover.svg new file mode 100644 index 0000000..a96118a --- /dev/null +++ b/src/Resources/public/images/payment-methods/discover.svg @@ -0,0 +1 @@ +Discover \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/forbrugsforeningen.svg b/src/Resources/public/images/payment-methods/forbrugsforeningen.svg new file mode 100644 index 0000000..e68e42c --- /dev/null +++ b/src/Resources/public/images/payment-methods/forbrugsforeningen.svg @@ -0,0 +1 @@ +Forbrugsforeningen \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/google-pay.svg b/src/Resources/public/images/payment-methods/google-pay.svg new file mode 100644 index 0000000..42ea1f6 --- /dev/null +++ b/src/Resources/public/images/payment-methods/google-pay.svg @@ -0,0 +1 @@ +Google Pay diff --git a/src/Resources/public/images/payment-methods/ideal.svg b/src/Resources/public/images/payment-methods/ideal.svg new file mode 100644 index 0000000..1ba2b5d --- /dev/null +++ b/src/Resources/public/images/payment-methods/ideal.svg @@ -0,0 +1 @@ +iDEAL diff --git a/src/Resources/public/images/payment-methods/jcb.svg b/src/Resources/public/images/payment-methods/jcb.svg new file mode 100644 index 0000000..c201767 --- /dev/null +++ b/src/Resources/public/images/payment-methods/jcb.svg @@ -0,0 +1 @@ +JCB \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/klarna.svg b/src/Resources/public/images/payment-methods/klarna.svg new file mode 100644 index 0000000..3001045 --- /dev/null +++ b/src/Resources/public/images/payment-methods/klarna.svg @@ -0,0 +1 @@ +Klarna \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/maestro.svg b/src/Resources/public/images/payment-methods/maestro.svg new file mode 100644 index 0000000..0343fe3 --- /dev/null +++ b/src/Resources/public/images/payment-methods/maestro.svg @@ -0,0 +1 @@ +Maestro \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/mastercard.svg b/src/Resources/public/images/payment-methods/mastercard.svg new file mode 100644 index 0000000..6d97472 --- /dev/null +++ b/src/Resources/public/images/payment-methods/mastercard.svg @@ -0,0 +1 @@ +Mastercard diff --git a/src/Resources/public/images/payment-methods/mobilepay.svg b/src/Resources/public/images/payment-methods/mobilepay.svg new file mode 100644 index 0000000..fb43184 --- /dev/null +++ b/src/Resources/public/images/payment-methods/mobilepay.svg @@ -0,0 +1 @@ +MobilePay \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/paypal.svg b/src/Resources/public/images/payment-methods/paypal.svg new file mode 100644 index 0000000..1537546 --- /dev/null +++ b/src/Resources/public/images/payment-methods/paypal.svg @@ -0,0 +1 @@ +PayPal \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/paysafecard.svg b/src/Resources/public/images/payment-methods/paysafecard.svg new file mode 100644 index 0000000..d28bbd4 --- /dev/null +++ b/src/Resources/public/images/payment-methods/paysafecard.svg @@ -0,0 +1 @@ +PaysafeCard \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/sofort.svg b/src/Resources/public/images/payment-methods/sofort.svg new file mode 100644 index 0000000..7fc7f33 --- /dev/null +++ b/src/Resources/public/images/payment-methods/sofort.svg @@ -0,0 +1 @@ +SOFORT \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/swish.svg b/src/Resources/public/images/payment-methods/swish.svg new file mode 100644 index 0000000..cb9c193 --- /dev/null +++ b/src/Resources/public/images/payment-methods/swish.svg @@ -0,0 +1 @@ +Swish \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/trustly.svg b/src/Resources/public/images/payment-methods/trustly.svg new file mode 100644 index 0000000..f6e07de --- /dev/null +++ b/src/Resources/public/images/payment-methods/trustly.svg @@ -0,0 +1 @@ +Trustly \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/unionpay.svg b/src/Resources/public/images/payment-methods/unionpay.svg new file mode 100644 index 0000000..037aebf --- /dev/null +++ b/src/Resources/public/images/payment-methods/unionpay.svg @@ -0,0 +1 @@ +Union Pay \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/viabill.svg b/src/Resources/public/images/payment-methods/viabill.svg new file mode 100644 index 0000000..4c361b1 --- /dev/null +++ b/src/Resources/public/images/payment-methods/viabill.svg @@ -0,0 +1 @@ +ViaBill \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/vipps.svg b/src/Resources/public/images/payment-methods/vipps.svg new file mode 100644 index 0000000..c9cc6c7 --- /dev/null +++ b/src/Resources/public/images/payment-methods/vipps.svg @@ -0,0 +1 @@ +Vipps diff --git a/src/Resources/public/images/payment-methods/visa-electron.svg b/src/Resources/public/images/payment-methods/visa-electron.svg new file mode 100644 index 0000000..ec863da --- /dev/null +++ b/src/Resources/public/images/payment-methods/visa-electron.svg @@ -0,0 +1 @@ +Visa Electron \ No newline at end of file diff --git a/src/Resources/public/images/payment-methods/visa.svg b/src/Resources/public/images/payment-methods/visa.svg new file mode 100644 index 0000000..1299545 --- /dev/null +++ b/src/Resources/public/images/payment-methods/visa.svg @@ -0,0 +1 @@ +Visa diff --git a/src/Resources/views/shop/checkout/select_payment/_payment_method_logos.html.twig b/src/Resources/views/shop/checkout/select_payment/_payment_method_logos.html.twig new file mode 100644 index 0000000..92b7eff --- /dev/null +++ b/src/Resources/views/shop/checkout/select_payment/_payment_method_logos.html.twig @@ -0,0 +1,19 @@ +{# + Rendered inside each payment method's choice item on the checkout payment step + (sylius.shop.checkout.select_payment.choice_item_content). Shows the brands the Quickpay payment + window will offer for this method, derived from the gateway configuration's `payment_methods`. + Override this template, or the plugin's `checkout.payment_method_logos` configuration, to change + the images. +#} +{% set logos = setono_sylius_quickpay_payment_method_logos(method) %} +{% if logos is not empty %} +
+ {% for logo in logos %} + {% if logo.image is not null %} + {{ logo.label }} + {% else %} + {{ logo.label }} + {% endif %} + {% endfor %} +
+{% endif %} diff --git a/src/Twig/PaymentMethodLogoExtension.php b/src/Twig/PaymentMethodLogoExtension.php new file mode 100644 index 0000000..e544541 --- /dev/null +++ b/src/Twig/PaymentMethodLogoExtension.php @@ -0,0 +1,18 @@ + + */ + public function logos(PaymentMethodInterface $paymentMethod): array + { + return $this->paymentMethodLogoProvider->provide($paymentMethod); + } +} diff --git a/tests/Checkout/PaymentMethodLogoProviderTest.php b/tests/Checkout/PaymentMethodLogoProviderTest.php new file mode 100644 index 0000000..86f0faf --- /dev/null +++ b/tests/Checkout/PaymentMethodLogoProviderTest.php @@ -0,0 +1,141 @@ +provide($this->createPaymentMethod('quickpay', 'mobilepay, dankort,visa , apple-pay')); + + self::assertSame(['mobilepay', 'dankort', 'visa', 'apple-pay'], self::tokens($logos)); + self::assertSame(['MobilePay', 'Dankort', 'Visa', 'Apple Pay'], self::labels($logos)); + self::assertSame('bundles/setonosyliusquickpayplugin/images/payment-methods/mobilepay.svg', $logos[0]->image); + self::assertSame('bundles/setonosyliusquickpayplugin/images/payment-methods/apple-pay.svg', $logos[3]->image); + } + + /** + * @test + */ + public function it_expands_creditcard_to_the_configured_brands(): void + { + self::assertSame(['visa', 'mastercard'], self::tokens((new PaymentMethodLogoProvider())->provide($this->createPaymentMethod('quickpay', 'creditcard')))); + self::assertSame( + ['dankort', 'visa', 'mastercard', 'mobilepay'], + self::tokens((new PaymentMethodLogoProvider([], ['dankort', 'visa', 'mastercard']))->provide($this->createPaymentMethod('quickpay', 'creditcard, mobilepay'))), + ); + } + + /** + * @test + */ + public function it_follows_quickpay_token_grammar(): void + { + $logos = (new PaymentMethodLogoProvider())->provide($this->createPaymentMethod( + 'quickpay', + '3d-creditcard, !diners, visa-dk, mastercard-debet-dk, mobilepay-subscriptions, american-express-dk, klarna-payments, visa-electron-dk, VISA', + )); + + // 3d- is stripped, exclusions are skipped, regional/debit variants collapse onto their brand, + // and the result is deduplicated in first-seen order + self::assertSame( + ['visa', 'mastercard', 'mobilepay', 'american-express', 'klarna-payments', 'visa-electron'], + self::tokens($logos), + ); + } + + /** + * @test + */ + public function it_renders_unknown_tokens_as_text_labels_without_stripping_them(): void + { + $logos = (new PaymentMethodLogoProvider())->provide($this->createPaymentMethod('quickpay', 'resurs, unzer-pay-later-invoice')); + + self::assertSame(['resurs', 'unzer-pay-later-invoice'], self::tokens($logos)); + self::assertSame(['Resurs Bank', 'Unzer Pay Later Invoice'], self::labels($logos)); + self::assertNull($logos[0]->image); + self::assertNull($logos[1]->image); + } + + /** + * @test + */ + public function it_lets_configuration_add_override_and_hide_logos(): void + { + $provider = new PaymentMethodLogoProvider([ + 'mobilepay' => 'build/images/my-mobilepay.svg', + 'resurs' => 'https://cdn.example/resurs.png', + 'apple-pay' => null, + ]); + + $logos = $provider->provide($this->createPaymentMethod('quickpay', 'mobilepay, resurs, apple-pay, visa')); + + self::assertSame(['mobilepay', 'resurs', 'visa'], self::tokens($logos)); + self::assertSame('build/images/my-mobilepay.svg', $logos[0]->image); + self::assertSame('MobilePay', $logos[0]->label); + self::assertSame('https://cdn.example/resurs.png', $logos[1]->image); + self::assertSame('Resurs Bank', $logos[1]->label); + } + + /** + * @test + */ + public function it_returns_nothing_for_other_gateways_and_empty_configuration(): void + { + $provider = new PaymentMethodLogoProvider(); + + self::assertSame([], $provider->provide($this->createPaymentMethod('offline', 'visa'))); + self::assertSame([], $provider->provide($this->createPaymentMethod('quickpay', ''))); + self::assertSame([], $provider->provide($this->createPaymentMethod('quickpay', null))); + + $method = $this->prophesize(PaymentMethodInterface::class); + $method->getGatewayConfig()->willReturn(null); + self::assertSame([], $provider->provide($method->reveal())); + } + + private function createPaymentMethod(string $factoryName, ?string $paymentMethods): PaymentMethodInterface + { + $gatewayConfig = $this->prophesize(GatewayConfigInterface::class); + $gatewayConfig->getFactoryName()->willReturn($factoryName); + $gatewayConfig->getConfig()->willReturn(null === $paymentMethods ? [] : ['payment_methods' => $paymentMethods]); + + $method = $this->prophesize(PaymentMethodInterface::class); + $method->getGatewayConfig()->willReturn($gatewayConfig->reveal()); + + return $method->reveal(); + } + + /** + * @param list $logos + * + * @return list + */ + private static function tokens(array $logos): array + { + return array_map(static fn (PaymentMethodLogo $logo): string => $logo->token, $logos); + } + + /** + * @param list $logos + * + * @return list + */ + private static function labels(array $logos): array + { + return array_map(static fn (PaymentMethodLogo $logo): string => $logo->label, $logos); + } +} diff --git a/tests/DependencyInjection/ConfigurationTest.php b/tests/DependencyInjection/ConfigurationTest.php index 5eb7b74..b8e1343 100644 --- a/tests/DependencyInjection/ConfigurationTest.php +++ b/tests/DependencyInjection/ConfigurationTest.php @@ -28,6 +28,10 @@ public function it_has_sensible_defaults(): void 'refund' => true, 'cancel' => true, ], + 'checkout' => [ + 'payment_method_logos' => [], + 'creditcard_brands' => ['visa', 'mastercard'], + ], ]); } @@ -45,6 +49,31 @@ public function it_allows_disabling_individual_operations(): void 'refund' => true, 'cancel' => false, ], + 'checkout' => [ + 'payment_method_logos' => [], + 'creditcard_brands' => ['visa', 'mastercard'], + ], + ]); + } + + /** + * @test + */ + public function it_keeps_payment_method_tokens_as_configured(): void + { + $this->assertProcessedConfigurationEquals([ + ['checkout' => ['payment_method_logos' => ['mobilepay' => 'build/images/mobilepay.svg', 'apple-pay' => null]]], + ['checkout' => ['creditcard_brands' => ['dankort', 'visa']]], + ], [ + 'operations' => [ + 'capture' => true, + 'refund' => true, + 'cancel' => true, + ], + 'checkout' => [ + 'payment_method_logos' => ['mobilepay' => 'build/images/mobilepay.svg', 'apple-pay' => null], + 'creditcard_brands' => ['dankort', 'visa'], + ], ]); } } diff --git a/tests/DependencyInjection/SetonoSyliusQuickpayExtensionTest.php b/tests/DependencyInjection/SetonoSyliusQuickpayExtensionTest.php index b5136e9..d449c74 100644 --- a/tests/DependencyInjection/SetonoSyliusQuickpayExtensionTest.php +++ b/tests/DependencyInjection/SetonoSyliusQuickpayExtensionTest.php @@ -30,6 +30,23 @@ public function it_sets_the_operation_toggle_parameters(): void $this->assertContainerBuilderHasParameter('setono_sylius_quickpay.operations.cancel', true); } + /** + * @test + */ + public function it_sets_the_checkout_presentation_parameters(): void + { + $container = new ContainerBuilder(); + (new SetonoSyliusQuickpayExtension())->load([[ + 'checkout' => [ + 'payment_method_logos' => ['mobilepay' => 'build/mobilepay.svg', 'resurs' => null], + 'creditcard_brands' => ['visa', 'mastercard', 'dankort'], + ], + ]], $container); + + self::assertSame(['mobilepay' => 'build/mobilepay.svg', 'resurs' => null], $container->getParameter('setono_sylius_quickpay.checkout.payment_method_logos')); + self::assertSame(['visa', 'mastercard', 'dankort'], $container->getParameter('setono_sylius_quickpay.checkout.creditcard_brands')); + } + /** * @test */ @@ -157,6 +174,14 @@ public function getAlias(): string ], ], ], + 'sylius.shop.checkout.select_payment.choice_item_content' => [ + 'blocks' => [ + 'setono_sylius_quickpay_payment_method_logos' => [ + 'template' => '@SetonoSyliusQuickpayPlugin/shop/checkout/select_payment/_payment_method_logos.html.twig', + 'priority' => -10, + ], + ], + ], ], ], ], $container->getExtensionConfig('sylius_ui')); diff --git a/tests/Twig/Fixtures/PaymentMethodLogo/payment_method_logos.test b/tests/Twig/Fixtures/PaymentMethodLogo/payment_method_logos.test new file mode 100644 index 0000000..65be041 --- /dev/null +++ b/tests/Twig/Fixtures/PaymentMethodLogo/payment_method_logos.test @@ -0,0 +1,38 @@ +--TEST-- +"setono_sylius_quickpay_payment_method_logos" lists the brands a Quickpay method's payment window offers +--TEMPLATE-- +{% for logo in setono_sylius_quickpay_payment_method_logos(method) %}{{ logo.token }}|{{ logo.label }}|{{ logo.image ?? '-' }} +{% else %}nothing{% endfor %} +--DATA-- +return ['method' => (static function () { + $gatewayConfig = new \Sylius\Bundle\PayumBundle\Model\GatewayConfig(); + $gatewayConfig->setFactoryName('quickpay'); + $gatewayConfig->setGatewayName('quickpay'); + $gatewayConfig->setConfig(['payment_methods' => '3d-creditcard, !diners, mobilepay-subscriptions, resurs, unzer-pay-later-invoice']); + + $method = new \Sylius\Component\Core\Model\PaymentMethod(); + $method->setGatewayConfig($gatewayConfig); + + return $method; +})()] +--EXPECT-- +dankort|Dankort|bundles/setonosyliusquickpayplugin/images/payment-methods/dankort.svg +visa|Visa|bundles/setonosyliusquickpayplugin/images/payment-methods/visa.svg +mastercard|Mastercard|bundles/setonosyliusquickpayplugin/images/payment-methods/mastercard.svg +mobilepay|MobilePay|bundles/setonosyliusquickpayplugin/images/payment-methods/mobilepay.svg +resurs|Resurs Bank|https://cdn.example/resurs.png +unzer-pay-later-invoice|Unzer Pay Later Invoice|- +--DATA-- +return ['method' => (static function () { + $gatewayConfig = new \Sylius\Bundle\PayumBundle\Model\GatewayConfig(); + $gatewayConfig->setFactoryName('offline'); + $gatewayConfig->setGatewayName('offline'); + $gatewayConfig->setConfig(['payment_methods' => 'visa']); + + $method = new \Sylius\Component\Core\Model\PaymentMethod(); + $method->setGatewayConfig($gatewayConfig); + + return $method; +})()] +--EXPECT-- +nothing diff --git a/tests/Twig/PaymentMethodLogoExtensionTest.php b/tests/Twig/PaymentMethodLogoExtensionTest.php new file mode 100644 index 0000000..18696d0 --- /dev/null +++ b/tests/Twig/PaymentMethodLogoExtensionTest.php @@ -0,0 +1,51 @@ + + */ + protected function getExtensions(): array + { + return [new PaymentMethodLogoExtension()]; + } + + /** + * @return list + */ + protected function getRuntimeLoaders(): array + { + return [new FactoryRuntimeLoader([ + PaymentMethodLogoRuntime::class => static fn (): PaymentMethodLogoRuntime => new PaymentMethodLogoRuntime( + new PaymentMethodLogoProvider(['resurs' => 'https://cdn.example/resurs.png'], ['dankort', 'visa', 'mastercard']), + ), + ])]; + } +}