diff --git a/CHANGELOG.md b/CHANGELOG.md
index 50228d6..79fc2d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,10 @@
# Changelog
All notable changes to this project will be documented in this file.
+## [2.2.0]
+### Added
+- Support Apple Pay functionality
+
## [2.1.0]
### Added
- Add support for MarketPay payment methods.
diff --git a/Docs/Configuration/shopware_configure_altapay_terminal_detail.png b/Docs/Configuration/shopware_configure_altapay_terminal_detail.png
index f49261a..2988f5c 100644
Binary files a/Docs/Configuration/shopware_configure_altapay_terminal_detail.png and b/Docs/Configuration/shopware_configure_altapay_terminal_detail.png differ
diff --git a/composer.json b/composer.json
index 6181673..5fb7123 100644
--- a/composer.json
+++ b/composer.json
@@ -3,7 +3,7 @@
"description": "AltaPay plugin for Shopware 6",
"type": "shopware-platform-plugin",
"license": "MIT",
- "version": "2.1.0",
+ "version": "2.2.0",
"authors": [
{
"name": "AltaPay A/S",
diff --git a/sonar-project.properties b/sonar-project.properties
index 6b906d5..5fc80f5 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -2,3 +2,8 @@ sonar.projectKey=AltaPay_plugin-shopware_6ff55bfb-f13e-4cba-a54c-9f0804511bbf
sonar.projectBaseDir=src
sonar.coverage.exclusions=**
sonar.cpd.exclusions=**
+sonar.issue.ignore.multicriteria=e1,e2
+sonar.issue.ignore.multicriteria.e1.ruleKey=php:S1142
+sonar.issue.ignore.multicriteria.e1.resourceKey=**/Controller/*.php
+sonar.issue.ignore.multicriteria.e2.ruleKey=php:S3776
+sonar.issue.ignore.multicriteria.e2.resourceKey=**/Service/*.php
diff --git a/src/Controller/CallbackController.php b/src/Controller/CallbackController.php
index eaf7f31..ac3513d 100644
--- a/src/Controller/CallbackController.php
+++ b/src/Controller/CallbackController.php
@@ -3,6 +3,7 @@
namespace Wexo\AltaPay\Controller;
use Exception;
+use GuzzleHttp\Exception\GuzzleException;
use Psr\Log\LoggerInterface;
use Shopware\Core\Checkout\Cart\CartException;
use Shopware\Core\Checkout\Order\OrderEntity;
@@ -16,19 +17,24 @@
use Shopware\Storefront\Controller\StorefrontController;
use SimpleXMLElement;
use Symfony\Component\HttpFoundation\IpUtils;
+use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
+use Wexo\AltaPay\Service\Exception\AltaPayException;
use Wexo\AltaPay\Service\PaymentService;
use Twig\Environment;
#[Route(defaults: ['_routeScope' => ['storefront']])]
class CallbackController
{
+ private const ERROR_MISSING_PARAMETERS = 'Missing required parameters';
+
public function __construct(
protected readonly PaymentService $paymentService,
protected readonly EntityRepository $orderRepository,
@@ -37,7 +43,8 @@ public function __construct(
protected readonly TranslatorInterface $translator,
protected readonly SystemConfigService $systemConfigService,
protected readonly EntityRepository $mediaRepository,
- protected Environment $twig
+ protected Environment $twig,
+ protected readonly RequestStack $requestStack
) {
}
@@ -176,7 +183,7 @@ public function notification(Request $request, SalesChannelContext $salesChannel
$result = new SimpleXMLElement($request->get('xml'));
$orderNumber = (string)$result?->Body?->Transactions?->Transaction?->ShopOrderId;
if (!$orderNumber) {
- throw new Exception();
+ throw new AltaPayException();
}
} catch (Exception) {
return new Response('Error processing request', 400);
@@ -195,4 +202,165 @@ public function notification(Request $request, SalesChannelContext $salesChannel
$this->paymentService->transactionCallback($result, $order, $transaction, $salesChannelContext, $allRequestParams, true);
return new Response("Acknowledged", 200);
}
+
+ /**
+ * Validate an Apple Pay merchant session using only the payment method ID.
+ */
+ #[Route(
+ path: '/altapay/applepay/validate-merchant-by-method',
+ name: 'altapay.applepay.validate_merchant_by_method',
+ defaults: ['auth_required' => false, 'csrf_protected' => false],
+ methods: ['POST']
+ )]
+ public function validateMerchantByMethod(Request $request, SalesChannelContext $salesChannelContext): Response
+ {
+ $validationUrl = $request->get('validationUrl');
+ $paymentMethodId = $request->get('paymentMethodId');
+
+ if (!$validationUrl || !$paymentMethodId) {
+ return new JsonResponse(['success' => false, 'error' => self::ERROR_MISSING_PARAMETERS]);
+ }
+
+ try {
+ $config = $this->paymentService->getApplePayConfigByPaymentMethodId(
+ $paymentMethodId,
+ $salesChannelContext->getContext(),
+ $salesChannelContext->getSalesChannelId()
+ );
+ } catch (\Throwable $e) {
+ $this->logger->error('Apple Pay validate-by-method config error: ' . $e->getMessage(), ['exception' => $e]);
+ return new JsonResponse(['success' => false, 'error' => 'Payment method config not found: ' . $e->getMessage()]);
+ }
+
+ try {
+ $applePaySession = $this->paymentService->cardWalletSession(
+ $validationUrl,
+ $config['terminal'],
+ $request->getHost(),
+ $salesChannelContext->getSalesChannelId()
+ );
+ return new JsonResponse(['success' => true, 'applePaySession' => $applePaySession]);
+ } catch (\Throwable $e) {
+ $this->logger->error('Apple Pay validate-by-method error: ' . $e->getMessage(), ['exception' => $e]);
+ return new JsonResponse(['success' => false, 'error' => $e->getMessage()]);
+ }
+ }
+
+ /**
+ * Serve the dedicated Apple Pay checkout page.
+ */
+ #[Route(
+ path: '/altapay/applepay/checkout',
+ name: 'altapay.applepay.checkout',
+ defaults: ['auth_required' => false],
+ methods: ['GET']
+ )]
+ public function applePayCheckout(Request $request, SalesChannelContext $salesChannelContext): Response
+ {
+ $orderTransactionId = $request->get('orderTransactionId');
+ $returnUrl = $request->get('returnUrl');
+
+ if (!$orderTransactionId || !$returnUrl) {
+ return new Response('Missing orderTransactionId or returnUrl', 400);
+ }
+
+ try {
+ $config = $this->paymentService->getApplePayConfig($orderTransactionId, $salesChannelContext->getContext());
+ } catch (\Exception $e) {
+ $this->logger->error('Apple Pay checkout config error: ' . $e->getMessage());
+ return new Response('Error loading payment data: ' . $e->getMessage(), 500);
+ }
+
+ $config['returnUrl'] = $returnUrl;
+ $config['sessionUrl'] = $this->router->generate('altapay.applepay.session', [], UrlGeneratorInterface::ABSOLUTE_URL);
+ $config['authorizeUrl']= $this->router->generate('altapay.applepay.authorize', [], UrlGeneratorInterface::ABSOLUTE_URL);
+
+ return $this->renderTemplate(
+ '@WexoAltaPay/gateway/applepay.html.twig',
+ ['applePayData' => $config]
+ );
+ }
+
+ /**
+ * Validate the Apple Pay merchant session with AltaPay (called by onvalidatemerchant JS event).
+ */
+ #[Route(
+ path: '/altapay/applepay/session',
+ name: 'altapay.applepay.session',
+ defaults: ['auth_required' => false, 'csrf_protected' => false],
+ methods: ['POST']
+ )]
+ public function applePaySession(Request $request, SalesChannelContext $salesChannelContext): Response
+ {
+ $validationUrl = $request->get('validationUrl');
+ $orderTransactionId = $request->get('orderTransactionId');
+
+ if (!$validationUrl || !$orderTransactionId) {
+ return new JsonResponse(['success' => false, 'error' => self::ERROR_MISSING_PARAMETERS], 400);
+ }
+
+ try {
+ $config = $this->paymentService->getApplePayConfig($orderTransactionId, $salesChannelContext->getContext());
+ } catch (GuzzleException $e) {
+ $this->logger->error('Apple Pay session config error: ' . $e->getMessage());
+ return new JsonResponse(['success' => false, 'error' => 'Failed to load payment config: ' . $e->getMessage()]);
+ }
+
+ try {
+ $applePaySession = $this->paymentService->cardWalletSession(
+ $validationUrl,
+ $config['terminal'],
+ $request->getHost(),
+ $config['salesChannelId']
+ );
+ return new JsonResponse(['success' => true, 'applePaySession' => $applePaySession]);
+ } catch (GuzzleException $e) {
+ $this->logger->error('Apple Pay merchant validation network error: ' . $e->getMessage());
+ return new JsonResponse(['success' => false, 'error' => 'Network error during merchant validation: ' . $e->getMessage()]);
+ } catch (\Exception $e) {
+ $this->logger->error('Apple Pay merchant validation error: ' . $e->getMessage());
+ return new JsonResponse(['success' => false, 'error' => $e->getMessage()]);
+ }
+ }
+
+ /**
+ * Process the Apple Pay payment token received after onpaymentauthorized.
+ */
+ #[Route(
+ path: '/altapay/applepay/authorize',
+ name: 'altapay.applepay.authorize',
+ defaults: ['auth_required' => false, 'csrf_protected' => false],
+ methods: ['POST']
+ )]
+ public function applePayAuthorize(Request $request, SalesChannelContext $salesChannelContext): Response
+ {
+ $content = $request->getContent();
+ $data = json_decode($content, true);
+
+ $orderTransactionId = $data['orderTransactionId'] ?? null;
+ $providerData = $data['providerData'] ?? null;
+ $returnUrl = $data['returnUrl'] ?? null;
+
+ if (!$orderTransactionId || $providerData === null || !$returnUrl) {
+ return new JsonResponse(['success' => false, 'error' => self::ERROR_MISSING_PARAMETERS], 400);
+ }
+
+ $providerDataJson = json_encode($providerData);
+
+ try {
+ $finalReturnUrl = $this->paymentService->processApplePayPayment(
+ $orderTransactionId,
+ $providerDataJson,
+ $returnUrl,
+ $salesChannelContext->getContext()
+ );
+ return new JsonResponse(['success' => true, 'redirectUrl' => $finalReturnUrl]);
+ } catch (GuzzleException $e) {
+ $this->logger->error('Apple Pay authorize network error: ' . $e->getMessage());
+ return new JsonResponse(['success' => false, 'error' => 'Network error during payment processing: ' . $e->getMessage()]);
+ } catch (\Exception $e) {
+ $this->logger->error('Apple Pay authorize error: ' . $e->getMessage());
+ return new JsonResponse(['success' => false, 'error' => 'Payment processing failed: ' . $e->getMessage()]);
+ }
+ }
}
diff --git a/src/Resources/config/services.xml b/src/Resources/config/services.xml
index f2f28d6..ae26907 100644
--- a/src/Resources/config/services.xml
+++ b/src/Resources/config/services.xml
@@ -40,6 +40,7 @@
+
@@ -71,5 +72,10 @@
+
+
+
+
+
diff --git a/src/Resources/views/gateway/apple-pay-session.html.twig b/src/Resources/views/gateway/apple-pay-session.html.twig
new file mode 100644
index 0000000..a99a230
--- /dev/null
+++ b/src/Resources/views/gateway/apple-pay-session.html.twig
@@ -0,0 +1,92 @@
+{#
+ Shared Apple Pay session helper.
+
+ Wires the ApplePaySession event handlers that are identical regardless of
+ where the session is started from: merchant validation (POST to a caller
+ supplied URL), payment-method reselection, shipping no-ops, and cancel.
+
+ Only `onvalidatemerchant`'s request body and `onpaymentauthorized` differ
+ between entry points (pre-order vs. post-order), so those are left to the
+ caller via `cfg.validateMerchantParams` and `cfg.onPaymentAuthorized`.
+
+ Include this once before calling AltaPayApplePay.runSession(cfg), where
+ cfg is:
+ {
+ countryCode, currencyCode, supportedNetworks (array), label, amount,
+ validateMerchantUrl, validateMerchantParams (plain object, optional),
+ onPaymentAuthorized: function (event, session) {},
+ onError: function (message) {},
+ onCancel: function () {}
+ }
+#}
+
diff --git a/src/Resources/views/gateway/applepay.html.twig b/src/Resources/views/gateway/applepay.html.twig
new file mode 100644
index 0000000..6dfdfae
--- /dev/null
+++ b/src/Resources/views/gateway/applepay.html.twig
@@ -0,0 +1,270 @@
+
+{% block altapay_applepay_head %}
+
+
+
+
+ {{ 'altapay.gateway.title'|trans }}
+
+
+
+
+
+{% endblock %}
+
+{% block altapay_applepay_body %}
+
+
+ {% block applepay_title %}
+
{{ 'altapay.gateway.title'|trans }}
+ {% endblock %}
+
+ {% block applepay_order_info %}
+
+ {{ 'altapay.gateway.orderNumber'|trans }}: {{ applePayData.orderTransactionId|slice(0,8)|upper }}
+
+ {% endblock %}
+
+ {% block applepay_amount %}
+
{{ applePayData.currency }} {{ applePayData.amount }}
+ {% endblock %}
+
+ {% block applepay_button %}
+
+
+
+ Apple Pay is not available in this browser. Please use Safari on a supported Apple device.
+
+
+ {% endblock %}
+
+ {% block applepay_error %}
+
+ {% endblock %}
+
+ {% block applepay_back %}
+
+ ← {{ 'checkout.finishButtonBackToShop'|trans|striptags }}
+
+ {% endblock %}
+
+
+ {% block applepay_script %}
+ {% include '@WexoAltaPay/gateway/apple-pay-session.html.twig' %}
+
+ {% endblock %}
+
+{% endblock %}
+
diff --git a/src/Resources/views/storefront/page/checkout/confirm/index.html.twig b/src/Resources/views/storefront/page/checkout/confirm/index.html.twig
new file mode 100644
index 0000000..e7ea775
--- /dev/null
+++ b/src/Resources/views/storefront/page/checkout/confirm/index.html.twig
@@ -0,0 +1,204 @@
+{% sw_extends '@Storefront/storefront/page/checkout/confirm/index.html.twig' %}
+
+{% block page_checkout_confirm %}
+ {{ parent() }}
+
+ {#
+ Detect Apple Pay methods. customFields may be on pm.customFields (not translated),
+ check that first, fall back to pm.translated.customFields.
+ #}
+ {% set altapayAppleMethods = [] %}
+ {% if page.paymentMethods is defined %}
+ {% for pm in page.paymentMethods %}
+ {% set cf = pm.customFields ?? pm.translated.customFields ?? {} %}
+ {% if cf.wexoAltaPayIsApplePay is defined and cf.wexoAltaPayIsApplePay %}
+ {% set altapayAppleMethods = altapayAppleMethods|merge([{
+ 'id': pm.id,
+ 'label': pm.translated.name,
+ 'networks': (cf.wexoAltaPayApplePayNetworks is defined and cf.wexoAltaPayApplePayNetworks)
+ ? cf.wexoAltaPayApplePayNetworks
+ : 'visa,masterCard,amex'
+ }]) %}
+ {% endif %}
+ {% endfor %}
+ {% endif %}
+
+ {% if altapayAppleMethods is not empty %}
+
+ {% include '@WexoAltaPay/gateway/apple-pay-session.html.twig' %}
+
+ {% endif %}
+{% endblock %}
diff --git a/src/Service/Exception/AltaPayException.php b/src/Service/Exception/AltaPayException.php
new file mode 100644
index 0000000..08b3c31
--- /dev/null
+++ b/src/Service/Exception/AltaPayException.php
@@ -0,0 +1,7 @@
+get($orderTransactionId);
if (!$orderTransaction) {
- throw new \RuntimeException("OrderTransaction not found.");
+ throw new AltaPayException("OrderTransaction not found.");
}
$orderId = $orderTransaction->getOrderId();
@@ -141,7 +146,7 @@ private function loadOrderTransactionAndOrder(PaymentTransactionStruct $transact
$order = $this->orderRepository->search($criteria, $context)->first();
if (!$order) {
- throw new \RuntimeException("Order not found.");
+ throw new AltaPayException("Order not found.");
}
$billingAddress = $order->getBillingAddress();
@@ -234,6 +239,20 @@ public function pay(
}
$paymentRequestType = ($paymentMethod->getTranslated()['customFields'][self::ALTAPAY_AUTO_CAPTURE_CUSTOM_FIELD] ?? null) ? 'paymentAndCapture' : 'payment';
+
+ $isApplePay = (bool)($paymentMethod->getTranslated()['customFields'][self::ALTAPAY_IS_APPLE_PAY_CUSTOM_FIELD] ?? false);
+ if ($isApplePay) {
+ $applePayCheckoutUrl = $this->router->generate(
+ 'altapay.applepay.checkout',
+ [
+ 'orderTransactionId' => $orderTransactionId,
+ 'returnUrl' => $transaction->getReturnUrl(),
+ ],
+ UrlGeneratorInterface::ABSOLUTE_URL
+ );
+ return new RedirectResponse($applePayCheckoutUrl);
+ }
+
try {
$altaPayResponse = $this->createPaymentRequest(
$order,
@@ -376,8 +395,26 @@ public function finalize(
);
$allRequestParams = array_merge($request->query->all(), $request->request->all());
+ $xmlString = $request->get('xml');
+
+ // For Apple Pay payments the XML result is stored in the session
+ if (empty($xmlString)) {
+ $sessionKey = self::ALTAPAY_APPLE_PAY_RESULT_PREFIX . $transaction->getOrderTransactionId();
+ try {
+ $session = $this->requestStack->getSession();
+ $storedData = $session->get($sessionKey);
+ if ($storedData) {
+ $xmlString = $storedData['xml'];
+ $allRequestParams = array_merge($allRequestParams, $storedData['params'] ?? []);
+ $session->remove($sessionKey);
+ }
+ } catch (\Exception $e) {
+ $this->logger->warning('Apple Pay: failed to restore stored payment result from session: ' . $e->getMessage());
+ }
+ }
+
$this->transactionCallback(
- new SimpleXMLElement($request->get('xml')),
+ new SimpleXMLElement($xmlString),
$order,
$orderTransaction,
$salesChannelContext,
@@ -655,7 +692,8 @@ public function createPaymentRequest(
SalesChannelContext $context,
string $terminal,
string $paymentRequestType,
- string $sessionId = null
+ string $sessionId = null,
+ string $providerData = null
): SimpleXMLElement {
$orderLines = [];
$itemIdCounter = 0;
@@ -831,6 +869,10 @@ public function createPaymentRequest(
$formParams['session_id'] = $sessionId;
}
+ if ($providerData !== null) {
+ $formParams['provider_data'] = $providerData;
+ }
+
$checkoutStyle = $this->systemConfigService->get('WexoAltaPay.config.checkoutStyle', $salesChannelId);
if (!empty($checkoutStyle)) {
$formParams['form_template'] = $checkoutStyle;
@@ -843,8 +885,222 @@ public function createPaymentRequest(
return new SimpleXMLElement($response->getBody()->getContents());
}
- public function getTransaction(OrderEntity $order, string $salesChannelId): ResponseInterface
+ /**
+ * Load Apple Pay config (terminal, label, networks) from a payment method ID directly.
+ *
+ * @throws \RuntimeException
+ */
+ public function getApplePayConfigByPaymentMethodId(
+ string $paymentMethodId,
+ Context $context,
+ string $salesChannelId
+ ): array {
+ $criteria = new Criteria([$paymentMethodId]);
+ $paymentMethod = $this->container->get('payment_method.repository')
+ ->search($criteria, $context)
+ ->first();
+
+ if (!$paymentMethod) {
+ throw new AltaPayException('Payment method not found: ' . $paymentMethodId);
+ }
+
+ $customFields = $paymentMethod->getTranslated()['customFields'] ?? [];
+
+ if (empty($customFields[self::ALTAPAY_IS_APPLE_PAY_CUSTOM_FIELD])) {
+ throw new AltaPayException('Payment method is not an Apple Pay terminal: ' . $paymentMethodId);
+ }
+
+ $terminal = $customFields[self::ALTAPAY_TERMINAL_ID_CUSTOM_FIELD] ?? null;
+ $salesChannelTerminal = $customFields[self::ALTAPAY_SALES_CHANNEL_TERMINAL_ID] ?? null;
+
+ if (!empty($salesChannelTerminal)) {
+ $field = 'WexoAltaPay.config.' . $salesChannelTerminal;
+ $salesChannelTerminalValue = $this->systemConfigService->get($field, $salesChannelId);
+ if (!empty($salesChannelTerminalValue)) {
+ $terminal = $salesChannelTerminalValue;
+ }
+ }
+
+ if (empty($terminal)) {
+ throw new AltaPayException(
+ 'Apple Pay terminal is not configured. Please set the AltaPay Terminal ID on the payment method "'
+ . ($paymentMethod->getName() ?? $paymentMethodId) . '" in the Shopware admin.'
+ );
+ }
+
+ return [
+ 'terminal' => $terminal,
+ 'applePayLabel' => (string)($paymentMethod->getTranslated()['name'] ?? $paymentMethod->getName() ?? 'Payment'),
+ 'networks' => array_values(array_filter(array_map('trim',
+ explode(',', $customFields[self::ALTAPAY_APPLE_PAY_NETWORKS_CUSTOM_FIELD] ?? 'visa,masterCard,amex')
+ ))),
+ ];
+ }
+
+ /**
+ * Load all Apple Pay configuration needed for the checkout page, merchant validation,
+ * and payment authorisation. Reads fresh from the DB — no session dependency.
+ *
+ * @throws \RuntimeException
+ */
+ public function getApplePayConfig(string $orderTransactionId, Context $context): array
{
+ $criteria = new Criteria([$orderTransactionId]);
+ $orderTransaction = $this->orderTransactionRepository->search($criteria, $context)->get($orderTransactionId);
+
+ if (!$orderTransaction) {
+ throw new AltaPayException('OrderTransaction not found: ' . $orderTransactionId);
+ }
+
+ $orderId = $orderTransaction->getOrderId();
+ $criteria = new Criteria([$orderId]);
+ $criteria->addAssociation('currency');
+ $criteria->addAssociation('billingAddress.country');
+ $criteria->addAssociation('salesChannel');
+
+ $order = $this->orderRepository->search($criteria, $context)->first();
+ if (!$order) {
+ throw new AltaPayException('Order not found for transaction: ' . $orderTransactionId);
+ }
+
+ $pmCriteria = new Criteria([$orderTransaction->getPaymentMethodId()]);
+ $paymentMethod = $this->container->get('payment_method.repository')
+ ->search($pmCriteria, $context)
+ ->first();
+
+ $customFields = ($paymentMethod ? $paymentMethod->getTranslated()['customFields'] : null) ?? [];
+
+ $terminal = $customFields[self::ALTAPAY_TERMINAL_ID_CUSTOM_FIELD] ?? null;
+ $salesChannelTerminal = $customFields[self::ALTAPAY_SALES_CHANNEL_TERMINAL_ID] ?? null;
+
+ if (!empty($salesChannelTerminal)) {
+ $field = 'WexoAltaPay.config.' . $salesChannelTerminal;
+ $salesChannelTerminalValue = $this->systemConfigService->get($field, $order->getSalesChannelId());
+ if (!empty($salesChannelTerminalValue)) {
+ $terminal = $salesChannelTerminalValue;
+ }
+ }
+
+ $paymentMethodName = $paymentMethod
+ ? ($paymentMethod->getTranslated()['name'] ?? $paymentMethod->getName())
+ : null;
+ $applePayLabel = (string)$paymentMethodName ?: $order->getSalesChannel()?->getName() ?: 'Payment';
+ $networksRaw = (string)($customFields[self::ALTAPAY_APPLE_PAY_NETWORKS_CUSTOM_FIELD] ?? 'visa,masterCard,amex');
+ $networks = array_values(array_filter(array_map('trim', explode(',', $networksRaw))));
+ $paymentRequestType = ($customFields[self::ALTAPAY_AUTO_CAPTURE_CUSTOM_FIELD] ?? false) ? 'paymentAndCapture' : 'payment';
+
+ return [
+ 'orderTransactionId' => $orderTransactionId,
+ 'orderId' => $order->getId(),
+ 'terminal' => $terminal,
+ 'paymentRequestType' => $paymentRequestType,
+ 'amount' => (string)round($order->getAmountTotal(), 2),
+ 'currency' => $order->getCurrency()->getIsoCode(),
+ 'countryCode' => $order->getBillingAddress()?->getCountry()?->getIso() ?? 'US',
+ 'salesChannelId' => $order->getSalesChannelId(),
+ 'applePayLabel' => $applePayLabel,
+ 'supportedNetworks' => $networks,
+ ];
+ }
+
+ /**
+ * Validate an Apple Pay merchant session via AltaPay's cardWalletSession API.
+ *
+ * @see https://documentation.altapay.com/Content/Ecom/API/API%20Methods/cardWalletSession.htm
+ * @throws GuzzleException
+ */
+ public function cardWalletSession(
+ string $validationUrl,
+ string $terminal,
+ string $domain,
+ string $salesChannelId
+ ): string {
+ $response = $this->getAltaPayClient($salesChannelId)->request('POST', 'cardWallet/session', [
+ 'form_params' => [
+ 'terminal' => $terminal,
+ 'validationUrl' => $validationUrl,
+ 'domain' => $domain,
+ ]
+ ]);
+
+ $xml = new SimpleXMLElement($response->getBody()->getContents());
+
+ if ((string)$xml->Body->Result !== 'Success') {
+ throw new AltaPayException(
+ 'Apple Pay merchant validation failed: '
+ . ((string)($xml->Body->MerchantErrorMessage ?? $xml->Header->ErrorMessage ?? 'Unknown error'))
+ );
+ }
+
+ return (string)$xml->Body->ApplePaySession;
+ }
+
+ /**
+ * Process an Apple Pay payment token via AltaPay's createPaymentRequest with provider_data.
+ *
+ * @throws GuzzleException|\RuntimeException
+ * @return string The returnUrl that the browser should navigate to (Shopware's finalize URL).
+ */
+ public function processApplePayPayment(
+ string $orderTransactionId,
+ string $providerData,
+ string $returnUrl,
+ Context $context
+ ): string {
+ $config = $this->getApplePayConfig($orderTransactionId, $context);
+
+ $criteria = new Criteria([$config['orderId']]);
+ $criteria->addAssociation('currency');
+ $order = $this->orderRepository->search($criteria, $context)->first();
+ if (!$order) {
+ throw new AltaPayException('Order not found.');
+ }
+
+ $amount = number_format((float)$order->getAmountTotal(), 2, '.', '');
+ $currency = $order->getCurrency()?->getIsoCode() ?? 'EUR';
+ $shopOrderId = $order->getOrderNumber();
+
+ // --- Call AltaPay cardWallet/authorize ---
+ $response = $this->getAltaPayClient($config['salesChannelId'])->request('POST', 'cardWallet/authorize', [
+ 'form_params' => [
+ 'provider_data' => $providerData,
+ 'terminal' => $config['terminal'],
+ 'shop_orderid' => $shopOrderId,
+ 'amount' => $amount,
+ 'currency' => $currency,
+ ]
+ ]);
+
+ $altaPayResponse = new SimpleXMLElement($response->getBody()->getContents());
+
+ $result = strtolower((string)($altaPayResponse->Body?->Result ?? ''));
+ if (!in_array($result, ['success', 'open'], true)) {
+ throw new AltaPayException(
+ 'AltaPay Apple Pay payment failed: '
+ . ((string)($altaPayResponse->Body?->MerchantErrorMessage ?? $altaPayResponse->Header?->ErrorMessage ?? 'Unknown error'))
+ );
+ }
+
+ // --- Store XML result in session so finalize() can read it ---
+ $xmlString = $altaPayResponse->asXML();
+ $resultKey = self::ALTAPAY_APPLE_PAY_RESULT_PREFIX . $orderTransactionId;
+ try {
+ $session = $this->requestStack->getSession();
+ $session->set($resultKey, [
+ 'xml' => $xmlString,
+ 'params' => [
+ 'type' => $config['paymentRequestType'] ?? 'payment',
+ 'require_capture' => 'false',
+ ],
+ ]);
+ } catch (\Exception $e) {
+ $this->logger->warning('Apple Pay: could not store payment result in session: ' . $e->getMessage());
+ }
+
+ return $returnUrl;
+ }
+
+ public function getTransaction(OrderEntity $order, string $salesChannelId): ResponseInterface {
return $this->getAltaPayClient($salesChannelId)->request('GET', 'payments', [
'query' => [
'transaction_id' => $order->getCustomFields()[self::ALTAPAY_TRANSACTION_ID_CUSTOM_FIELD],
diff --git a/src/Service/Setup/CustomFieldSetupService.php b/src/Service/Setup/CustomFieldSetupService.php
index e9762e6..4bb572f 100644
--- a/src/Service/Setup/CustomFieldSetupService.php
+++ b/src/Service/Setup/CustomFieldSetupService.php
@@ -186,37 +186,46 @@ private function createPaymentMethodCustomField(Context $context): void
context: $context
);
- $terminals = [];
- $languages = ['de-DE', 'en-GB', 'da-DK'];
-
- // Add default empty option
- $terminals[] = [
- 'label' => [
- 'de-DE' => 'Wählen Sie ein Terminal aus',
- 'en-GB' => 'Select a terminal',
- 'da-DK' => 'Vælg en terminal',
+ $this->addCustomField(
+ name: PaymentService::ALTAPAY_IS_APPLE_PAY_CUSTOM_FIELD,
+ type: CustomFieldTypes::SWITCH,
+ config: [
+ 'label' => [
+ 'de-DE' => 'Ist Apple Pay',
+ 'en-GB' => 'Is Apple Pay',
+ 'da-DK' => 'Er Apple Pay',
+ ],
+ 'customFieldPosition' => 4,
+ 'defaultValue' => false,
],
- 'value' => '',
- ];
-
- for ($i = 1; $i <= 10; $i++) {
- $label = [];
-
- foreach ($languages as $lang) {
- $label[$lang] = 'terminal ' . $i;
- }
+ customFieldSetId: $fieldSetId,
+ context: $context
+ );
- $terminals[] = [
- 'label' => $label,
- 'value' => 'terminal' . $i,
- ];
- }
+ $this->addCustomField(
+ name: PaymentService::ALTAPAY_APPLE_PAY_NETWORKS_CUSTOM_FIELD,
+ type: CustomFieldTypes::TEXT,
+ config: [
+ 'label' => [
+ 'de-DE' => 'Apple Pay unterstützte Netzwerke',
+ 'en-GB' => 'Apple Pay Supported Networks',
+ 'da-DK' => 'Apple Pay understøttede netværk',
+ ],
+ 'helpText' => [
+ 'en-GB' => 'Comma-separated list of supported networks, e.g.: visa,masterCard,amex',
+ ],
+ 'customFieldPosition' => 6,
+ 'defaultValue' => 'visa,masterCard,amex,discover',
+ ],
+ customFieldSetId: $fieldSetId,
+ context: $context
+ );
$this->addCustomField(
name: PaymentService::ALTAPAY_SALES_CHANNEL_TERMINAL_ID,
type: CustomFieldTypes::SELECT,
config: [
- 'customFieldPosition' => 4,
+ 'customFieldPosition' => 7,
'componentName' => 'sw-single-select',
'customFieldType' => 'select',
'label' => [
@@ -234,12 +243,46 @@ private function createPaymentMethodCustomField(Context $context): void
'en-GB' => 'Select the terminal configuration for the sales channel. If left empty, the default terminal ID from the payment method will be used.',
'da-DK' => 'Vælg terminal-konfigurationen for salgskanalen. Hvis den efterlades tom, vil standard terminal-ID fra betalingsmetoden blive brugt.',
],
- 'options' => $terminals,
+ 'options' => $this->buildTerminalOptions(),
],
customFieldSetId: $fieldSetId,
context: $context
);
+ }
+
+ /**
+ * @return array, value: string}>
+ */
+ private function buildTerminalOptions(): array
+ {
+ $languages = ['de-DE', 'en-GB', 'da-DK'];
+
+ // Add default empty option
+ $terminals = [
+ [
+ 'label' => [
+ 'de-DE' => 'Wählen Sie ein Terminal aus',
+ 'en-GB' => 'Select a terminal',
+ 'da-DK' => 'Vælg en terminal',
+ ],
+ 'value' => '',
+ ],
+ ];
+
+ for ($i = 1; $i <= 10; $i++) {
+ $label = [];
+
+ foreach ($languages as $lang) {
+ $label[$lang] = 'terminal ' . $i;
+ }
+
+ $terminals[] = [
+ 'label' => $label,
+ 'value' => 'terminal' . $i,
+ ];
+ }
+ return $terminals;
}
private function addCustomField(
diff --git a/src/Subscriber/ApplePayAvailabilitySubscriber.php b/src/Subscriber/ApplePayAvailabilitySubscriber.php
new file mode 100644
index 0000000..435c258
--- /dev/null
+++ b/src/Subscriber/ApplePayAvailabilitySubscriber.php
@@ -0,0 +1,52 @@
+ 'hideApplePayForNonSafari',
+ ];
+ }
+
+ public function hideApplePayForNonSafari(CheckoutConfirmPageLoadedEvent $event): void
+ {
+ $request = $this->requestStack->getCurrentRequest();
+ $userAgent = $request ? $request->headers->get('User-Agent', '') : '';
+
+ /* Same regex as AltaPay Magento2 plugin: exclude Chrome and Android browsers */
+ if (preg_match('/^((?!chrome|android).)*safari/i', $userAgent)) {
+ return;
+ }
+
+ $paymentMethods = $event->getPage()->getPaymentMethods();
+ if (!$paymentMethods) {
+ return;
+ }
+
+ $filtered = $paymentMethods->filter(function ($method) {
+ $customFields = $method->getTranslated()['customFields'] ?? $method->getCustomFields() ?? [];
+ return empty($customFields[PaymentService::ALTAPAY_IS_APPLE_PAY_CUSTOM_FIELD]);
+ });
+
+ $event->getPage()->setPaymentMethods($filtered);
+ }
+}
diff --git a/src/WexoAltaPay.php b/src/WexoAltaPay.php
index 97bef51..caa8be0 100644
--- a/src/WexoAltaPay.php
+++ b/src/WexoAltaPay.php
@@ -21,7 +21,7 @@ class WexoAltaPay extends Plugin
public const ALTAPAY_FIELD_SET_NAME = "wexoAltaPay";
public const ALTAPAY_PAYMENT_METHOD_FIELD_SET_NAME = "wexoAltaPayPaymentMethod";
public const ALTAPAY_CART_TOKEN = "wexoAltaPayCartToken";
- public const ALTAPAY_PLUGIN_VERSION = '2.1.0';
+ public const ALTAPAY_PLUGIN_VERSION = '2.2.0';
public const ALTAPAY_PLUGIN_NAME = 'WexoAltaPay';
public function update(UpdateContext $updateContext): void
diff --git a/wiki.md b/wiki.md
index d6c491d..50905ee 100644
--- a/wiki.md
+++ b/wiki.md
@@ -106,23 +106,27 @@ be provided by AltaPay.
7. Click the **Save** button.
8. Once saved, a new section, **Custom fields**, will appear with the options **AltaPay Terminal ID**(where you must enter the terminal name from AltaPay), **Auto Capture**, and **Surcharge**.
+
+9. Enable this option when the payment method is for Apple Pay. This marks the terminal as an Apple Pay payment option.
+
+10. Enter the card networks supported for Apple Pay, separated by commas, for example: amex,visa,mastercard. This defines which card networks are allowed for Apple Pay payments.

-9. Select a sales channel-specific terminal configuration. If left empty, the default terminal ID (from the field above) will be used.
+11. Select a sales channel-specific terminal configuration. If left empty, the default terminal ID (from the field above) will be used.
-10. Click the **Save** button again.
+12. Click the **Save** button again.
-11. Now click on your desired shop from the **Sales Channels** menu on the left.
+13. Now click on your desired shop from the **Sales Channels** menu on the left.
-12. In the **General** tab, scroll down to the **Payment and shipping** section & search by name for the payment method you just created.
+14. In the **General** tab, scroll down to the **Payment and shipping** section & search by name for the payment method you just created.

-13. Choose the payment method and click Save button in the top-right corner.
+15. Choose the payment method and click Save button in the top-right corner.

-14. Once the payment methods are configured, you will be ready to process transactions through AltaPay.
+16. Once the payment methods are configured, you will be ready to process transactions through AltaPay.
