diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f5c3d58c..21f085038 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+# 9.6.6
+- PPI-1044 - Improved compatibility of Vaulting with Store API usage and Headless setups
+
# 9.6.5
- PPI-1025 - Improves the performance of the installment banner in the Storefront
- PPI-1043 - Fixes an issue, where a payment method is toggled twice in the Administration
diff --git a/CHANGELOG_de-DE.md b/CHANGELOG_de-DE.md
index 041218755..04054d621 100644
--- a/CHANGELOG_de-DE.md
+++ b/CHANGELOG_de-DE.md
@@ -1,3 +1,6 @@
+# 9.6.6
+- PPI-1044 - Verbesserte Kompatibilität von Vaulting mit der Store-API und Headless
+
# 9.6.5
- PPI-1025 - Verbessert die Performance des Ratenzahlungsbanners in der Storefront
- PPI-1043 - Behebt ein Problem, bei dem eine Zahlungsmethode doppelt umgeschalten wurde
diff --git a/src/Checkout/Exception/MissingCustomerVaultTokenException.php b/src/Checkout/Exception/MissingCustomerVaultTokenException.php
new file mode 100644
index 000000000..e143d5b4d
--- /dev/null
+++ b/src/Checkout/Exception/MissingCustomerVaultTokenException.php
@@ -0,0 +1,34 @@
+
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Swag\PayPal\Checkout\Exception;
+
+use Shopware\Core\Framework\Log\Package;
+use Shopware\Core\Framework\ShopwareHttpException;
+use Symfony\Component\HttpFoundation\Response;
+
+#[Package('checkout')]
+class MissingCustomerVaultTokenException extends ShopwareHttpException
+{
+ public function __construct(string $customerId)
+ {
+ parent::__construct(
+ 'Missing vault token for customer "{{ customerId }}"',
+ ['customerId' => $customerId]
+ );
+ }
+
+ public function getStatusCode(): int
+ {
+ return Response::HTTP_BAD_REQUEST;
+ }
+
+ public function getErrorCode(): string
+ {
+ return 'SWAG_PAYPAL__MISSING_CUSTOMER_VAULT_TOKEN';
+ }
+}
diff --git a/src/Checkout/SalesChannel/CustomerVaultTokenRoute.php b/src/Checkout/SalesChannel/CustomerVaultTokenRoute.php
new file mode 100644
index 000000000..429153843
--- /dev/null
+++ b/src/Checkout/SalesChannel/CustomerVaultTokenRoute.php
@@ -0,0 +1,74 @@
+
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Swag\PayPal\Checkout\SalesChannel;
+
+use OpenApi\Attributes as OA;
+use Shopware\Core\Checkout\Customer\CustomerException;
+use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
+use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
+use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
+use Shopware\Core\Framework\Log\Package;
+use Shopware\Core\System\SalesChannel\SalesChannelContext;
+use Swag\PayPal\Checkout\Exception\MissingCustomerVaultTokenException;
+use Swag\PayPal\Checkout\TokenResponse;
+use Swag\PayPal\DataAbstractionLayer\VaultToken\VaultTokenEntity;
+use Swag\PayPal\RestApi\V1\Resource\TokenResourceInterface;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Attribute\Route;
+
+#[Package('checkout')]
+#[Route(defaults: ['_routeScope' => ['store-api']])]
+class CustomerVaultTokenRoute
+{
+ /**
+ * @internal
+ */
+ public function __construct(
+ private EntityRepository $vaultRepository,
+ private TokenResourceInterface $tokenResource,
+ ) {
+ }
+
+ #[OA\Get(
+ path: '/paypal/vault-token',
+ operationId: 'getPayPalCustomerVaultToken',
+ description: 'Tries to get the customer vault token',
+ tags: ['Store API', 'PayPal'],
+ responses: [new OA\Response(
+ response: Response::HTTP_OK,
+ description: 'The customer vault token',
+ content: new OA\JsonContent(properties: [new OA\Property(
+ property: 'token',
+ type: 'string'
+ )])
+ )],
+ )]
+ #[Route(path: '/store-api/paypal/vault-token', name: 'store-api.paypal.vault.token', defaults: ['_loginRequired' => true, '_loginRequiredAllowGuest' => false], methods: ['GET'])]
+ public function getVaultToken(SalesChannelContext $context): TokenResponse
+ {
+ $customer = $context->getCustomer();
+ if (!$customer || $customer->getGuest()) {
+ throw CustomerException::customerNotLoggedIn();
+ }
+
+ $criteria = new Criteria();
+ $criteria->addFilter(new EqualsFilter('mainMapping.customerId', $customer->getId()));
+ $criteria->addFilter(new EqualsFilter('mainMapping.paymentMethodId', $context->getPaymentMethod()->getId()));
+
+ /** @var VaultTokenEntity|null $vault */
+ $vault = $this->vaultRepository->search($criteria, $context->getContext())->first();
+
+ $token = $this->tokenResource->getUserIdToken($context->getSalesChannelId(), $vault?->getTokenCustomer())->getIdToken();
+
+ if ($token === null) {
+ throw new MissingCustomerVaultTokenException($customer->getId());
+ }
+
+ return new TokenResponse($token);
+ }
+}
diff --git a/src/Resources/Schema/StoreApi/openapi.json b/src/Resources/Schema/StoreApi/openapi.json
index 59b32eef3..8d10ef3f5 100644
--- a/src/Resources/Schema/StoreApi/openapi.json
+++ b/src/Resources/Schema/StoreApi/openapi.json
@@ -166,6 +166,33 @@
}
}
},
+ "/paypal/vault-token": {
+ "get": {
+ "tags": [
+ "Store API",
+ "PayPal"
+ ],
+ "description": "Tries to get the customer vault token",
+ "operationId": "getPayPalCustomerVaultToken",
+ "responses": {
+ "200": {
+ "description": "The customer vault token",
+ "content": {
+ "application/json": {
+ "schema": {
+ "properties": {
+ "token": {
+ "type": "string"
+ }
+ },
+ "type": "object"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
"/paypal/payment-method-eligibility": {
"post": {
"tags": [
diff --git a/src/Resources/config/services/checkout.xml b/src/Resources/config/services/checkout.xml
index 396ba201c..cf4629a6d 100644
--- a/src/Resources/config/services/checkout.xml
+++ b/src/Resources/config/services/checkout.xml
@@ -45,6 +45,11 @@
+
+
+
+
+
-
+
-
+
diff --git a/src/Storefront/Data/Service/SPBCheckoutDataService.php b/src/Storefront/Data/Service/SPBCheckoutDataService.php
index bc9725f10..f6e47bba6 100644
--- a/src/Storefront/Data/Service/SPBCheckoutDataService.php
+++ b/src/Storefront/Data/Service/SPBCheckoutDataService.php
@@ -13,6 +13,7 @@
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
use Swag\PayPal\Checkout\ExpressCheckout\SalesChannel\ExpressPrepareCheckoutRoute;
+use Swag\PayPal\Checkout\SalesChannel\CustomerVaultTokenRoute;
use Swag\PayPal\Checkout\SPBCheckout\SPBCheckoutButtonData;
use Swag\PayPal\Setting\Service\CredentialsUtilInterface;
use Swag\PayPal\Setting\Settings;
@@ -37,7 +38,7 @@ public function __construct(
RouterInterface $router,
SystemConfigService $systemConfigService,
CredentialsUtilInterface $credentialsUtil,
- private readonly VaultDataService $vaultDataService,
+ private readonly CustomerVaultTokenRoute $customerVaultTokenRoute,
) {
parent::__construct($paymentMethodDataRegistry, $localeCodeProvider, $router, $systemConfigService, $credentialsUtil);
}
@@ -75,7 +76,7 @@ public function buildCheckoutData(
'useAlternativePaymentMethods' => $this->systemConfigService->getBool(Settings::SPB_ALTERNATIVE_PAYMENT_METHODS_ENABLED, $salesChannelId),
'disabledAlternativePaymentMethods' => $this->getDisabledAlternativePaymentMethods($price, $currency->getIsoCode()),
'showPayLater' => $this->systemConfigService->getBool(Settings::SPB_SHOW_PAY_LATER, $salesChannelId),
- 'userIdToken' => $this->vaultDataService->getUserIdToken($context),
+ 'userIdToken' => $this->customerVaultTokenRoute->getVaultToken($context)->getToken(),
]));
}
diff --git a/src/Storefront/Data/Service/VaultDataService.php b/src/Storefront/Data/Service/VaultDataService.php
index e0af61dbe..ad5ab91f5 100644
--- a/src/Storefront/Data/Service/VaultDataService.php
+++ b/src/Storefront/Data/Service/VaultDataService.php
@@ -61,6 +61,9 @@ public function buildData(SalesChannelContext $context): ?VaultData
return $struct;
}
+ /**
+ * @deprecated tag:v10.0.0 - Will be removed. Use `CustomerVaultTokenRoute::getVaultToken` instead
+ */
public function getUserIdToken(SalesChannelContext $context): ?string
{
$customer = $context->getCustomer();
diff --git a/src/Storefront/Data/Service/VenmoCheckoutDataService.php b/src/Storefront/Data/Service/VenmoCheckoutDataService.php
index e66d87061..91e6afd50 100644
--- a/src/Storefront/Data/Service/VenmoCheckoutDataService.php
+++ b/src/Storefront/Data/Service/VenmoCheckoutDataService.php
@@ -12,6 +12,7 @@
use Shopware\Core\Framework\Log\Package;
use Shopware\Core\System\SalesChannel\SalesChannelContext;
use Shopware\Core\System\SystemConfig\SystemConfigService;
+use Swag\PayPal\Checkout\SalesChannel\CustomerVaultTokenRoute;
use Swag\PayPal\Setting\Service\CredentialsUtilInterface;
use Swag\PayPal\Storefront\Data\Struct\VenmoCheckoutData;
use Swag\PayPal\Util\Lifecycle\Method\PaymentMethodDataRegistry;
@@ -31,7 +32,7 @@ public function __construct(
RouterInterface $router,
SystemConfigService $systemConfigService,
CredentialsUtilInterface $credentialsUtil,
- private readonly VaultDataService $vaultDataService,
+ private readonly CustomerVaultTokenRoute $customerVaultTokenRoute,
) {
parent::__construct($paymentMethodDataRegistry, $localeCodeProvider, $router, $systemConfigService, $credentialsUtil);
}
@@ -41,7 +42,7 @@ public function buildCheckoutData(SalesChannelContext $context, ?Cart $cart = nu
$data = $this->getBaseData($context, $order);
return (new VenmoCheckoutData())->assign(\array_merge($data, [
- 'userIdToken' => $this->vaultDataService->getUserIdToken($context),
+ 'userIdToken' => $this->customerVaultTokenRoute->getVaultToken($context)->getToken(),
]));
}
diff --git a/tests/Checkout/SalesChannel/CustomerVaultTokenRouteTest.php b/tests/Checkout/SalesChannel/CustomerVaultTokenRouteTest.php
new file mode 100644
index 000000000..b81648427
--- /dev/null
+++ b/tests/Checkout/SalesChannel/CustomerVaultTokenRouteTest.php
@@ -0,0 +1,103 @@
+
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Swag\PayPal\Test\Checkout\SalesChannel;
+
+use PHPUnit\Framework\MockObject\MockObject;
+use PHPUnit\Framework\TestCase;
+use Shopware\Core\Checkout\Customer\CustomerException;
+use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
+use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
+use Shopware\Core\Framework\Log\Package;
+use Shopware\Core\Test\Generator;
+use Swag\PayPal\Checkout\Exception\MissingCustomerVaultTokenException;
+use Swag\PayPal\Checkout\SalesChannel\CustomerVaultTokenRoute;
+use Swag\PayPal\DataAbstractionLayer\VaultToken\VaultTokenEntity;
+use Swag\PayPal\RestApi\V1\Api\Token;
+use Swag\PayPal\RestApi\V1\Resource\TokenResourceInterface;
+
+/**
+ * @internal
+ */
+#[Package('checkout')]
+class CustomerVaultTokenRouteTest extends TestCase
+{
+ private EntityRepository&MockObject $repository;
+
+ private TokenResourceInterface&MockObject $tokenResource;
+
+ private CustomerVaultTokenRoute $route;
+
+ protected function setUp(): void
+ {
+ $this->repository = $this->createMock(EntityRepository::class);
+ $this->tokenResource = $this->createMock(TokenResourceInterface::class);
+
+ $this->route = new CustomerVaultTokenRoute($this->repository, $this->tokenResource);
+ }
+
+ public function testGetVaultTokenWithoutCustomer(): void
+ {
+ $salesChannelContext = Generator::generateSalesChannelContext();
+ $salesChannelContext->assign(['customer' => null]);
+
+ $this->expectException(CustomerException::class);
+
+ $this->route->getVaultToken($salesChannelContext);
+ }
+
+ public function testGetVaultTokenWithGuestCustomer(): void
+ {
+ $salesChannelContext = Generator::generateSalesChannelContext();
+ $salesChannelContext->getCustomer()?->setGuest(true);
+
+ $this->expectException(CustomerException::class);
+
+ $this->route->getVaultToken($salesChannelContext);
+ }
+
+ public function testGetVaultToken(): void
+ {
+ $salesChannelContext = Generator::generateSalesChannelContext();
+ $salesChannelContext->getCustomer()?->setGuest(false);
+
+ $entitySearchResult = $this->createMock(EntitySearchResult::class);
+ $this->repository->expects(static::once())->method('search')->willReturn($entitySearchResult);
+ $entitySearchResult->expects(static::once())->method('first')->willReturn(new VaultTokenEntity());
+
+ $token = new Token();
+ $token->assign(['idToken' => 'dummy-token', 'expiresIn' => 45000]);
+
+ $this->tokenResource->expects(static::once())->method('getUserIdToken')->willReturn($token);
+
+ $response = $this->route->getVaultToken($salesChannelContext);
+
+ static::assertSame(
+ $token->getIdToken(),
+ $response->getToken()
+ );
+ }
+
+ public function testVaultTokenIsNull(): void
+ {
+ $salesChannelContext = Generator::generateSalesChannelContext();
+ $salesChannelContext->getCustomer()?->setGuest(false);
+
+ $entitySearchResult = $this->createMock(EntitySearchResult::class);
+ $this->repository->expects(static::once())->method('search')->willReturn($entitySearchResult);
+ $entitySearchResult->expects(static::once())->method('first')->willReturn(new VaultTokenEntity());
+
+ $token = new Token();
+ $token->assign(['idToken' => null, 'expiresIn' => 45000]);
+
+ $this->tokenResource->expects(static::once())->method('getUserIdToken')->willReturn($token);
+
+ $this->expectException(MissingCustomerVaultTokenException::class);
+
+ $this->route->getVaultToken($salesChannelContext);
+ }
+}
diff --git a/tests/Storefront/Data/CheckoutSubscriberTest.php b/tests/Storefront/Data/CheckoutSubscriberTest.php
index 6129eca0f..82bc4beda 100644
--- a/tests/Storefront/Data/CheckoutSubscriberTest.php
+++ b/tests/Storefront/Data/CheckoutSubscriberTest.php
@@ -8,6 +8,7 @@
namespace Swag\PayPal\Test\Storefront\Data;
use PHPUnit\Framework\Attributes\DataProvider;
+use PHPUnit\Framework\MockObject\Exception;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
use Shopware\Core\Checkout\Payment\PaymentMethodCollection;
@@ -28,6 +29,7 @@
use Swag\PayPal\Checkout\Payment\Method\SEPAHandler;
use Swag\PayPal\Checkout\Payment\Method\VenmoHandler;
use Swag\PayPal\Checkout\Payment\PayPalPaymentHandler;
+use Swag\PayPal\Checkout\SalesChannel\CustomerVaultTokenRoute;
use Swag\PayPal\RestApi\V2\PaymentIntentV2;
use Swag\PayPal\Setting\Service\CredentialsUtil;
use Swag\PayPal\Setting\Service\SettingsValidationService;
@@ -37,7 +39,6 @@
use Swag\PayPal\Storefront\Data\Service\PayLaterCheckoutDataService;
use Swag\PayPal\Storefront\Data\Service\SEPACheckoutDataService;
use Swag\PayPal\Storefront\Data\Service\SPBCheckoutDataService;
-use Swag\PayPal\Storefront\Data\Service\VaultDataService;
use Swag\PayPal\Storefront\Data\Service\VenmoCheckoutDataService;
use Swag\PayPal\Storefront\Data\Struct\AbstractCheckoutData;
use Swag\PayPal\Storefront\Data\Struct\ACDCCheckoutData;
@@ -285,6 +286,9 @@ public static function dataProviderPaymentMethods(): iterable
];
}
+ /**
+ * @throws Exception
+ */
private function createSubscriber(array $settingsOverride = []): CheckoutDataSubscriber
{
$settings = $this->createSystemConfigServiceMock(\array_merge([
@@ -321,7 +325,7 @@ private function createSubscriber(array $settingsOverride = []): CheckoutDataSub
$router,
$settings,
$credentialsUtil,
- $this->createMock(VaultDataService::class),
+ $this->createMock(CustomerVaultTokenRoute::class)
);
$acdcDataService = new ACDCCheckoutDataService(
@@ -338,7 +342,7 @@ private function createSubscriber(array $settingsOverride = []): CheckoutDataSub
$router,
$settings,
$credentialsUtil,
- $this->createMock(VaultDataService::class),
+ $this->createMock(CustomerVaultTokenRoute::class)
);
$sessionMock = $this->createMock(Session::class);
diff --git a/tests/Storefront/Data/Service/VaultDataServiceTest.php b/tests/Storefront/Data/Service/VaultDataServiceTest.php
index d4cd15505..64389aaf0 100644
--- a/tests/Storefront/Data/Service/VaultDataServiceTest.php
+++ b/tests/Storefront/Data/Service/VaultDataServiceTest.php
@@ -16,7 +16,6 @@
use Shopware\Core\Test\Stub\DataAbstractionLayer\StaticEntityRepository;
use Swag\PayPal\DataAbstractionLayer\VaultToken\VaultTokenCollection;
use Swag\PayPal\DataAbstractionLayer\VaultToken\VaultTokenEntity;
-use Swag\PayPal\RestApi\V1\Api\Token;
use Swag\PayPal\RestApi\V1\Resource\TokenResourceInterface;
use Swag\PayPal\Storefront\Data\Service\VaultDataService;
use Swag\PayPal\Util\Lifecycle\Method\AbstractMethodData;
@@ -159,83 +158,4 @@ public function testBuildDataWithExistingToken(): void
static::assertSame('test-identifier', $data->getIdentifier());
static::assertSame('card', $data->getSnippetType());
}
-
- public function testGetUserIdTokenWithGuestCustomer(): void
- {
- $salesChannelContext = Generator::createSalesChannelContext();
- $salesChannelContext->getCustomer()?->setGuest(true);
-
- $service = new VaultDataService(
- new StaticEntityRepository([]),
- $this->createMock(PaymentMethodDataRegistry::class),
- $this->createMock(TokenResourceInterface::class),
- );
-
- $token = $service->getUserIdToken($salesChannelContext);
- static::assertNull($token);
- }
-
- public function testGetUserIdTokenWithExistingToken(): void
- {
- $salesChannelContext = Generator::createSalesChannelContext();
- $salesChannelContext->getCustomer()?->setGuest(false);
-
- $token = new Token();
- $token->setIdToken('test-id-token');
- $tokenResource = $this->createMock(TokenResourceInterface::class);
- $tokenResource
- ->expects(static::once())
- ->method('getUserIdToken')
- ->with($salesChannelContext->getSalesChannelId(), 'token-customer')
- ->willReturn($token);
-
- $existingToken = new VaultTokenEntity();
- $existingToken->setId(Uuid::randomHex());
- $existingToken->setTokenCustomer('token-customer');
-
- $repository = new StaticEntityRepository([static function (Criteria $criteria) use ($existingToken, $salesChannelContext): VaultTokenCollection {
- static::assertInstanceOf(EqualsFilter::class, $criteria->getFilters()[0]);
- static::assertSame('mainMapping.customerId', $criteria->getFilters()[0]->getField());
- static::assertSame($salesChannelContext->getCustomerId(), $criteria->getFilters()[0]->getValue());
-
- static::assertInstanceOf(EqualsFilter::class, $criteria->getFilters()[1]);
- static::assertSame('mainMapping.paymentMethodId', $criteria->getFilters()[1]->getField());
- static::assertSame($salesChannelContext->getPaymentMethod()->getId(), $criteria->getFilters()[1]->getValue());
-
- return new VaultTokenCollection([$existingToken]);
- }]);
-
- $service = new VaultDataService(
- $repository,
- $this->createMock(PaymentMethodDataRegistry::class),
- $tokenResource,
- );
-
- $token = $service->getUserIdToken($salesChannelContext);
- static::assertSame('test-id-token', $token);
- }
-
- public function testGetUserIdTokenWithoutExistingToken(): void
- {
- $salesChannelContext = Generator::createSalesChannelContext();
- $salesChannelContext->getCustomer()?->setGuest(false);
-
- $token = new Token();
- $token->setIdToken('test-id-token');
- $tokenResource = $this->createMock(TokenResourceInterface::class);
- $tokenResource
- ->expects(static::once())
- ->method('getUserIdToken')
- ->with($salesChannelContext->getSalesChannelId())
- ->willReturn($token);
-
- $service = new VaultDataService(
- new StaticEntityRepository([new VaultTokenCollection()]),
- $this->createMock(PaymentMethodDataRegistry::class),
- $tokenResource,
- );
-
- $token = $service->getUserIdToken($salesChannelContext);
- static::assertSame('test-id-token', $token);
- }
}