From ca6443415970c9d3bf0bf225cf4ad3b781878d17 Mon Sep 17 00:00:00 2001 From: stromek Date: Fri, 17 Jul 2026 12:02:49 +0200 Subject: [PATCH 1/2] Add entityResolve conf option and language snippet parameter Mirrors two new capabilities in the main app: the Tiptap message editor's entity chip hover-card (dataLayer.conf.entityResolve) and the widget's UI-language override (data-lang), so the generated snippet stays in sync. --- CHANGELOG.md | 5 +++++ README.md | 9 ++++++++- src/Internal/Generator.php | 7 +++++-- src/Options/ConfOptions.php | 13 +++++++++++++ src/SnippetClient.php | 6 +++++- tests/Options/ConfOptionsTest.php | 15 +++++++++++++++ tests/SnippetClientTest.php | 12 ++++++++++++ 7 files changed, 63 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5528c24..f01033a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## [0.4.0] - 2026-08-07 +### Added +- `entityResolve` callback option on `ConfOptions` — resolves business-entity detail for the message editor's chip hover-card +- Optional `$language` parameter on `SnippetClient::snippet()` to force the widget UI language (`data-lang`) + ## [0.3.4] - 2026-08-07 ### Added - `CspPolicy` — builds the Content-Security-Policy directives the host page needs (`script-src`, `connect-src`, `style-src`, `img-src`, `frame-src`); exposes `getDirectives()`, `getHeaderName()`, `getHeaderValue()` and `getMetaTag()`. Only the CDN origin comes from the loader URL; the API and application origins are taken from the environment or from the `apiUrl` / `applicationUrl` arguments, and an unknown origin throws a `CspException` instead of producing a guessed policy that would silently block the widget diff --git a/README.md b/README.md index c3d7baf..ef55b2d 100644 --- a/README.md +++ b/README.md @@ -251,7 +251,7 @@ $client = new SnippetClient('key', 'secret', codeHasher: new MyCustomHasher()); ## Methods | Method | Description | |---|---| -| `snippet()` | Async loader script. Place once per page. | +| `snippet(?string $language = null)` | Async loader script. Place once per page. Optional UI language (`cs`, `en`, `sk`…), otherwise auto-detected from the browser. | | `conf(ConfOptions)` | SDK configuration (notification renderer, CSS, callbacks…). | | `user(UserOptions)` | Identifies the current user. | | `thread(string $selector, ThreadOptions)` | Embeds a thread into a DOM element. | @@ -285,6 +285,13 @@ See the full list of parameters in [`src/Options/ConfOptions.php`](src/Options/C | `notificationElementTargetElement` | `?string` | JS expression returning the target DOM element. | | `notificationElementPosition` | `?int` | Icon position: 1=top-left, 2=top-right, 3=bottom-right, 4=bottom-left. | | `theme` | `?string` | Light/dark mode for the app. Values: `null` (follows browser preference), `stromcom-light`, `stromcom-dark`. | +| `entityResolve` | `?string` | JS callback resolving business-entity detail (order, ticket…) for the message editor's chip hover-card. Receives `{type, id}`, returns (or resolves to) `{title, url?, fields: [{label, value}]}`. | + +### UI language +Pass a language code to `snippet()` to force the widget UI language (`cs`, `en`, `sk`…). When omitted, the widget detects it from the browser, falling back to English. +```php +echo $client->snippet('cs')->getHTML(); +``` ## Avatar helper `AvatarStyle` generates Gravatar URLs without any external dependency: ```php diff --git a/src/Internal/Generator.php b/src/Internal/Generator.php index ad05c51..2dd461c 100644 --- a/src/Internal/Generator.php +++ b/src/Internal/Generator.php @@ -31,18 +31,21 @@ public function __construct(?string $dataLayer = null, bool $withDocs = false, ? $this->nonce = $nonce; } - public function generateSnippet(string $loaderUrl, string $clientKey, string $clientSecret): SnippetCode { + public function generateSnippet(string $loaderUrl, string $clientKey, string $clientSecret, ?string $language = null): SnippetCode { try { $layer = $this->jsonEncode($this->dataLayer); $url = $this->jsonEncode($loaderUrl . '?'); $key = $this->jsonEncode($clientKey); $secret = $this->jsonEncode($clientSecret); + $lang = $language !== null ? $this->jsonEncode($language) : null; + + $langAssign = $lang !== null ? "j.dataset.lang={$lang};" : ''; return new SnippetCode(<<nonce); diff --git a/src/Options/ConfOptions.php b/src/Options/ConfOptions.php index 516ee9e..27a141a 100644 --- a/src/Options/ConfOptions.php +++ b/src/Options/ConfOptions.php @@ -48,6 +48,9 @@ class ConfOptions extends SnippetOptions { #[Docs('Theme name applied to the host page via data-theme on . Available: stromcom-default, stromcom-dark', 'stromcom-dark', true)] private ?string $theme = null; + #[Docs('Callback resolving business-entity detail for the message editor chip hover-card. Receives {type, id}, returns an object (or Promise) with {title, url?, fields: [{label, value}]}', null, true, 'Function|null')] + private ?string $entityResolve = null; + /** @param array|null $notificationElementStyles */ public function __construct( ?string $notificationRenderer = null, @@ -63,6 +66,7 @@ public function __construct( ?string $notificationElementAfterRender = null, ?string $homeBeforeRender = null, ?string $theme = null, + ?string $entityResolve = null, ) { $this->notificationRenderer = $notificationRenderer; $this->onNotification = $onNotification; @@ -77,6 +81,7 @@ public function __construct( $this->notificationElementAfterRender = $notificationElementAfterRender; $this->homeBeforeRender = $homeBeforeRender; $this->theme = $theme; + $this->entityResolve = $entityResolve; } public function getNotificationRenderer(): ?string { @@ -156,6 +161,14 @@ public function getTheme(): ?string { return $this->theme; } + public function getEntityResolve(): ?string { + return $this->entityResolve; + } + + public function renderEntityResolve(): ?JsValue { + return $this->wrapJsValue($this->entityResolve); + } + private function wrapJsValue(?string $value): ?JsValue { return $value !== null ? JsValue::createDOMContentLoaded($value) : null; } diff --git a/src/SnippetClient.php b/src/SnippetClient.php index 1483280..1541f75 100644 --- a/src/SnippetClient.php +++ b/src/SnippetClient.php @@ -59,13 +59,17 @@ public function csp(): CspPolicy { * Generates the async loader snippet that bootstraps the SDK. * Place this once on every page where you want the widget to appear. * + * @param string|null $language UI language (e.g. "cs", "en", "sk"). Null (default) lets the + * widget detect it from the browser, falling back to English. + * * @throws SnippetGenerationException */ - public function snippet(): SnippetCode { + public function snippet(?string $language = null): SnippetCode { return $this->generator->generateSnippet( $this->environment->getLoaderUrl(), $this->clientKey, $this->clientSecret, + $language, ); } diff --git a/tests/Options/ConfOptionsTest.php b/tests/Options/ConfOptionsTest.php index 87d682c..ed9f7fc 100644 --- a/tests/Options/ConfOptionsTest.php +++ b/tests/Options/ConfOptionsTest.php @@ -89,6 +89,21 @@ public function with_docs_excludes_fields_with_show_null_in_docs_false(): void { $this->assertArrayNotHasKey('notificationElementShowAlways', $result); } + #[Test] + public function entity_resolve_string_is_wrapped_in_js_value(): void { + $options = new ConfOptions(entityResolve: 'async ({type, id}) => ({title: `${type} #${id}`})'); + $result = $options->getOptions(); + + $this->assertArrayHasKey('entityResolve', $result); + $this->assertInstanceOf(JsValue::class, $result['entityResolve']); + } + + #[Test] + public function entity_resolve_null_renders_as_null(): void { + $options = new ConfOptions(); + $this->assertNull($options->renderEntityResolve()); + } + #[Test] public function get_options_with_docs_returns_all_property_schemas(): void { $schema = ConfOptions::getOptionsWithDocs(); diff --git a/tests/SnippetClientTest.php b/tests/SnippetClientTest.php index e8af390..d5b1785 100644 --- a/tests/SnippetClientTest.php +++ b/tests/SnippetClientTest.php @@ -55,6 +55,18 @@ public function snippet_uses_custom_environment_url(): void { $this->assertStringContainsString('localhost:9000', $client->snippet()->getCode()); } + #[Test] + public function snippet_without_language_omits_lang_dataset(): void { + $code = $this->client->snippet()->getCode(); + $this->assertStringNotContainsString('dataset.lang', $code); + } + + #[Test] + public function snippet_with_language_sets_lang_dataset(): void { + $code = $this->client->snippet('cs')->getCode(); + $this->assertStringContainsString('j.dataset.lang="cs"', $code); + } + #[Test] public function snippet_html_is_wrapped_in_script_tags(): void { $html = $this->client->snippet()->getHTML(); From 91571ed95bc71edca7631a21c0b51066c0137f32 Mon Sep 17 00:00:00 2001 From: stromek Date: Fri, 17 Jul 2026 12:07:34 +0200 Subject: [PATCH 2/2] Backfill CHANGELOG entries for 0.3.2 and 0.3.3 Both tags were cut without a matching CHANGELOG entry (two SnippetOptions default-value bugfixes). Add them retroactively so 0.4.0 doesn't sit directly above 0.3.1 with an undocumented gap in between. --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f01033a..eb29d56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,14 @@ - `CspException` for an invalid nonce or an origin that is neither known nor given - CSP section in the README and a runnable `examples/csp.php` +## [0.3.3] - 2026-06-09 +### Fixed +- Default value handling in `SnippetOptions` for properties without a declared default (follow-up to 0.3.2 — the previous fix still relied on `null` in one more spot) + +## [0.3.2] - 2026-06-08 +### Fixed +- Default value resolution in `SnippetOptions` for properties without a default — `getDefaultValue()` was called unguarded, relying on `null` for properties that have no declared default instead of checking `hasDefaultValue()` first + ## [0.3.1] - 2026-04-17 ### Changed - Add theme support to ConfOptions