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:
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": {
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/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/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%
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 4ff1607..d089167 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**.
@@ -32,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..3e7cd96 100644
--- a/doc/installation.md
+++ b/doc/installation.md
@@ -8,8 +8,20 @@
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_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.
```php
return [
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'],
]);
};
-
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(
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(),
diff --git a/src/Cookie/CookieQueue.php b/src/Cookie/CookieQueue.php
new file mode 100644
index 0000000..283ff86
--- /dev/null
+++ b/src/Cookie/CookieQueue.php
@@ -0,0 +1,33 @@
+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/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',
];
diff --git a/src/Manager/CookieManager.php b/src/Manager/CookieManager.php
index d1623c9..ff4b663 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,29 @@ 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)
+ ->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);
+ }
}
- $request->cookies->set(self::CHAT_COOKIE_NAME, $value);
+
+ $this->queue->queue($cookie);
}
private function isShopUser(): bool
@@ -59,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/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',
];
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;
+ }
+}
diff --git a/src/Updater/CustomerWithKeyUpdater.php b/src/Updater/CustomerWithKeyUpdater.php
index 90ede7a..8dd1f3c 100644
--- a/src/Updater/CustomerWithKeyUpdater.php
+++ b/src/Updater/CustomerWithKeyUpdater.php
@@ -78,11 +78,18 @@ public function updateWithUserKey(
null !== $customer->getEmail() &&
$userFoundByKey['email'] === strtolower($customer->getEmail())
) {
- return $this->userApi->updateUser(
+ $user = $this->userApi->updateUser(
$apiAwareResource,
$userFoundByKey['id'],
$payload,
);
+
+ 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;
}
$userByEmailFromForm = $this->userApi->findUser(
@@ -100,15 +107,20 @@ public function updateWithUserKey(
$payload,
);
- $this->userApi->mergeUsers($apiAwareResource, $userByEmailFromForm['id'], [$userFoundByKey['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;
}
$user = $this->userApi->createUser($apiAwareResource, $payload);
- $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;
}
@@ -148,15 +160,16 @@ private function updateForUserWithoutEmail(
$this->userApi->mergeUsers($apiAwareResource, $customerFoundByEmail['id'], [$userFromUserKey['id']]);
}
- $this->sendEvent($apiAwareResource, $email, $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;
}
- public function changeCookieWithEvent(
+ public function changeCookie(
?array $user,
- UserComApiAwareInterface $apiAwareResource,
- string $eventName,
): void {
if (false === is_array($user) ||
false === array_key_exists('id', $user) ||
@@ -166,6 +179,5 @@ public function changeCookieWithEvent(
}
$this->cookieManager->setUserComCookie($user['user_key']);
- $this->sendEvent($apiAwareResource, $user['email'], $eventName);
}
}
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 %}
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