From e27aad937d72fcd17265c7ea92882c91a961a63f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Fri, 26 Sep 2025 09:43:55 +0200 Subject: [PATCH 01/18] Add missing event name (shop_customer_default_address_update) --- .../CustomerProfileUpdatedSubscriberInterface.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/EventSubscriber/CustomerProfileUpdatedSubscriberInterface.php b/src/EventSubscriber/CustomerProfileUpdatedSubscriberInterface.php index 3652780..20de0b7 100644 --- a/src/EventSubscriber/CustomerProfileUpdatedSubscriberInterface.php +++ b/src/EventSubscriber/CustomerProfileUpdatedSubscriberInterface.php @@ -24,6 +24,7 @@ interface CustomerProfileUpdatedSubscriberInterface 'sylius_customer_profile' => 'customer_profile_update', 'sylius_admin_customer_update' => 'admin_customer_update', 'sylius_shop_account_profile_update' => 'shop_customer_update', + 'sylius_shop_account_address_book_set_as_default' => 'shop_customer_default_address_update', 'sylius_shop_register' => 'customer_registration', 'sylius_shop_checkout_address' => 'customer_order_address_provided', ]; From 5028d04706b2b849dd8e8335646180d75f91db6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Mon, 29 Sep 2025 12:38:43 +0200 Subject: [PATCH 02/18] [UC-25] Update CustomerPayloadBuilder --- src/Builder/Payload/CustomerPayloadBuilder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Builder/Payload/CustomerPayloadBuilder.php b/src/Builder/Payload/CustomerPayloadBuilder.php index 3eb3913..5e22791 100644 --- a/src/Builder/Payload/CustomerPayloadBuilder.php +++ b/src/Builder/Payload/CustomerPayloadBuilder.php @@ -25,8 +25,8 @@ public function build(string $email, ?CustomerInterface $customer = null, ?Addre $payload = [ 'custom_id' => strtolower($customer?->getEmail() ?? $email), 'email' => strtolower($customer?->getEmail() ?? $email), - 'firstName' => $customer?->getFirstName(), - 'lastName' => $customer?->getLastName(), + 'first_name' => $customer?->getFirstName(), + 'last_name' => $customer?->getLastName(), 'phone_number' => $customer?->getPhoneNumber(), 'country' => $address?->getCountryCode(), 'region' => $address?->getProvinceCode(), From d65320a351ba37ee5935d1c15d4ab4525e09a133 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Tue, 30 Sep 2025 13:23:06 +0200 Subject: [PATCH 03/18] [UC-25] Encode query parameter value in UserApi --- src/Api/UserApi.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/UserApi.php b/src/Api/UserApi.php index cce4e2c..741284f 100644 --- a/src/Api/UserApi.php +++ b/src/Api/UserApi.php @@ -28,7 +28,7 @@ public function findUser( $url = $this->getApiEndpointUrl( $resource, self::FIND_USER_ENDPOINT, - sprintf('?%s=%s', $field, $value), + sprintf('?%s=%s', $field, rawurlencode($value)), ); return $this->request( From a6ac1e6c3a35678ba79a5ba49624f6beb2105eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Tue, 30 Sep 2025 13:23:15 +0200 Subject: [PATCH 04/18] [UC-23] Remove product feed details from documentation --- doc/functionalities.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/functionalities.md b/doc/functionalities.md index 4ff1607..660c773 100644 --- a/doc/functionalities.md +++ b/doc/functionalities.md @@ -22,9 +22,8 @@ The **BitBagSyliusUserComPlugin** integrates **User.com** with Sylius-based stor ### 4. Event-Driven System - Each customer interaction generates an **event**, which is stored and sent to **User.com** for automation and reporting. -### 5. Product Persistence & Feed Generation +### 5. Product Persistence - **Persists products** within the system for accurate data reporting. -- Generates a **product feed** that can be used for marketing and analytics purposes. ### 6. Tag Manager Script Injection - Allows users to **inject custom scripts** via **Tag Manager**. From 778e108059c5b2aa5476a3093cfd8d9250a0c0f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Tue, 30 Sep 2025 15:39:02 +0200 Subject: [PATCH 05/18] [UC-26] Refactor user_com_customer_info script to improve null and empty field handling --- templates/Scripts/_userComScripts.html.twig | 37 ++++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/templates/Scripts/_userComScripts.html.twig b/templates/Scripts/_userComScripts.html.twig index 2c7a4af..f7699fc 100644 --- a/templates/Scripts/_userComScripts.html.twig +++ b/templates/Scripts/_userComScripts.html.twig @@ -2,15 +2,34 @@ {% set active = constant('BitBag\\SyliusUserComPlugin\\Builder\\Payload\\CustomerPayloadBuilderInterface::STATUS_USER')%} {% set visitor = constant('BitBag\\SyliusUserComPlugin\\Builder\\Payload\\CustomerPayloadBuilderInterface::STATUS_VISITOR')%} {% set customer = sylius.customer|default(null) %} - +{% if customer is not null %} + +{% else %} + +{% endif %} {% set apiAwareResource = getUserComApiAwareResource() %} {% if null != apiAwareResource and null != apiAwareResource.userComGTMContainerId %} From 1d829a06c9e608a361265ad3520559ce6906cb86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Wed, 1 Oct 2025 11:04:35 +0200 Subject: [PATCH 06/18] [UC-28] Update product event mappings in OrderUpdateManagerInterface --- src/Manager/OrderUpdateManagerInterface.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Manager/OrderUpdateManagerInterface.php b/src/Manager/OrderUpdateManagerInterface.php index 8b43f19..e71c707 100644 --- a/src/Manager/OrderUpdateManagerInterface.php +++ b/src/Manager/OrderUpdateManagerInterface.php @@ -16,8 +16,8 @@ interface OrderUpdateManagerInterface { public const PRODUCT_EVENT_MAP = [ - OrderInterface::STATE_NEW => 'purchase', - OrderInterface::STATE_FULFILLED => 'reservation', + OrderInterface::STATE_NEW => 'order', + OrderInterface::STATE_FULFILLED => 'purchase', OrderInterface::STATE_CANCELLED => 'remove', ]; From 4617766c0403cd837bc15908e6602b64b2990630 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Wed, 1 Oct 2025 14:01:04 +0200 Subject: [PATCH 07/18] [UC-29] Add event to CustomerWithKeyUpdater --- src/Updater/CustomerWithKeyUpdater.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/Updater/CustomerWithKeyUpdater.php b/src/Updater/CustomerWithKeyUpdater.php index 90ede7a..8dec88b 100644 --- a/src/Updater/CustomerWithKeyUpdater.php +++ b/src/Updater/CustomerWithKeyUpdater.php @@ -78,11 +78,19 @@ public function updateWithUserKey( null !== $customer->getEmail() && $userFoundByKey['email'] === strtolower($customer->getEmail()) ) { - return $this->userApi->updateUser( + $user = $this->userApi->updateUser( $apiAwareResource, $userFoundByKey['id'], $payload, ); + + if (false === is_array($user) || false === array_key_exists('email', $user)) { + throw new \RuntimeException('User was not created or updated.'); + } + + $this->sendEvent($apiAwareResource, $user['email'], $eventName, $payload); + + return $user; } $userByEmailFromForm = $this->userApi->findUser( @@ -101,14 +109,13 @@ public function updateWithUserKey( ); $this->userApi->mergeUsers($apiAwareResource, $userByEmailFromForm['id'], [$userFoundByKey['id']]); - - $this->changeCookieWithEvent($user, $apiAwareResource, $eventName); + $this->changeCookieWithEvent($user, $apiAwareResource, $eventName, $payload); return $user; } $user = $this->userApi->createUser($apiAwareResource, $payload); - $this->changeCookieWithEvent($user, $apiAwareResource, $eventName); + $this->changeCookieWithEvent($user, $apiAwareResource, $eventName, $payload); return $user; } @@ -157,6 +164,7 @@ public function changeCookieWithEvent( ?array $user, UserComApiAwareInterface $apiAwareResource, string $eventName, + ?array $payload = null, ): void { if (false === is_array($user) || false === array_key_exists('id', $user) || @@ -166,6 +174,6 @@ public function changeCookieWithEvent( } $this->cookieManager->setUserComCookie($user['user_key']); - $this->sendEvent($apiAwareResource, $user['email'], $eventName); + $this->sendEvent($apiAwareResource, $user['email'], $eventName, $payload); } } From bddfcbcaa23e04b268bab83d503e91d741289bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Wed, 1 Oct 2025 15:30:30 +0200 Subject: [PATCH 08/18] [UC-29] PHPstan and ECS fix --- src/Updater/CustomerWithKeyUpdater.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Updater/CustomerWithKeyUpdater.php b/src/Updater/CustomerWithKeyUpdater.php index 8dec88b..99daa7b 100644 --- a/src/Updater/CustomerWithKeyUpdater.php +++ b/src/Updater/CustomerWithKeyUpdater.php @@ -84,10 +84,9 @@ public function updateWithUserKey( $payload, ); - if (false === is_array($user) || false === array_key_exists('email', $user)) { - throw new \RuntimeException('User was not created or updated.'); + if (!is_array($user) || !isset($user['email']) || !is_string($user['email'])) { + throw new \RuntimeException('User was not created or updated (missing email).'); } - $this->sendEvent($apiAwareResource, $user['email'], $eventName, $payload); return $user; From c6b7ea4bee05de93998b1e2a27e4c3ff47d6c4ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Wed, 1 Oct 2025 20:15:09 +0200 Subject: [PATCH 09/18] [UC-27] Add OpenApi decorator for User.com customer agreements and update database schema --- config/services/api.xml | 4 ++ src/OpenApi/OpenApiFactory.php | 101 +++++++++++++++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 src/OpenApi/OpenApiFactory.php diff --git a/config/services/api.xml b/config/services/api.xml index aeaf200..b15cfbf 100644 --- a/config/services/api.xml +++ b/config/services/api.xml @@ -21,5 +21,9 @@ + + + + diff --git a/src/OpenApi/OpenApiFactory.php b/src/OpenApi/OpenApiFactory.php new file mode 100644 index 0000000..e29745d --- /dev/null +++ b/src/OpenApi/OpenApiFactory.php @@ -0,0 +1,101 @@ +decorated)($context); + $paths = $openApi->getPaths(); + + $pathItem = new Model\PathItem( + summary: 'Synchronize user agreements coming from User.com', + post: new Model\Operation( + operationId: 'bitbag_usercom_customer_agreements_post', + tags: ['UserComAgreements'], + responses: [ + '200' => [ + 'description' => 'OK', + 'content' => [ + 'text/plain' => [ + 'schema' => [ + 'type' => 'string', + 'example' => 'OK', + ], + ], + ], + ], + '400' => ['description' => 'Invalid JSON payload'], + '401' => ['description' => 'Unauthorized'], + '404' => ['description' => 'Not found'], + '500' => ['description' => 'Internal server error'], + ], + parameters: [ + new Model\Parameter( + name: 'X-User-Com-Signature', + in: 'header', + description: 'Request signature', + required: false, + schema: ['type' => 'string'], + ), + ], + requestBody: new Model\RequestBody( + description: 'User.com agreements payload', + content: new \ArrayObject([ + 'application/json' => [ + 'schema' => [ + 'type' => 'object', + 'required' => ['extra'], + 'properties' => [ + 'extra' => [ + 'type' => 'object', + 'required' => ['email', 'agreements'], + 'properties' => [ + 'email' => [ + 'type' => 'string', + 'format' => 'email', + 'example' => 'john.doe@example.com', + ], + 'agreements' => [ + 'type' => 'object', + 'additionalProperties' => [ + 'type' => 'boolean', + ], + 'example' => [ + 'email_agreement' => true, + ], + ], + ], + ], + ], + ], + ], + ]), + ), + ), + ); + + $paths->addPath('/user-com/customer-agreements', $pathItem); + + return $openApi; + } +} From 25275f4d5dbfdaa2295da96e083e0c34dd2bca10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Thu, 2 Oct 2025 10:30:57 +0200 Subject: [PATCH 10/18] [UC-30] Introduce cookie queue handling and implement CookieFlusherSubscriber --- config/services/cookie.xml | 8 +++++ config/services/event_subscriber.xml | 8 +++++ config/services/manager.xml | 1 + src/Cookie/CookieQueue.php | 33 +++++++++++++++++++ src/Cookie/CookieQueueInterface.php | 22 +++++++++++++ .../CookieFlusherSubscriber.php | 31 +++++++++++++++++ src/Manager/CookieManager.php | 16 ++++++--- src/Updater/CustomerWithKeyUpdater.php | 2 +- 8 files changed, 115 insertions(+), 6 deletions(-) create mode 100644 config/services/cookie.xml create mode 100644 src/Cookie/CookieQueue.php create mode 100644 src/Cookie/CookieQueueInterface.php create mode 100644 src/EventSubscriber/CookieFlusherSubscriber.php diff --git a/config/services/cookie.xml b/config/services/cookie.xml new file mode 100644 index 0000000..063f778 --- /dev/null +++ b/config/services/cookie.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/config/services/event_subscriber.xml b/config/services/event_subscriber.xml index 322ec22..a40e769 100644 --- a/config/services/event_subscriber.xml +++ b/config/services/event_subscriber.xml @@ -15,5 +15,13 @@ + + + + + diff --git a/config/services/manager.xml b/config/services/manager.xml index 2dfe828..6caf8c3 100644 --- a/config/services/manager.xml +++ b/config/services/manager.xml @@ -9,6 +9,7 @@ > + queued[] = $cookie; + } + + /** @return Cookie[] */ + public function pullAll(): array + { + $all = $this->queued; + $this->queued = []; + + return $all; + } +} diff --git a/src/Cookie/CookieQueueInterface.php b/src/Cookie/CookieQueueInterface.php new file mode 100644 index 0000000..efa7f64 --- /dev/null +++ b/src/Cookie/CookieQueueInterface.php @@ -0,0 +1,22 @@ +getResponse(); + + foreach ($this->queue->pullAll() as $cookie) { + $response->headers->setCookie($cookie); + } + } +} diff --git a/src/Manager/CookieManager.php b/src/Manager/CookieManager.php index d1623c9..bb5496f 100644 --- a/src/Manager/CookieManager.php +++ b/src/Manager/CookieManager.php @@ -11,7 +11,9 @@ namespace BitBag\SyliusUserComPlugin\Manager; +use BitBag\SyliusUserComPlugin\Cookie\CookieQueueInterface; use Sylius\Component\Core\Model\AdminUserInterface; +use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; @@ -20,6 +22,7 @@ final class CookieManager implements CookieManagerInterface public function __construct( private readonly RequestStack $requestStack, private readonly TokenStorageInterface $tokenStorage, + private readonly CookieQueueInterface $queue, ) { } @@ -41,11 +44,14 @@ public function getUserComCookie(): ?string public function setUserComCookie(string $value): void { - $request = $this->requestStack->getCurrentRequest(); - if (null === $request) { - return; - } - $request->cookies->set(self::CHAT_COOKIE_NAME, $value); + $cookie = Cookie::create(self::CHAT_COOKIE_NAME) + ->withValue($value) + ->withPath('/') + ->withSecure(true) + ->withHttpOnly(true) + ->withSameSite('lax'); + + $this->queue->queue($cookie); } private function isShopUser(): bool diff --git a/src/Updater/CustomerWithKeyUpdater.php b/src/Updater/CustomerWithKeyUpdater.php index 99daa7b..f227819 100644 --- a/src/Updater/CustomerWithKeyUpdater.php +++ b/src/Updater/CustomerWithKeyUpdater.php @@ -154,7 +154,7 @@ private function updateForUserWithoutEmail( $this->userApi->mergeUsers($apiAwareResource, $customerFoundByEmail['id'], [$userFromUserKey['id']]); } - $this->sendEvent($apiAwareResource, $email, $eventName, $payload); + $this->changeCookieWithEvent($user, $apiAwareResource, $eventName); return $user; } From 7c25aafd95e717171e0544d8f37b5f519d4e3bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Thu, 2 Oct 2025 10:30:57 +0200 Subject: [PATCH 11/18] [UC-30] Introduce cookie queue handling and implement CookieFlusherSubscriber --- config/config.yml | 3 +- config/services/cookie.xml | 8 +++++ config/services/event_subscriber.xml | 8 +++++ config/services/manager.xml | 2 ++ src/Cookie/CookieQueue.php | 33 +++++++++++++++++++ src/Cookie/CookieQueueInterface.php | 22 +++++++++++++ .../CookieFlusherSubscriber.php | 31 +++++++++++++++++ src/Manager/CookieManager.php | 19 ++++++++--- src/Updater/CustomerWithKeyUpdater.php | 2 +- tests/Application/.env | 1 + 10 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 config/services/cookie.xml create mode 100644 src/Cookie/CookieQueue.php create mode 100644 src/Cookie/CookieQueueInterface.php create mode 100644 src/EventSubscriber/CookieFlusherSubscriber.php diff --git a/config/config.yml b/config/config.yml index b8cb8dd..9c3818e 100644 --- a/config/config.yml +++ b/config/config.yml @@ -2,7 +2,8 @@ parameters: user_com.frontend_api_key: '%env(USER_COM_FRONTEND_API_KEY)%' user_com.encryption_key: '%env(USER_COM_ENCRYPTION_KEY)%' user_com.encryption_iv: '%env(USER_COM_ENCRYPTION_IV)%' - + user_com.cookie_domain: '%env(USER_COM_COOKIE_DOMAIN)%' + twig: globals: user_com_frontend_api_key: '%user_com.frontend_api_key%' diff --git a/config/services/cookie.xml b/config/services/cookie.xml new file mode 100644 index 0000000..063f778 --- /dev/null +++ b/config/services/cookie.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/config/services/event_subscriber.xml b/config/services/event_subscriber.xml index 322ec22..a40e769 100644 --- a/config/services/event_subscriber.xml +++ b/config/services/event_subscriber.xml @@ -15,5 +15,13 @@ + + + + + diff --git a/config/services/manager.xml b/config/services/manager.xml index 2dfe828..fb6371e 100644 --- a/config/services/manager.xml +++ b/config/services/manager.xml @@ -9,6 +9,8 @@ > + + %user_com.cookie_domain% queued[] = $cookie; + } + + /** @return Cookie[] */ + public function pullAll(): array + { + $all = $this->queued; + $this->queued = []; + + return $all; + } +} diff --git a/src/Cookie/CookieQueueInterface.php b/src/Cookie/CookieQueueInterface.php new file mode 100644 index 0000000..efa7f64 --- /dev/null +++ b/src/Cookie/CookieQueueInterface.php @@ -0,0 +1,22 @@ +getResponse(); + + foreach ($this->queue->pullAll() as $cookie) { + $response->headers->setCookie($cookie); + } + } +} diff --git a/src/Manager/CookieManager.php b/src/Manager/CookieManager.php index d1623c9..3580f40 100644 --- a/src/Manager/CookieManager.php +++ b/src/Manager/CookieManager.php @@ -11,7 +11,9 @@ namespace BitBag\SyliusUserComPlugin\Manager; +use BitBag\SyliusUserComPlugin\Cookie\CookieQueueInterface; use Sylius\Component\Core\Model\AdminUserInterface; +use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; @@ -20,6 +22,8 @@ final class CookieManager implements CookieManagerInterface public function __construct( private readonly RequestStack $requestStack, private readonly TokenStorageInterface $tokenStorage, + private readonly CookieQueueInterface $queue, + private readonly ?string $cookieDomain = null, ) { } @@ -41,11 +45,18 @@ public function getUserComCookie(): ?string public function setUserComCookie(string $value): void { - $request = $this->requestStack->getCurrentRequest(); - if (null === $request) { - return; + $cookie = Cookie::create(self::CHAT_COOKIE_NAME) + ->withValue($value) + ->withPath('') + ->withSecure(true) + ->withHttpOnly(true) + ->withSameSite('lax'); + + if (null !== $this->cookieDomain && '' !== $this->cookieDomain) { + $cookie = $cookie->withDomain($this->cookieDomain); } - $request->cookies->set(self::CHAT_COOKIE_NAME, $value); + + $this->queue->queue($cookie); } private function isShopUser(): bool diff --git a/src/Updater/CustomerWithKeyUpdater.php b/src/Updater/CustomerWithKeyUpdater.php index 99daa7b..f227819 100644 --- a/src/Updater/CustomerWithKeyUpdater.php +++ b/src/Updater/CustomerWithKeyUpdater.php @@ -154,7 +154,7 @@ private function updateForUserWithoutEmail( $this->userApi->mergeUsers($apiAwareResource, $customerFoundByEmail['id'], [$userFromUserKey['id']]); } - $this->sendEvent($apiAwareResource, $email, $eventName, $payload); + $this->changeCookieWithEvent($user, $apiAwareResource, $eventName); return $user; } diff --git a/tests/Application/.env b/tests/Application/.env index b5d9811..7a3f667 100644 --- a/tests/Application/.env +++ b/tests/Application/.env @@ -36,5 +36,6 @@ SYLIUS_MESSENGER_TRANSPORT_CATALOG_PROMOTION_REMOVAL_FAILED_DSN=doctrine://defau USER_COM_FRONTEND_API_KEY="" USER_COM_ENCRYPTION_KEY=your-32-character-long-key USER_COM_ENCRYPTION_IV=your-16-character-long- +USER_COM_COOKIE_DOMAIN="" MESSENGER_USER_COM_ASYNCHRONOUS_DSN="doctrine://default" ###< UserCom From 3a3ce98be10f8dbd095ebb5ebbdf34bcd55e2038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Thu, 2 Oct 2025 10:30:57 +0200 Subject: [PATCH 12/18] [UC-30] Introduce cookie queue handling and implement CookieFlusherSubscriber --- config/config.yml | 3 +- config/services/cookie.xml | 8 +++++ config/services/event_subscriber.xml | 8 +++++ config/services/manager.xml | 2 ++ src/Cookie/CookieQueue.php | 33 +++++++++++++++++++ src/Cookie/CookieQueueInterface.php | 22 +++++++++++++ .../CookieFlusherSubscriber.php | 31 +++++++++++++++++ src/Manager/CookieManager.php | 19 ++++++++--- src/Updater/CustomerWithKeyUpdater.php | 2 +- tests/Application/.env | 1 + 10 files changed, 123 insertions(+), 6 deletions(-) create mode 100644 config/services/cookie.xml create mode 100644 src/Cookie/CookieQueue.php create mode 100644 src/Cookie/CookieQueueInterface.php create mode 100644 src/EventSubscriber/CookieFlusherSubscriber.php diff --git a/config/config.yml b/config/config.yml index b8cb8dd..9c3818e 100644 --- a/config/config.yml +++ b/config/config.yml @@ -2,7 +2,8 @@ parameters: user_com.frontend_api_key: '%env(USER_COM_FRONTEND_API_KEY)%' user_com.encryption_key: '%env(USER_COM_ENCRYPTION_KEY)%' user_com.encryption_iv: '%env(USER_COM_ENCRYPTION_IV)%' - + user_com.cookie_domain: '%env(USER_COM_COOKIE_DOMAIN)%' + twig: globals: user_com_frontend_api_key: '%user_com.frontend_api_key%' diff --git a/config/services/cookie.xml b/config/services/cookie.xml new file mode 100644 index 0000000..063f778 --- /dev/null +++ b/config/services/cookie.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/config/services/event_subscriber.xml b/config/services/event_subscriber.xml index 322ec22..a40e769 100644 --- a/config/services/event_subscriber.xml +++ b/config/services/event_subscriber.xml @@ -15,5 +15,13 @@ + + + + + diff --git a/config/services/manager.xml b/config/services/manager.xml index 2dfe828..fb6371e 100644 --- a/config/services/manager.xml +++ b/config/services/manager.xml @@ -9,6 +9,8 @@ > + + %user_com.cookie_domain% queued[] = $cookie; + } + + /** @return Cookie[] */ + public function pullAll(): array + { + $all = $this->queued; + $this->queued = []; + + return $all; + } +} diff --git a/src/Cookie/CookieQueueInterface.php b/src/Cookie/CookieQueueInterface.php new file mode 100644 index 0000000..efa7f64 --- /dev/null +++ b/src/Cookie/CookieQueueInterface.php @@ -0,0 +1,22 @@ +getResponse(); + + foreach ($this->queue->pullAll() as $cookie) { + $response->headers->setCookie($cookie); + } + } +} diff --git a/src/Manager/CookieManager.php b/src/Manager/CookieManager.php index d1623c9..d407a8c 100644 --- a/src/Manager/CookieManager.php +++ b/src/Manager/CookieManager.php @@ -11,7 +11,9 @@ namespace BitBag\SyliusUserComPlugin\Manager; +use BitBag\SyliusUserComPlugin\Cookie\CookieQueueInterface; use Sylius\Component\Core\Model\AdminUserInterface; +use Symfony\Component\HttpFoundation\Cookie; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; @@ -20,6 +22,8 @@ final class CookieManager implements CookieManagerInterface public function __construct( private readonly RequestStack $requestStack, private readonly TokenStorageInterface $tokenStorage, + private readonly CookieQueueInterface $queue, + private readonly ?string $cookieDomain = null, ) { } @@ -41,11 +45,18 @@ public function getUserComCookie(): ?string public function setUserComCookie(string $value): void { - $request = $this->requestStack->getCurrentRequest(); - if (null === $request) { - return; + $cookie = Cookie::create(self::CHAT_COOKIE_NAME) + ->withValue($value) + ->withPath('/') + ->withSecure(true) + ->withHttpOnly(true) + ->withSameSite('lax'); + + if (null !== $this->cookieDomain && '' !== $this->cookieDomain) { + $cookie = $cookie->withDomain($this->cookieDomain); } - $request->cookies->set(self::CHAT_COOKIE_NAME, $value); + + $this->queue->queue($cookie); } private function isShopUser(): bool diff --git a/src/Updater/CustomerWithKeyUpdater.php b/src/Updater/CustomerWithKeyUpdater.php index 99daa7b..f227819 100644 --- a/src/Updater/CustomerWithKeyUpdater.php +++ b/src/Updater/CustomerWithKeyUpdater.php @@ -154,7 +154,7 @@ private function updateForUserWithoutEmail( $this->userApi->mergeUsers($apiAwareResource, $customerFoundByEmail['id'], [$userFromUserKey['id']]); } - $this->sendEvent($apiAwareResource, $email, $eventName, $payload); + $this->changeCookieWithEvent($user, $apiAwareResource, $eventName); return $user; } diff --git a/tests/Application/.env b/tests/Application/.env index b5d9811..7a3f667 100644 --- a/tests/Application/.env +++ b/tests/Application/.env @@ -36,5 +36,6 @@ SYLIUS_MESSENGER_TRANSPORT_CATALOG_PROMOTION_REMOVAL_FAILED_DSN=doctrine://defau USER_COM_FRONTEND_API_KEY="" USER_COM_ENCRYPTION_KEY=your-32-character-long-key USER_COM_ENCRYPTION_IV=your-16-character-long- +USER_COM_COOKIE_DOMAIN="" MESSENGER_USER_COM_ASYNCHRONOUS_DSN="doctrine://default" ###< UserCom From b67f1ef07afd4599fab2d63c0b59c25caa719f72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Thu, 2 Oct 2025 14:58:37 +0200 Subject: [PATCH 13/18] [UC-30] Extend cookie handling with domain fallback and refactor event dispatch in CustomerWithKeyUpdater --- src/Manager/CookieManager.php | 31 +++++++++++++++++++++++++- src/Updater/CustomerWithKeyUpdater.php | 21 ++++++++++------- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/Manager/CookieManager.php b/src/Manager/CookieManager.php index d407a8c..ff4b663 100644 --- a/src/Manager/CookieManager.php +++ b/src/Manager/CookieManager.php @@ -49,11 +49,22 @@ public function setUserComCookie(string $value): void ->withValue($value) ->withPath('/') ->withSecure(true) - ->withHttpOnly(true) + ->withExpires(new \DateTimeImmutable('+1 year')) + ->withHttpOnly(false) ->withSameSite('lax'); if (null !== $this->cookieDomain && '' !== $this->cookieDomain) { $cookie = $cookie->withDomain($this->cookieDomain); + } else { + $request = $this->requestStack->getCurrentRequest(); + if (null === $request) { + return; + } + + $domain = $this->getBaseDomain($request->getHost()); + if (null !== $domain) { + $cookie = $cookie->withDomain($domain); + } } $this->queue->queue($cookie); @@ -70,4 +81,22 @@ private function isShopUser(): bool return true; } + + private function getBaseDomain(string $host): ?string + { + $host = (string) preg_replace('/:\d+$/', '', $host); + + if ($host === 'localhost' || filter_var($host, \FILTER_VALIDATE_IP) !== false) { + return null; + } + + $parts = explode('.', $host); + $count = count($parts); + + if ($count >= 2) { + return '.' . $parts[$count - 2] . '.' . $parts[$count - 1]; + } + + return null; + } } diff --git a/src/Updater/CustomerWithKeyUpdater.php b/src/Updater/CustomerWithKeyUpdater.php index f227819..6000f84 100644 --- a/src/Updater/CustomerWithKeyUpdater.php +++ b/src/Updater/CustomerWithKeyUpdater.php @@ -108,13 +108,19 @@ public function updateWithUserKey( ); $this->userApi->mergeUsers($apiAwareResource, $userByEmailFromForm['id'], [$userFoundByKey['id']]); - $this->changeCookieWithEvent($user, $apiAwareResource, $eventName, $payload); + if (is_array($user) && isset($user['email']) && is_string($user['email'])) { + $this->sendEvent($apiAwareResource, $user['email'], $eventName, $payload); + } + $this->changeCookie($user); return $user; } $user = $this->userApi->createUser($apiAwareResource, $payload); - $this->changeCookieWithEvent($user, $apiAwareResource, $eventName, $payload); + + if (is_array($user) && isset($user['email']) && is_string($user['email'])) { + $this->sendEvent($apiAwareResource, $user['email'], $eventName, $payload); + } $this->changeCookie($user); return $user; } @@ -154,16 +160,16 @@ private function updateForUserWithoutEmail( $this->userApi->mergeUsers($apiAwareResource, $customerFoundByEmail['id'], [$userFromUserKey['id']]); } - $this->changeCookieWithEvent($user, $apiAwareResource, $eventName); + if (is_array($user) && isset($user['email']) && is_string($user['email'])) { + $this->sendEvent($apiAwareResource, $user['email'], $eventName, $payload); + } + $this->changeCookie($user); return $user; } - public function changeCookieWithEvent( + public function changeCookie( ?array $user, - UserComApiAwareInterface $apiAwareResource, - string $eventName, - ?array $payload = null, ): void { if (false === is_array($user) || false === array_key_exists('id', $user) || @@ -173,6 +179,5 @@ public function changeCookieWithEvent( } $this->cookieManager->setUserComCookie($user['user_key']); - $this->sendEvent($apiAwareResource, $user['email'], $eventName, $payload); } } From 7eacc3150ebb011a3762ef5bd73c0e082a764f2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Thu, 2 Oct 2025 17:02:20 +0200 Subject: [PATCH 14/18] [UC-31] Remove unused mergeUsers call in CustomerWithKeyUpdater --- src/Updater/CustomerWithKeyUpdater.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Updater/CustomerWithKeyUpdater.php b/src/Updater/CustomerWithKeyUpdater.php index d2f58ff..8dd1f3c 100644 --- a/src/Updater/CustomerWithKeyUpdater.php +++ b/src/Updater/CustomerWithKeyUpdater.php @@ -107,7 +107,6 @@ public function updateWithUserKey( $payload, ); - $this->userApi->mergeUsers($apiAwareResource, $userByEmailFromForm['id'], [$userFoundByKey['id']]); if (is_array($user) && isset($user['email']) && is_string($user['email'])) { $this->sendEvent($apiAwareResource, $user['email'], $eventName, $payload); } From f92f81a030d64201ef6662d0c0c9165581eaf770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Fri, 3 Oct 2025 08:08:18 +0200 Subject: [PATCH 15/18] Update documentation with User.com API usage, cookie domain handling, and webhook endpoint details --- doc/adjustments.md | 5 +++++ doc/functionalities.md | 5 +++++ doc/installation.md | 5 +++++ 3 files changed, 15 insertions(+) diff --git a/doc/adjustments.md b/doc/adjustments.md index 67f24eb..eb9cee6 100644 --- a/doc/adjustments.md +++ b/doc/adjustments.md @@ -10,6 +10,11 @@ There is designated validator to check if request payload contains required fiel If there will be any field added by User.com, you can adjust this validator to check if this field is present in request payload. Also, in `BitBag\SyliusUserComPlugin\Assigner\AgreementsAssigner` you can adjust existing logic to assign agreements to customer in a way that suits your needs. +The endpoint is exposed in `Swagger` under `UserComAgreements`, making it easy to test and explore directly from the API documentation. + +>If you use several channels, remember to select one of the available channels using the get method and parameters before using the API: +`?_channel_code=CHANNEL_CODE` + ```php public function assign(CustomerInterface $customer, array $agreements): void { diff --git a/doc/functionalities.md b/doc/functionalities.md index 660c773..d089167 100644 --- a/doc/functionalities.md +++ b/doc/functionalities.md @@ -31,3 +31,8 @@ The **BitBagSyliusUserComPlugin** integrates **User.com** with Sylius-based stor ### 7. User information object - you can use `user_com_customer_info` in browser console to check currently logged in customer data + +### 8. Webhook Endpoint: Updating User Marketing Consents +- Exposes a dedicated endpoint for handling User.com webhooks (`UserComAgreements` in `Swagger`), +allowing the update of user marketing consents. By default, it manages the `subscribedToNewsletter` flag, +but the mechanism is fully extensible to support additional types of consents.” diff --git a/doc/installation.md b/doc/installation.md index bacbefe..e643607 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -8,8 +8,13 @@ USER_COM_FRONTEND_API_KEY="" USER_COM_ENCRYPTION_KEY=your-32-character-long-key USER_COM_ENCRYPTION_IV=your-16-character-long-iv + USER_COM_COOKIE_DOMAIN="" MESSENGER_USER_COM_ASYNCHRONOUS_DSN="" ``` + - You can find the `USER_COM_FRONTEND_API_KEY` in the User.Com integration guide for `Google Tag Manager (Settings->Setup & Integrations)`. + - `USER_COM_COOKIE_DOMAIN` is optional, if not set, cookies will be set for the current domain. + + 3. Add plugin dependencies to `config/bundles.php` file. Make sure that none of the bundles are duplicated. ```php return [ From ae3fed8944c563376053f961fa04dd9a1678253a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Tue, 25 Nov 2025 13:26:31 +0100 Subject: [PATCH 16/18] Update installation guide with encryption key/IV generation and async DSN details --- doc/installation.md | 9 ++++++++- ecs.php | 1 - 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/doc/installation.md b/doc/installation.md index e643607..3e7cd96 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -12,7 +12,14 @@ MESSENGER_USER_COM_ASYNCHRONOUS_DSN="" ``` - You can find the `USER_COM_FRONTEND_API_KEY` in the User.Com integration guide for `Google Tag Manager (Settings->Setup & Integrations)`. - - `USER_COM_COOKIE_DOMAIN` is optional, if not set, cookies will be set for the current domain. + - `USER_COM_ENCRYPTION_KEY` and `USER_COM_ENCRYPTION_IV` are required for cookie encryption. +- You can generate the encryption key and IV using the following command: + ```bash + php -r '$key = bin2hex(random_bytes(16)); echo "USER_COM_ENCRYPTION_KEY=\"" . $key . "\"\n"; $iv = bin2hex(random_bytes(8)); echo "USER_COM_ENCRYPTION_IV=\"" . $iv . "\"\n";' + ``` + - `MESSENGER_USER_COM_ASYNCHRONOUS_DSN` is the DSN for the messenger transport that will handle asynchronous messages. You can use different transports like `doctrine://default`, `amqp://guest:guest@localhost:5672/%2f/messages`, etc. + + - `USER_COM_COOKIE_DOMAIN` is optional, if not set, cookies will be set for the current domain. 3. Add plugin dependencies to `config/bundles.php` file. Make sure that none of the bundles are duplicated. diff --git a/ecs.php b/ecs.php index 3cf43de..fb0426f 100644 --- a/ecs.php +++ b/ecs.php @@ -18,4 +18,3 @@ VisibilityRequiredFixer::class => ['*Spec.php'], ]); }; - From 89b0c7ed6a04f801d04e88f12fa0a0f23f8a46e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Tue, 25 Nov 2025 13:30:40 +0100 Subject: [PATCH 17/18] Update build workflow and Behat configuration - Remove unused certificate installation step from build workflow. - Update base_url in Behat configuration to use HTTP instead of HTTPS. --- .github/workflows/build.yml | 6 +----- behat.yml.dist | 2 +- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6f68ab2..02bfcbd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,11 +61,7 @@ jobs: - name: Output PHP version for Symfony CLI run: php -v | head -n 1 | awk '{ print $2 }' > .php-version - - - - 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='*' http://127.0.0.1 > /dev/null 2>&1 & diff --git a/behat.yml.dist b/behat.yml.dist index ab46300..bb387af 100644 --- a/behat.yml.dist +++ b/behat.yml.dist @@ -20,7 +20,7 @@ default: Behat\MinkExtension: files_path: "%paths.base%/vendor/sylius/sylius/src/Sylius/Behat/Resources/fixtures/" - base_url: "https://127.0.0.1:8080/" + base_url: "http://127.0.0.1:8080/" default_session: symfony javascript_session: panther sessions: From b7bcc660cfc10a94bbc8e31399ec06e0a359d30f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Kali=C5=84ski?= Date: Tue, 25 Nov 2025 13:46:44 +0100 Subject: [PATCH 18/18] Add audit ignore rules to composer.json configuration --- composer.json | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/composer.json b/composer.json index b1f2c51..6666de3 100644 --- a/composer.json +++ b/composer.json @@ -53,6 +53,15 @@ "dealerdirect/phpcodesniffer-composer-installer": false, "phpstan/extension-installer": true, "symfony/flex": true + }, + "audit": { + "ignore": [ + "PKSA-gs8r-6kz6-pp56", + "PKSA-gnn4-pxdg-q76m", + "PKSA-yhcn-xrg3-68b1", + "PKSA-2wrf-1xmk-1pky", + "PKSA-4g5g-4rkv-myqs" + ] } }, "extra": {