@eturnity/eturnity_reusable_components 9.19.1 → 9.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -115,3 +115,100 @@ https://docs.npmjs.com/about-semantic-versioning
115
115
 
116
116
  npm publish
117
117
  ```
118
+
119
+ ## Provided assets release flow (per-asset version tags)
120
+
121
+ Customers load provided files from jsDelivr using stable tag-based URLs:
122
+
123
+ ```html
124
+ <script src="https://cdn.jsdelivr.net/npm/@eturnity/eturnity_reusable_components@latest-consent-bridge/dist/eturnity-consent-bridge.min.js"></script>
125
+ ```
126
+
127
+ Consent bridge integration documentation is maintained in `docs/eturnity-consent-bridge.md` and published to:
128
+
129
+ ```text
130
+ https://cdn.jsdelivr.net/npm/@eturnity/eturnity_reusable_components@latest-consent-bridge/dist/eturnity-consent-bridge.md
131
+ ```
132
+
133
+ Provided files and docs are configured in `provided-assets.config.js`. Script and doc entries can both define their own `versionTag` (for example `latest-consent-bridge`). Because customers keep these URLs unchanged, we update what they receive by moving those npm dist-tags to a published package version.
134
+
135
+ ### Build and publish behavior
136
+
137
+ - `npm run build` runs `vite build` (library), `npm run build:provided-files` (JS CDN bundles), and `npm run copy:provided-docs` (published docs).
138
+ - `npm run build:provided-files` builds files listed in `provided-assets.config.js` into `dist/*.min.js`.
139
+ - `npm run copy:provided-docs` copies docs listed in `provided-assets.config.js` into `dist/*.md`.
140
+ - `npm run verify:provided-files` checks expected provided files exist in `dist/` and are non-empty.
141
+ - `npm run verify:provided-docs` checks expected provided docs exist in `dist/` and are non-empty.
142
+ - `prepack` and `prepublishOnly` run both verification steps to reduce broken publish risk.
143
+
144
+ ### Standard release steps
145
+
146
+ 1. Publish a new immutable npm version:
147
+
148
+ ```bash
149
+ npm publish
150
+ ```
151
+
152
+ 2. Optional: promote provided-asset tag(s) so customer script/doc URLs point to that version.
153
+
154
+ Use this only if you want to update what customers receive from stable CDN tag URLs:
155
+
156
+ Script options (`--dry-run`, `--version=…`, `--name=…`, and combinations)
157
+
158
+ ```bash
159
+ # Preview planned commands only (no npm dist-tag, no registry wait, no cache purge, no CDN verification).
160
+ npm run promote:provided-files-tag -- --dry-run
161
+ ```
162
+
163
+ ```bash
164
+ # Promote all configured promotable assets to package.json version.
165
+ npm run promote:provided-files-tag
166
+ ```
167
+
168
+ ```bash
169
+ # Promote all configured promotable assets to a specific published version.
170
+ npm run promote:provided-files-tag -- --version=9.16.0
171
+ ```
172
+
173
+ ```bash
174
+ # Promote one specific promotable asset by name (uses package.json version).
175
+ npm run promote:provided-files-tag -- --name=eturnity-consent-bridge
176
+ ```
177
+
178
+ ```bash
179
+ # Promote one specific promotable asset by name to a specific version.
180
+ npm run promote:provided-files-tag -- --name=eturnity-consent-bridge --version=9.16.0
181
+ ```
182
+
183
+ When promotion is used, the script:
184
+
185
+ 1. updates npm dist-tag(s) for selected promotable asset(s)
186
+ 2. waits until npm registry serves updated dist-tag value(s)
187
+ 3. purges jsDelivr cache for selected promotable asset(s)
188
+ 4. verifies CDN serves promoted version bytes
189
+
190
+ Optional tuning via environment variables:
191
+
192
+ - `PROMOTE_TAG_REGISTRY_RETRIES` (default `24`)
193
+ - `PROMOTE_TAG_REGISTRY_INTERVAL_MS` (default `5000`)
194
+ - `PROMOTE_TAG_VERIFY_RETRIES` (default `12`)
195
+ - `PROMOTE_TAG_VERIFY_INTERVAL_MS` (default `5000`)
196
+
197
+ ### Optional rollback (re-point customer URL to older stable package)
198
+
199
+ If a promoted version is broken, you can optionally promote a previously published version:
200
+
201
+ ```bash
202
+ npm run promote:provided-files-tag -- --version=<older-published-version>
203
+ ```
204
+
205
+ ```bash
206
+ # Roll back only one promotable asset tag.
207
+ npm run promote:provided-files-tag -- --name=eturnity-consent-bridge --version=<older-published-version>
208
+ ```
209
+
210
+ Example:
211
+
212
+ ```bash
213
+ npm run promote:provided-files-tag -- --version=9.15.2
214
+ ```
@@ -0,0 +1,207 @@
1
+ # eturnityConsentBridge
2
+
3
+ Lightweight browser bridge for synchronizing consent-related cookie payloads between a host page and an embedded Eturnity iframe via `postMessage`.
4
+
5
+ ## Overview
6
+
7
+ `eturnityConsentBridge` is a public browser/CDN script that exposes a global function:
8
+
9
+ - `window.eturnityConsentBridge(...)`
10
+ - `eturnityConsentBridge(...)`
11
+
12
+ It is designed for third-party embedding on customer websites and supports multiple bridge instances (multiple iframes) on the same page.
13
+
14
+ ## Features
15
+
16
+ - Global API for plain HTML and JavaScript-based script injection flows
17
+ - Safe message routing per iframe instance using `event.source` and origin checks
18
+ - Idempotent initialization for repeated calls with the same iframe ID
19
+ - Non-throwing input validation (returns `null` on invalid init input)
20
+ - Cookie fallback persistence and replay on iframe sync-ready handshake
21
+ - Optional debug logging with `logs: true`
22
+ - Lifecycle helpers: per-instance and global cleanup
23
+
24
+ ## Installation
25
+
26
+ ### 1) CDN script tag
27
+
28
+ ```html
29
+ <script src="https://ABSOLUTE_CDN_LINK/eturnity-consent-bridge.min.js"></script>
30
+ ```
31
+
32
+ ### 2) Dynamic script injection
33
+
34
+ ```js
35
+ const script = document.createElement('script')
36
+ script.src = 'https://ABSOLUTE_CDN_LINK/eturnity-consent-bridge.min.js'
37
+ script.async = true
38
+ script.addEventListener('load', () => {
39
+ window.eturnityConsentBridge('my-iframe-id', { type: 'solar_calculator' })
40
+ }, { once: true })
41
+ document.body.appendChild(script)
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ After the script is loaded, initialize per iframe:
47
+
48
+ ```js
49
+ eturnityConsentBridge('my-iframe-id', {
50
+ type: 'solar_calculator',
51
+ logs: false,
52
+ })
53
+ ```
54
+
55
+ Initialization returns:
56
+
57
+ - instance object on success
58
+ - `null` when validation fails
59
+
60
+ The iframe can be missing at init time; the bridge will keep trying to resolve it during message handling.
61
+
62
+ ## Message Contract
63
+
64
+ All message names are namespaced by selected `type`:
65
+
66
+ - `eturnity_<type>_cookie_sync_ready` (iframe -> host)
67
+ - `eturnity_<type>_cookie_fallback` (iframe -> host)
68
+ - `eturnity_<type>_cookie_delete` (iframe -> host)
69
+ - `eturnity_<type>_cookie_sync` (host -> iframe)
70
+
71
+ ### Payloads
72
+
73
+ - `cookie_fallback` payload:
74
+ - `{ cookieName: string, cookieValue: string }`
75
+ - `cookie_delete` payload:
76
+ - `{ cookieName: string }`
77
+ - `cookie_sync` payload for set/update replay:
78
+ - `{ cookieName: string, cookieValue: string }`
79
+ - `cookie_sync` payload for delete replay:
80
+ - `{ cookieName: string, isDeleted: true }`
81
+
82
+ `isDeleted: true` is the delete marker for iframe listeners.
83
+
84
+ ### Replay lifecycle (important)
85
+
86
+ Stored cookie replay can be triggered in two safe ways:
87
+
88
+ - **on init**: right after bridge initialization (`eturnityConsentBridge(...)`)
89
+ - **on handshake**: when iframe sends `eturnity_<type>_cookie_sync_ready`
90
+
91
+ The init replay exists to avoid reload race conditions where the iframe handshake might be sent before the host bridge is fully attached.
92
+
93
+ Delete events are also synchronized: when iframe sends `eturnity_<type>_cookie_delete`, host removes that entry from its cache and then emits `eturnity_<type>_cookie_sync` with `{ cookieName, isDeleted: true }`.
94
+
95
+ ## API
96
+
97
+ ### `eturnityConsentBridge(iframeId, options)`
98
+
99
+ Creates or updates a bridge instance for a target iframe.
100
+
101
+ - **`iframeId`**: `string` (required)
102
+ - Accepts values with or without leading `#`
103
+ - **`options`**: `object` (required)
104
+ - See [Options](#options)
105
+
106
+ **Returns**
107
+
108
+ - `BridgeInstance` on success
109
+ - `null` on invalid input
110
+
111
+ ### `eturnityConsentBridge.destroy(iframeId)`
112
+
113
+ Destroys one bridge instance.
114
+
115
+ - Returns `true` if an instance was removed, otherwise `false`.
116
+
117
+ ### `eturnityConsentBridge.destroyAll()`
118
+
119
+ Destroys all bridge instances and removes the shared `message` listener when no instances remain.
120
+
121
+ ### `BridgeInstance.destroy()`
122
+
123
+ Returned instance includes `destroy()` to remove itself.
124
+
125
+ ## Options
126
+
127
+ ```ts
128
+ type BridgeOptions = {
129
+ type: 'solar_calculator' | 'heating_calculator' | 'e_mobility_configurator'
130
+ logs?: boolean
131
+ }
132
+ ```
133
+
134
+ - **`type`** (required): selects internal message/cookie namespace
135
+ - **`logs`** (optional, default `false`): enables verbose console logging for this instance
136
+
137
+ Unknown option keys are ignored.
138
+
139
+ ## Examples
140
+
141
+ ### Basic
142
+
143
+ ```html
144
+ <iframe id="eturnity-solar" src="https://example-iframe-host/path"></iframe>
145
+ <script src="https://ABSOLUTE_CDN_LINK/eturnity-consent-bridge.min.js"></script>
146
+ <script>
147
+ eturnityConsentBridge('eturnity-solar', { type: 'solar_calculator' })
148
+ </script>
149
+ ```
150
+
151
+ ### Multiple iframes on one page
152
+
153
+ ```js
154
+ eturnityConsentBridge('solar-iframe', { type: 'solar_calculator' })
155
+ eturnityConsentBridge('heating-iframe', { type: 'heating_calculator', logs: true })
156
+ ```
157
+
158
+ ### Re-initialize same iframe (idempotent update)
159
+
160
+ ```js
161
+ eturnityConsentBridge('solar-iframe', { type: 'solar_calculator', logs: false })
162
+ eturnityConsentBridge('solar-iframe', { type: 'solar_calculator', logs: true }) // updates existing instance
163
+ ```
164
+
165
+ ### Cleanup
166
+
167
+ ```js
168
+ const instance = eturnityConsentBridge('solar-iframe', { type: 'solar_calculator' })
169
+ instance && instance.destroy()
170
+
171
+ eturnityConsentBridge.destroy('heating-iframe')
172
+ eturnityConsentBridge.destroyAll()
173
+ ```
174
+
175
+ ## Troubleshooting
176
+
177
+ ### `Missing iframe id` or `Invalid options.type`
178
+
179
+ - Ensure `iframeId` is a non-empty string.
180
+ - Ensure `options.type` is one of:
181
+ - `solar_calculator`
182
+ - `heating_calculator`
183
+ - `e_mobility_configurator`
184
+
185
+ ### `Iframe ... was not found during initialization`
186
+
187
+ - Verify iframe ID matches exactly.
188
+ - If iframe is rendered later, you can initialize early; bridge resolves context again during runtime.
189
+
190
+ ### `Could not resolve iframe origin`
191
+
192
+ - Ensure iframe has a valid `src` URL.
193
+
194
+ ### `Ignoring ... due to origin mismatch`
195
+
196
+ - Host and iframe messaging origins must match the iframe `src` origin resolved by the bridge.
197
+
198
+ ### Why replay may happen before `cookie_sync_ready`
199
+
200
+ - This is expected behavior: replay on init is intentional and does not depend on handshake timing.
201
+ - In some page-load orders, the iframe can emit `cookie_sync_ready` before the host bridge listener is attached; in that case you may not see a handshake log.
202
+ - This does not break integration: the iframe consumes `eturnity_<type>_cookie_sync` messages directly, and `cookie_sync_ready` is only a replay signal from iframe to host.
203
+
204
+ ### Cookie warning on non-HTTPS host
205
+
206
+ - The bridge writes cookies with `Secure`; on non-HTTPS pages browsers may reject persistence.
207
+ - Use HTTPS in production embedding environments.
@@ -0,0 +1 @@
1
+ (function(){"use strict";(()=>{const u="eturnityConsentBridge",k="__eturnityConsentBridgeState__",C=["solar_calculator","heating_calculator","e_mobility_configurator"];if(typeof window>"u"||typeof document>"u")return;const i=globalThis[k]||{instancesByIframeId:new Map,messageListenerAttached:!1,api:null};if(globalThis[k]=i,typeof i.api=="function"){window[u]=i.api,globalThis[u]=i.api;return}const $=e=>({consentCookieName:`eturnity_${e}_consent`,cookieSyncReadyMessage:`eturnity_${e}_cookie_sync_ready`,cookieFallbackMessage:`eturnity_${e}_cookie_fallback`,cookieDeleteMessage:`eturnity_${e}_cookie_delete`,cookieSyncMessage:`eturnity_${e}_cookie_sync`}),d=e=>typeof e!="string"?"":e.trim().replace(/^#/,""),l=e=>Object.prototype.toString.call(e)==="[object Object]",O=e=>!l(e)||typeof e.type!="string"?"":e.type,a=(e,o,r)=>{if(!e||!e.logs)return;const t=`[eturnityConsentBridge:${e.iframeId}]`;if(typeof r>"u"){console.log(`${t} ${o}`);return}console.log(`${t} ${o}`,r)},h=()=>{let e="path=/; SameSite=Lax";return window.location.protocol==="https:"&&(e+="; Secure"),e},I=(e,o,r)=>{try{const t=new Date;return t.setMonth(t.getMonth()+r),document.cookie=`${e}=${encodeURIComponent(o)}; expires=${t.toUTCString()}; ${h()}`,!0}catch(t){return console.warn(`[eturnityConsentBridge] Failed to write cookie "${e}".`,t),!1}},M=e=>{try{return document.cookie=`${e}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; ${h()}`,!0}catch(o){return console.warn(`[eturnityConsentBridge] Failed to delete cookie "${e}".`,o),!1}},N=e=>{try{const o=`${e}=`,r=document.cookie.split(";");for(let t=0;t<r.length;t+=1){const s=r[t].trim();if(s.indexOf(o)===0)return decodeURIComponent(s.substring(o.length))}return null}catch(o){return console.warn(`[eturnityConsentBridge] Failed to read cookie "${e}".`,o),null}},g=e=>{const o=N(e);if(!o)return{};try{const r=JSON.parse(o);return!r||typeof r!="object"?{}:r}catch{return console.warn(`[eturnityConsentBridge] Invalid JSON in "${e}" cookie. Ignoring stored value.`),{}}},v=(e,o,r)=>{const t=g(e.config.consentCookieName);t[o]={cookieValue:r,updatedAt:new Date().toISOString()};let s="";try{s=JSON.stringify(t)}catch(c){return console.warn(`[eturnityConsentBridge] Failed to serialize cookie cache for iframe "${e.iframeId}".`,c),!1}s.length>3500&&console.warn(`[eturnityConsentBridge] Cookie cache for iframe "${e.iframeId}" is approaching cookie size limits and may be truncated by browsers.`);const n=I(e.config.consentCookieName,s,6);return n&&a(e,`Stored cookie fallback "${o}" in host cookie cache.`),n},T=(e,o)=>{const r=g(e.config.consentCookieName);if(!Object.prototype.hasOwnProperty.call(r,o))return!0;if(delete r[o],Object.keys(r).length===0){const n=M(e.config.consentCookieName);return n&&a(e,`Removed "${o}" from host cookie cache and cleared empty cache cookie.`),n}let t="";try{t=JSON.stringify(r)}catch(n){return console.warn(`[eturnityConsentBridge] Failed to serialize cookie cache for iframe "${e.iframeId}" after deleting "${o}".`,n),!1}const s=I(e.config.consentCookieName,t,6);return s&&a(e,`Removed cookie fallback "${o}" from host cookie cache.`),s},x=e=>!l(e)||typeof e.cookieName!="string"||typeof e.cookieValue!="string"?null:{cookieName:e.cookieName,cookieValue:e.cookieValue},E=e=>!l(e)||typeof e.cookieName!="string"?null:{cookieName:e.cookieName},w=e=>{const o=document.getElementById(e);if(!o||o.tagName!=="IFRAME")return null;const r=o.getAttribute("src")||"";if(!r)return{iframe:o,origin:null};try{return{iframe:o,origin:new URL(r,window.location.origin).origin}}catch{return{iframe:o,origin:null}}},y=e=>{const o=w(e.iframeId);if(!o){e.iframe=null,e.origin=null;return}e.iframe=o.iframe,e.origin=o.origin},m=(e,o)=>{if(y(e),!e.iframe||!e.iframe.contentWindow||!e.origin){console.warn(`[eturnityConsentBridge] Cannot post "${e.config.cookieSyncMessage}" for iframe "${e.iframeId}" because iframe window or origin is not ready.`);return}e.iframe.contentWindow.postMessage({type:e.config.cookieSyncMessage,payload:o},e.origin),a(e,`Posted "${e.config.cookieSyncMessage}" to iframe origin "${e.origin}".`,o)},p=(e,o)=>{const r=o==="sync_ready"?"sync_ready":"init",t=g(e.config.consentCookieName),s=Object.keys(t);if(!s.length){a(e,`No stored cookies to replay (${r}).`);return}a(e,`Replaying ${s.length} stored cookie(s) to iframe (${r}).`),s.forEach(n=>{const c=t[n];!c||typeof c.cookieValue!="string"||m(e,{cookieName:n,cookieValue:c.cookieValue})})},z=e=>{const o=Array.from(i.instancesByIframeId.values());for(let r=0;r<o.length;r+=1){const t=o[r];if(y(t),t.iframe&&t.iframe.contentWindow&&t.iframe.contentWindow===e)return t}return null},_=e=>{if(!e||!l(e.data))return;const o=O(e.data),r=z(e.source);if(!r){o.indexOf("eturnity_")===0&&console.warn(`[eturnityConsentBridge] Received "${o}" but could not match event.source to a registered iframe instance.`);return}if(!r.origin){o.indexOf("eturnity_")===0&&console.warn(`[eturnityConsentBridge] Ignoring "${o}" for iframe "${r.iframeId}" because iframe origin could not be resolved from src.`);return}if(e.origin!==r.origin){console.warn(`[eturnityConsentBridge] Ignoring "${o}" due to origin mismatch for iframe "${r.iframeId}". Expected "${r.origin}", got "${e.origin}".`);return}if(o){if(o===r.config.cookieSyncReadyMessage){a(r,`Iframe reported "${r.config.cookieSyncReadyMessage}".`),p(r,"sync_ready");return}if(o===r.config.cookieFallbackMessage){a(r,`Received message "${o}" from origin "${e.origin}".`);const t=x(e.data.payload);if(!t){console.warn(`[eturnityConsentBridge] Invalid payload for "${o}" from iframe "${r.iframeId}".`);return}v(r,t.cookieName,t.cookieValue),m(r,t);return}if(o===r.config.cookieDeleteMessage){a(r,`Received message "${o}" from origin "${e.origin}".`);const t=E(e.data.payload);if(!t){console.warn(`[eturnityConsentBridge] Invalid payload for "${o}" from iframe "${r.iframeId}".`);return}T(r,t.cookieName),m(r,{cookieName:t.cookieName,isDeleted:!0});return}o.indexOf("eturnity_")===0&&console.warn(`[eturnityConsentBridge] Ignoring unsupported message "${o}" for iframe "${r.iframeId}".`)}},A=()=>{i.messageListenerAttached||(window.addEventListener("message",_),i.messageListenerAttached=!0)},B=()=>{!i.messageListenerAttached||i.instancesByIframeId.size>0||(window.removeEventListener("message",_),i.messageListenerAttached=!1)},S=e=>{const o=d(e);if(!o)return!1;const r=i.instancesByIframeId.delete(o);return B(),r},R=e=>l(e)?typeof e.logs<"u"&&typeof e.logs!="boolean"?{valid:!1,reason:"[eturnityConsentBridge] Invalid options.logs value. Expected boolean when provided."}:typeof e.type!="string"||!C.includes(e.type)?{valid:!1,reason:`[eturnityConsentBridge] Invalid options.type. Supported values: ${C.join(", ")}.`}:{valid:!0,value:{type:e.type,logs:e.logs===!0}}:{valid:!1,reason:'[eturnityConsentBridge] Missing or invalid options object. Use eturnityConsentBridge("iframe-id", { type: "solar_calculator | heating_calculator | e_mobility_configurator", logs: false }).'},L=(e,o)=>{const r=i.instancesByIframeId.get(e);if(r){const b=r.type!==o.type;return r.logs=o.logs,b&&(r.type=o.type,r.config=$(o.type)),y(r),a(r,b?`Bridge updated to type "${o.type}".`:"Bridge re-initialized with existing configuration."),p(r,"init"),r}const t=o.type,s=$(t),n=w(e),c={iframeId:e,iframe:n?n.iframe:null,origin:n?n.origin:null,type:t,config:s,logs:o.logs,destroy:()=>S(e)};return n?n.origin||console.warn(`[eturnityConsentBridge] Could not resolve iframe origin for "${e}". Please make sure iframe src is set to a valid URL.`):console.warn(`[eturnityConsentBridge] Iframe with id "${e}" was not found during initialization. The bridge will start working once the iframe is available.`),i.instancesByIframeId.set(e,c),A(),a(c,`Bridge initialized with type "${t}".`),p(c,"init"),c},f=(e,o)=>{const r=d(e);if(!r)return console.warn('[eturnityConsentBridge] Missing iframe id. Use eturnityConsentBridge("iframe-id", { type: "solar_calculator | heating_calculator | e_mobility_configurator" }).'),null;const t=R(o);return t.valid?L(r,t.value):(console.warn(t.reason),null)};f.destroy=e=>{const o=d(e);return o?S(o):(console.warn('[eturnityConsentBridge] Missing iframe id for destroy(). Use eturnityConsentBridge.destroy("iframe-id").'),!1)},f.destroyAll=()=>{i.instancesByIframeId.clear(),B()},i.api=f,window[u]=f,globalThis[u]=f})()})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eturnity/eturnity_reusable_components",
3
- "version": "9.19.1",
3
+ "version": "9.19.2",
4
4
  "files": [
5
5
  "dist",
6
6
  "src"
@@ -10,7 +10,13 @@
10
10
  "private": false,
11
11
  "scripts": {
12
12
  "dev": "vite",
13
- "build": "vite build",
13
+ "build": "vite build && npm run build:provided-files && npm run copy:provided-docs",
14
+ "build:provided-files": "node scripts/build-provided-files.js",
15
+ "verify:provided-files": "node scripts/verify-provided-files.js",
16
+ "copy:provided-docs": "node scripts/copy-provided-docs.js",
17
+ "verify:provided-docs": "node scripts/verify-provided-docs.js",
18
+ "purge:provided-files-cache": "node scripts/purge-provided-files-cache.js",
19
+ "promote:provided-files-tag": "node scripts/promote-provided-files-tag.js",
14
20
  "lint": "vue-cli-service lint",
15
21
  "storybook": "storybook dev -p 6006",
16
22
  "build-storybook": "storybook build",
@@ -18,7 +24,8 @@
18
24
  "test": "jest",
19
25
  "test-coverage": "jest --coverage",
20
26
  "merge-remote-master": "node scripts/merge-remote-master.js",
21
- "prepublishOnly": "npm run test && npm run build"
27
+ "prepack": "npm run verify:provided-files && npm run verify:provided-docs",
28
+ "prepublishOnly": "npm run test && npm run build && npm run verify:provided-files && npm run verify:provided-docs"
22
29
  },
23
30
  "peerDependencies": {
24
31
  "vue": "^3.0.0",
@@ -40,7 +40,7 @@
40
40
  import IconComponent from '../../icon'
41
41
  import ToggleComponent from '../../inputs/toggle'
42
42
  import MainButton from '../../buttons/mainButton'
43
- import { saveCookieConsent } from '../../../helpers/cookieHelper'
43
+ import { getCookieValue, saveCookieConsent } from '../../../helpers/cookieHelper'
44
44
 
45
45
  const ModalContainer = styled.div`
46
46
  box-sizing: border-box;
@@ -155,7 +155,7 @@
155
155
  },
156
156
  methods: {
157
157
  loadCookieState() {
158
- const cookieValue = this.getCookieValue('cookieConsent')
158
+ const cookieValue = getCookieValue('cookieConsent')
159
159
  if (cookieValue) {
160
160
  try {
161
161
  const cookieData = JSON.parse(cookieValue)
@@ -186,18 +186,6 @@
186
186
  })
187
187
  return filtered
188
188
  },
189
- getCookieValue(cookieName) {
190
- const name = cookieName + '='
191
- const decodedCookie = decodeURIComponent(document.cookie)
192
- const cookieArray = decodedCookie.split(';')
193
- for (let i = 0; i < cookieArray.length; i++) {
194
- let cookie = cookieArray[i].trim()
195
- if (cookie.indexOf(name) === 0) {
196
- return cookie.substring(name.length, cookie.length)
197
- }
198
- }
199
- return null
200
- },
201
189
  isToggleChecked(choice) {
202
190
  // Check if the option is required - if so, always return true
203
191
  const option = this.toggleOptions.find((opt) => opt.choice === choice)
@@ -220,7 +208,7 @@
220
208
  return
221
209
  }
222
210
 
223
- const cookieValue = this.getCookieValue('cookieConsent')
211
+ const cookieValue = getCookieValue('cookieConsent')
224
212
 
225
213
  if (cookieValue) {
226
214
  try {
@@ -1,29 +1,216 @@
1
- export function saveCookieConsent(categories) {
2
- // Create cookie data object with categories, timestamp, and consent_given
3
- const cookieData = {
4
- categories,
5
- timestamp: new Date().toISOString(),
6
- consent_given: true
1
+ export function isRunningInIframe() {
2
+ return typeof window !== 'undefined' && window.self !== window.top
3
+ }
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Iframe cookie sync (parent page ↔ iframe)
7
+ //
8
+ // In an iframe, cookies may not be readable right after normal document.cookie = STRING
9
+ // (ITP / third-party restrictions). We mirror the intended value in
10
+ // syncedCookieFallback so getCookieValue() returns it immediately;
11
+ // the host receives `eturnity_<consentBridgeAppType>_cookie_fallback` via postMessage
12
+ // to persist and echo `eturnity_<consentBridgeAppType>_cookie_sync`.
13
+ // On load, the parent's replay also fills syncedCookieFallback via that message.
14
+ // ---------------------------------------------------------------------------
15
+
16
+ const syncedCookieFallback = {}
17
+ let cookieSyncListenerAttachedToParent = false
18
+ let consentBridgeAppType = null
19
+ let parentCookieSyncOrigin = null
20
+
21
+ function hasKnownParentCookieSyncOrigin() {
22
+ const hasKnownOrigin = typeof parentCookieSyncOrigin === 'string' && parentCookieSyncOrigin.trim() !== ''
23
+ if (!hasKnownOrigin) {
24
+ console.warn('[cookieHelper] Cannot sync cookie to parent: origin unknown. Skipping.')
25
+ }
26
+ return hasKnownOrigin
27
+ }
28
+
29
+ function readSyncedCookieFallback(cookieName) {
30
+ if (!cookieName || typeof cookieName !== 'string') return null
31
+ return Object.prototype.hasOwnProperty.call(syncedCookieFallback, cookieName)
32
+ ? syncedCookieFallback[cookieName]
33
+ : null
34
+ }
35
+
36
+ function clearSyncedCookieFallback(cookieName) {
37
+ if (!cookieName || typeof cookieName !== 'string') return
38
+ if (Object.prototype.hasOwnProperty.call(syncedCookieFallback, cookieName)) {
39
+ delete syncedCookieFallback[cookieName]
7
40
  }
41
+ }
42
+
43
+ function saveSyncedCookieFallback(cookieName, cookieValue) {
44
+ if (!cookieName || typeof cookieName !== 'string') return
45
+ syncedCookieFallback[cookieName] = cookieValue
46
+ }
47
+
48
+ function getParentOriginFromReferrer() {
49
+ if (typeof document === 'undefined' || !document.referrer) return null
50
+ try {
51
+ return new URL(document.referrer).origin
52
+ } catch (_) {
53
+ return null
54
+ }
55
+ }
56
+
57
+ function getCookieAttributes() {
58
+ const sameSite = isRunningInIframe() ? 'None' : 'Lax'
59
+ let attributes = `path=/; sameSite=${sameSite}`
60
+ if (window.location.protocol === 'https:') {
61
+ attributes += '; Secure'
62
+ }
63
+ return attributes
64
+ }
65
+
66
+ export function setupParentCookieSyncListener(options = {}) {
67
+ if (typeof window === 'undefined' || !isRunningInIframe()) return
68
+
69
+ consentBridgeAppType = typeof options?.consentBridgeAppType === 'string' ? options.consentBridgeAppType : null
70
+ const onCookieSync = typeof options?.onCookieSync === 'function' ? options.onCookieSync : null
71
+ parentCookieSyncOrigin =
72
+ typeof options?.parentOrigin === 'string' && options.parentOrigin.trim()
73
+ ? options.parentOrigin.trim()
74
+ : getParentOriginFromReferrer()
75
+
76
+ if (consentBridgeAppType && !cookieSyncListenerAttachedToParent) {
77
+ window.addEventListener('message', (event) => {
78
+ if (
79
+ event.source !== window.parent ||
80
+ !hasKnownParentCookieSyncOrigin() ||
81
+ event.origin !== parentCookieSyncOrigin ||
82
+ !event.data ||
83
+ event.data.type !== `eturnity_${consentBridgeAppType}_cookie_sync`
84
+ ) {
85
+ return
86
+ }
87
+
88
+ const payload = event.data.payload || {}
89
+ if (!payload.cookieName || typeof payload.cookieName !== 'string') return
90
+
91
+ if (payload.isDeleted === true) {
92
+ clearSyncedCookieFallback(payload.cookieName)
93
+ } else if (typeof payload.cookieValue === 'string') {
94
+ saveSyncedCookieFallback(payload.cookieName, payload.cookieValue)
95
+ }
96
+
97
+ if (typeof onCookieSync === 'function') {
98
+ onCookieSync(payload)
99
+ }
100
+ })
101
+ cookieSyncListenerAttachedToParent = true
102
+ }
103
+
104
+ // Tell the parent it can start sending sync messages (optional handshake for embed integrations).
105
+ if (
106
+ hasKnownParentCookieSyncOrigin() &&
107
+ window.parent &&
108
+ typeof window.parent.postMessage === 'function'
109
+ ) {
110
+ window.parent.postMessage(
111
+ { type: `eturnity_${consentBridgeAppType}_cookie_sync_ready` },
112
+ parentCookieSyncOrigin
113
+ )
114
+ }
115
+ }
116
+
117
+ function getRawCookieValue(cookieName) {
118
+ const name = `${cookieName}=`
119
+ const decodedCookie = decodeURIComponent(document.cookie)
120
+ const cookieArray = decodedCookie.split(';')
121
+ for (let i = 0; i < cookieArray.length; i++) {
122
+ const cookie = cookieArray[i].trim()
123
+ if (cookie.indexOf(name) === 0) {
124
+ return cookie.substring(name.length, cookie.length)
125
+ }
126
+ }
127
+ return null
128
+ }
129
+
130
+ export function getCookieValue(cookieName) {
131
+ const rawCookieValue = getRawCookieValue(cookieName)
132
+ if (rawCookieValue !== null) {
133
+ return rawCookieValue
134
+ }
135
+ return readSyncedCookieFallback(cookieName)
136
+ }
137
+
138
+ export function setCookieWithThirdPartyFallback(cookieName, cookieValue) {
139
+ // If the cookie value is not immediately readable after
140
+ // document.cookie = cookieStr (common in third-party iframes), we keep the intended
141
+ // value in syncedCookieFallback and notify the parent to persist it.
142
+ if (!cookieName || typeof cookieName !== 'string' || typeof cookieValue !== 'string') return
8
143
 
9
- const cookieString = JSON.stringify(cookieData)
10
144
  const expiryDate = new Date()
11
145
  expiryDate.setMonth(expiryDate.getMonth() + 6)
146
+ let cookieStr = `${cookieName}=${encodeURIComponent(
147
+ cookieValue
148
+ )}; expires=${expiryDate.toUTCString()}; ${getCookieAttributes()}`
12
149
 
13
- let cookieStr = `cookieConsent=${encodeURIComponent(
14
- cookieString
15
- )}; expires=${expiryDate.toUTCString()}; path=/`
150
+ document.cookie = cookieStr
151
+ let storedCookie = getRawCookieValue(cookieName)
16
152
 
17
- const isEmbedded = window.self !== window.top
18
- const isHttps = window.location.protocol === 'https:'
153
+ if (
154
+ storedCookie !== cookieValue &&
155
+ isRunningInIframe() &&
156
+ hasKnownParentCookieSyncOrigin() &&
157
+ window.parent &&
158
+ typeof window.parent.postMessage === 'function'
159
+ ) {
160
+ saveSyncedCookieFallback(cookieName, cookieValue)
161
+ if (consentBridgeAppType) {
162
+ window.parent.postMessage(
163
+ {
164
+ type: `eturnity_${consentBridgeAppType}_cookie_fallback`,
165
+ payload: {
166
+ cookieName,
167
+ cookieValue
168
+ }
169
+ },
170
+ parentCookieSyncOrigin
171
+ )
172
+ }
173
+ storedCookie = getCookieValue(cookieName)
174
+ }
19
175
 
20
- const sameSite = (isEmbedded && isHttps) ? 'None' : 'Lax'
21
- cookieStr += `; sameSite=${sameSite}`
176
+ return storedCookie === cookieValue
177
+ }
22
178
 
23
- // Add Secure flag for HTTPS
24
- if (isHttps) {
25
- cookieStr += '; Secure'
26
- }
179
+ export function deleteCookieWithThirdPartyFallback(cookieName) {
180
+ if (!cookieName || typeof cookieName !== 'string') return false
27
181
 
182
+ const cookieStr = `${cookieName}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; ${getCookieAttributes()}`
28
183
  document.cookie = cookieStr
184
+
185
+ clearSyncedCookieFallback(cookieName)
186
+ if (
187
+ isRunningInIframe() &&
188
+ hasKnownParentCookieSyncOrigin() &&
189
+ window.parent &&
190
+ typeof window.parent.postMessage === 'function' &&
191
+ consentBridgeAppType
192
+ ) {
193
+ window.parent.postMessage(
194
+ {
195
+ type: `eturnity_${consentBridgeAppType}_cookie_delete`,
196
+ payload: { cookieName }
197
+ },
198
+ parentCookieSyncOrigin
199
+ )
200
+ }
201
+
202
+ const storedCookie = getCookieValue(cookieName)
203
+ return storedCookie === null
204
+ }
205
+
206
+ export function saveCookieConsent(categories) {
207
+ const cookieData = {
208
+ categories,
209
+ timestamp: new Date().toISOString(),
210
+ consent_given: true
211
+ }
212
+ setCookieWithThirdPartyFallback(
213
+ 'cookieConsent',
214
+ JSON.stringify(cookieData)
215
+ )
29
216
  }
@@ -0,0 +1,541 @@
1
+ ;(() => {
2
+ const API_NAME = 'eturnityConsentBridge'
3
+ const STATE_KEY = '__eturnityConsentBridgeState__'
4
+ const CONSENT_COOKIE_MONTHS = 6
5
+ const SUPPORTED_TYPES = [
6
+ 'solar_calculator',
7
+ 'heating_calculator',
8
+ 'e_mobility_configurator',
9
+ ]
10
+
11
+ if (typeof window === 'undefined' || typeof document === 'undefined') return
12
+
13
+ const sharedState = globalThis[STATE_KEY] || {
14
+ instancesByIframeId: new Map(),
15
+ messageListenerAttached: false,
16
+ api: null,
17
+ }
18
+ globalThis[STATE_KEY] = sharedState
19
+
20
+ if (typeof sharedState.api === 'function') {
21
+ window[API_NAME] = sharedState.api
22
+ globalThis[API_NAME] = sharedState.api
23
+ return
24
+ }
25
+
26
+ const getTypeConfig = (type) => {
27
+ return {
28
+ consentCookieName: `eturnity_${type}_consent`,
29
+ cookieSyncReadyMessage: `eturnity_${type}_cookie_sync_ready`,
30
+ cookieFallbackMessage: `eturnity_${type}_cookie_fallback`,
31
+ cookieDeleteMessage: `eturnity_${type}_cookie_delete`,
32
+ cookieSyncMessage: `eturnity_${type}_cookie_sync`,
33
+ }
34
+ }
35
+
36
+ const normalizeIframeId = (iframeId) => {
37
+ if (typeof iframeId !== 'string') return ''
38
+ return iframeId.trim().replace(/^#/, '')
39
+ }
40
+
41
+ const isPlainObject = (value) => {
42
+ return Object.prototype.toString.call(value) === '[object Object]'
43
+ }
44
+
45
+ const readMessageType = (data) => {
46
+ if (!isPlainObject(data) || typeof data.type !== 'string') return ''
47
+ return data.type
48
+ }
49
+
50
+ const log = (instance, message, details) => {
51
+ if (!instance || !instance.logs) return
52
+ const prefix = `[eturnityConsentBridge:${instance.iframeId}]`
53
+ if (typeof details === 'undefined') {
54
+ console.log(`${prefix} ${message}`)
55
+ return
56
+ }
57
+ console.log(`${prefix} ${message}`, details)
58
+ }
59
+
60
+ const getHostCookieAttributes = () => {
61
+ let attributes = 'path=/; SameSite=Lax'
62
+ if (window.location.protocol === 'https:') {
63
+ attributes += '; Secure'
64
+ }
65
+ return attributes
66
+ }
67
+
68
+ const setCookie = (name, value, months) => {
69
+ // Use calendar months so host and iframe expiry stay aligned.
70
+ try {
71
+ const expiresAt = new Date()
72
+ expiresAt.setMonth(expiresAt.getMonth() + months)
73
+ document.cookie = `${name}=${encodeURIComponent(
74
+ value
75
+ )}; expires=${expiresAt.toUTCString()}; ${getHostCookieAttributes()}`
76
+ return true
77
+ } catch (error) {
78
+ console.warn(
79
+ `[eturnityConsentBridge] Failed to write cookie "${name}".`,
80
+ error
81
+ )
82
+ return false
83
+ }
84
+ }
85
+
86
+ const deleteCookie = (name) => {
87
+ try {
88
+ document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; ${getHostCookieAttributes()}`
89
+ return true
90
+ } catch (error) {
91
+ console.warn(
92
+ `[eturnityConsentBridge] Failed to delete cookie "${name}".`,
93
+ error
94
+ )
95
+ return false
96
+ }
97
+ }
98
+
99
+ const getCookie = (name) => {
100
+ try {
101
+ const prefix = `${name}=`
102
+ const parts = document.cookie.split(';')
103
+ for (let i = 0; i < parts.length; i += 1) {
104
+ const part = parts[i].trim()
105
+ if (part.indexOf(prefix) === 0) {
106
+ return decodeURIComponent(part.substring(prefix.length))
107
+ }
108
+ }
109
+ return null
110
+ } catch (error) {
111
+ console.warn(`[eturnityConsentBridge] Failed to read cookie "${name}".`, error)
112
+ return null
113
+ }
114
+ }
115
+
116
+ const readStoredSyncedCookies = (consentCookieName) => {
117
+ // Stored cookie format: { [cookieName]: { cookieValue, updatedAt } }
118
+ const raw = getCookie(consentCookieName)
119
+ if (!raw) return {}
120
+ try {
121
+ const parsed = JSON.parse(raw)
122
+ if (!parsed || typeof parsed !== 'object') return {}
123
+ return parsed
124
+ } catch (error) {
125
+ console.warn(
126
+ `[eturnityConsentBridge] Invalid JSON in "${consentCookieName}" cookie. Ignoring stored value.`
127
+ )
128
+ return {}
129
+ }
130
+ }
131
+
132
+ const persistSyncedCookie = (instance, cookieName, cookieValue) => {
133
+ // Keep latest fallback payload in host cookie for page reload replay.
134
+ const currentData = readStoredSyncedCookies(instance.config.consentCookieName)
135
+ currentData[cookieName] = {
136
+ cookieValue,
137
+ updatedAt: new Date().toISOString(),
138
+ }
139
+
140
+ let serialized = ''
141
+ try {
142
+ serialized = JSON.stringify(currentData)
143
+ } catch (error) {
144
+ console.warn(
145
+ `[eturnityConsentBridge] Failed to serialize cookie cache for iframe "${instance.iframeId}".`,
146
+ error
147
+ )
148
+ return false
149
+ }
150
+
151
+ if (serialized.length > 3500) {
152
+ console.warn(
153
+ `[eturnityConsentBridge] Cookie cache for iframe "${instance.iframeId}" is approaching cookie size limits and may be truncated by browsers.`
154
+ )
155
+ }
156
+
157
+ const persisted = setCookie(
158
+ instance.config.consentCookieName,
159
+ serialized,
160
+ CONSENT_COOKIE_MONTHS
161
+ )
162
+ if (persisted) {
163
+ log(instance, `Stored cookie fallback "${cookieName}" in host cookie cache.`)
164
+ }
165
+ return persisted
166
+ }
167
+
168
+ const removeSyncedCookie = (instance, cookieName) => {
169
+ const currentData = readStoredSyncedCookies(instance.config.consentCookieName)
170
+ if (!Object.prototype.hasOwnProperty.call(currentData, cookieName)) return true
171
+
172
+ delete currentData[cookieName]
173
+
174
+ if (Object.keys(currentData).length === 0) {
175
+ const deleted = deleteCookie(instance.config.consentCookieName)
176
+ if (deleted) {
177
+ log(instance, `Removed "${cookieName}" from host cookie cache and cleared empty cache cookie.`)
178
+ }
179
+ return deleted
180
+ }
181
+
182
+ let serialized = ''
183
+ try {
184
+ serialized = JSON.stringify(currentData)
185
+ } catch (error) {
186
+ console.warn(
187
+ `[eturnityConsentBridge] Failed to serialize cookie cache for iframe "${instance.iframeId}" after deleting "${cookieName}".`,
188
+ error
189
+ )
190
+ return false
191
+ }
192
+
193
+ const persisted = setCookie(
194
+ instance.config.consentCookieName,
195
+ serialized,
196
+ CONSENT_COOKIE_MONTHS
197
+ )
198
+
199
+ if (persisted) {
200
+ log(instance, `Removed cookie fallback "${cookieName}" from host cookie cache.`)
201
+ }
202
+ return persisted
203
+ }
204
+
205
+ const parseCookiePayload = (payload) => {
206
+ if (!isPlainObject(payload)) return null
207
+ if (typeof payload.cookieName !== 'string') return null
208
+ if (typeof payload.cookieValue !== 'string') return null
209
+ return {
210
+ cookieName: payload.cookieName,
211
+ cookieValue: payload.cookieValue,
212
+ }
213
+ }
214
+
215
+ const parseCookieDeletePayload = (payload) => {
216
+ if (!isPlainObject(payload)) return null
217
+ if (typeof payload.cookieName !== 'string') return null
218
+ return {
219
+ cookieName: payload.cookieName,
220
+ }
221
+ }
222
+
223
+ const getIframeContext = (iframeId) => {
224
+ const iframe = document.getElementById(iframeId)
225
+ if (!iframe || iframe.tagName !== 'IFRAME') return null
226
+
227
+ const src = iframe.getAttribute('src') || ''
228
+ if (!src) return { iframe, origin: null }
229
+
230
+ try {
231
+ return {
232
+ iframe,
233
+ origin: new URL(src, window.location.origin).origin,
234
+ }
235
+ } catch (error) {
236
+ return { iframe, origin: null }
237
+ }
238
+ }
239
+
240
+ const refreshInstanceContext = (instance) => {
241
+ // Re-resolve iframe/origin because src can change after initialization.
242
+ const context = getIframeContext(instance.iframeId)
243
+ if (!context) {
244
+ instance.iframe = null
245
+ instance.origin = null
246
+ return
247
+ }
248
+ instance.iframe = context.iframe
249
+ instance.origin = context.origin
250
+ }
251
+
252
+ const postCookieToIframe = (instance, cookiePayload) => {
253
+ refreshInstanceContext(instance)
254
+ if (!instance.iframe || !instance.iframe.contentWindow || !instance.origin) {
255
+ console.warn(
256
+ `[eturnityConsentBridge] Cannot post "${instance.config.cookieSyncMessage}" for iframe "${instance.iframeId}" because iframe window or origin is not ready.`
257
+ )
258
+ return
259
+ }
260
+ // Post only to the known iframe origin for this instance.
261
+ instance.iframe.contentWindow.postMessage(
262
+ {
263
+ type: instance.config.cookieSyncMessage,
264
+ payload: cookiePayload,
265
+ },
266
+ instance.origin
267
+ )
268
+ log(
269
+ instance,
270
+ `Posted "${instance.config.cookieSyncMessage}" to iframe origin "${instance.origin}".`,
271
+ cookiePayload
272
+ )
273
+ }
274
+
275
+ const postStoredCookiesToIframe = (instance, source) => {
276
+ // Replay host-stored values when requested by init or handshake.
277
+ const replaySource = source === 'sync_ready' ? 'sync_ready' : 'init'
278
+ const stored = readStoredSyncedCookies(instance.config.consentCookieName)
279
+ const cookieNames = Object.keys(stored)
280
+ if (!cookieNames.length) {
281
+ log(instance, `No stored cookies to replay (${replaySource}).`)
282
+ return
283
+ }
284
+ log(
285
+ instance,
286
+ `Replaying ${cookieNames.length} stored cookie(s) to iframe (${replaySource}).`
287
+ )
288
+ cookieNames.forEach((cookieName) => {
289
+ const entry = stored[cookieName]
290
+ if (!entry || typeof entry.cookieValue !== 'string') return
291
+ postCookieToIframe(instance, {
292
+ cookieName,
293
+ cookieValue: entry.cookieValue,
294
+ })
295
+ })
296
+ }
297
+
298
+ const findMatchingInstance = (eventSource) => {
299
+ // Route by source window so multiple iframes on a page are isolated.
300
+ const instances = Array.from(sharedState.instancesByIframeId.values())
301
+ for (let i = 0; i < instances.length; i += 1) {
302
+ const instance = instances[i]
303
+ refreshInstanceContext(instance)
304
+ if (
305
+ instance.iframe &&
306
+ instance.iframe.contentWindow &&
307
+ instance.iframe.contentWindow === eventSource
308
+ ) {
309
+ return instance
310
+ }
311
+ }
312
+ return null
313
+ }
314
+
315
+ const onMessage = (event) => {
316
+ if (!event || !isPlainObject(event.data)) return
317
+
318
+ const messageType = readMessageType(event.data)
319
+ const instance = findMatchingInstance(event.source)
320
+ if (!instance) {
321
+ if (messageType.indexOf('eturnity_') === 0) {
322
+ console.warn(
323
+ `[eturnityConsentBridge] Received "${messageType}" but could not match event.source to a registered iframe instance.`
324
+ )
325
+ }
326
+ return
327
+ }
328
+
329
+ if (!instance.origin) {
330
+ if (messageType.indexOf('eturnity_') === 0) {
331
+ console.warn(
332
+ `[eturnityConsentBridge] Ignoring "${messageType}" for iframe "${instance.iframeId}" because iframe origin could not be resolved from src.`
333
+ )
334
+ }
335
+ return
336
+ }
337
+
338
+ // Reject messages not coming from the expected origin of that iframe.
339
+ if (event.origin !== instance.origin) {
340
+ console.warn(
341
+ `[eturnityConsentBridge] Ignoring "${messageType}" due to origin mismatch for iframe "${instance.iframeId}". Expected "${instance.origin}", got "${event.origin}".`
342
+ )
343
+ return
344
+ }
345
+
346
+ if (!messageType) return
347
+
348
+ // Handshake event: iframe requests replay of previously synced cookies.
349
+ if (messageType === instance.config.cookieSyncReadyMessage) {
350
+ log(instance, `Iframe reported "${instance.config.cookieSyncReadyMessage}".`)
351
+ postStoredCookiesToIframe(instance, 'sync_ready')
352
+ return
353
+ }
354
+
355
+ if (messageType === instance.config.cookieFallbackMessage) {
356
+ log(instance, `Received message "${messageType}" from origin "${event.origin}".`)
357
+
358
+ const payload = parseCookiePayload(event.data.payload)
359
+ if (!payload) {
360
+ console.warn(
361
+ `[eturnityConsentBridge] Invalid payload for "${messageType}" from iframe "${instance.iframeId}".`
362
+ )
363
+ return
364
+ }
365
+
366
+ persistSyncedCookie(instance, payload.cookieName, payload.cookieValue)
367
+ postCookieToIframe(instance, payload)
368
+ return
369
+ }
370
+
371
+ if (messageType === instance.config.cookieDeleteMessage) {
372
+ log(instance, `Received message "${messageType}" from origin "${event.origin}".`)
373
+
374
+ const payload = parseCookieDeletePayload(event.data.payload)
375
+ if (!payload) {
376
+ console.warn(
377
+ `[eturnityConsentBridge] Invalid payload for "${messageType}" from iframe "${instance.iframeId}".`
378
+ )
379
+ return
380
+ }
381
+
382
+ removeSyncedCookie(instance, payload.cookieName)
383
+ postCookieToIframe(instance, {
384
+ cookieName: payload.cookieName,
385
+ isDeleted: true,
386
+ })
387
+ return
388
+ }
389
+
390
+ if (messageType.indexOf('eturnity_') === 0) {
391
+ console.warn(
392
+ `[eturnityConsentBridge] Ignoring unsupported message "${messageType}" for iframe "${instance.iframeId}".`
393
+ )
394
+ }
395
+ }
396
+
397
+ const ensureMessageListener = () => {
398
+ // One shared listener handles all registered iframe instances.
399
+ if (sharedState.messageListenerAttached) return
400
+ window.addEventListener('message', onMessage)
401
+ sharedState.messageListenerAttached = true
402
+ }
403
+
404
+ const cleanupMessageListenerIfIdle = () => {
405
+ if (
406
+ !sharedState.messageListenerAttached ||
407
+ sharedState.instancesByIframeId.size > 0
408
+ ) {
409
+ return
410
+ }
411
+ window.removeEventListener('message', onMessage)
412
+ sharedState.messageListenerAttached = false
413
+ }
414
+
415
+ const destroyInstance = (iframeId) => {
416
+ const normalizedIframeId = normalizeIframeId(iframeId)
417
+ if (!normalizedIframeId) return false
418
+ const removed = sharedState.instancesByIframeId.delete(normalizedIframeId)
419
+ cleanupMessageListenerIfIdle()
420
+ return removed
421
+ }
422
+
423
+ const normalizeOptions = (options) => {
424
+ if (!isPlainObject(options)) {
425
+ return {
426
+ valid: false,
427
+ reason:
428
+ '[eturnityConsentBridge] Missing or invalid options object. Use eturnityConsentBridge("iframe-id", { type: "solar_calculator | heating_calculator | e_mobility_configurator", logs: false }).',
429
+ }
430
+ }
431
+
432
+ if (typeof options.logs !== 'undefined' && typeof options.logs !== 'boolean') {
433
+ return {
434
+ valid: false,
435
+ reason:
436
+ '[eturnityConsentBridge] Invalid options.logs value. Expected boolean when provided.',
437
+ }
438
+ }
439
+
440
+ if (typeof options.type !== 'string' || !SUPPORTED_TYPES.includes(options.type)) {
441
+ return {
442
+ valid: false,
443
+ reason: `[eturnityConsentBridge] Invalid options.type. Supported values: ${SUPPORTED_TYPES.join(
444
+ ', '
445
+ )}.`,
446
+ }
447
+ }
448
+
449
+ return {
450
+ valid: true,
451
+ value: {
452
+ type: options.type,
453
+ logs: options.logs === true,
454
+ },
455
+ }
456
+ }
457
+
458
+ const initInstance = (iframeId, options) => {
459
+ const existing = sharedState.instancesByIframeId.get(iframeId)
460
+ if (existing) {
461
+ const typeChanged = existing.type !== options.type
462
+ existing.logs = options.logs
463
+ if (typeChanged) {
464
+ existing.type = options.type
465
+ existing.config = getTypeConfig(options.type)
466
+ }
467
+ refreshInstanceContext(existing)
468
+ log(existing, typeChanged ? `Bridge updated to type "${options.type}".` : 'Bridge re-initialized with existing configuration.')
469
+ postStoredCookiesToIframe(existing, 'init')
470
+ return existing
471
+ }
472
+
473
+ const type = options.type
474
+ const config = getTypeConfig(type)
475
+ const context = getIframeContext(iframeId)
476
+ const instance = {
477
+ iframeId,
478
+ iframe: context ? context.iframe : null,
479
+ origin: context ? context.origin : null,
480
+ type,
481
+ config,
482
+ logs: options.logs,
483
+ destroy: () => destroyInstance(iframeId),
484
+ }
485
+
486
+ if (!context) {
487
+ console.warn(
488
+ `[eturnityConsentBridge] Iframe with id "${iframeId}" was not found during initialization. The bridge will start working once the iframe is available.`
489
+ )
490
+ } else if (!context.origin) {
491
+ console.warn(
492
+ `[eturnityConsentBridge] Could not resolve iframe origin for "${iframeId}". Please make sure iframe src is set to a valid URL.`
493
+ )
494
+ }
495
+
496
+ sharedState.instancesByIframeId.set(iframeId, instance)
497
+ ensureMessageListener()
498
+ log(instance, `Bridge initialized with type "${type}".`)
499
+ postStoredCookiesToIframe(instance, 'init')
500
+ return instance
501
+ }
502
+
503
+ const eturnityConsentBridge = (iframeId, options) => {
504
+ const normalizedIframeId = normalizeIframeId(iframeId)
505
+ if (!normalizedIframeId) {
506
+ console.warn(
507
+ '[eturnityConsentBridge] Missing iframe id. Use eturnityConsentBridge("iframe-id", { type: "solar_calculator | heating_calculator | e_mobility_configurator" }).'
508
+ )
509
+ return null
510
+ }
511
+
512
+ const normalizedOptions = normalizeOptions(options)
513
+ if (!normalizedOptions.valid) {
514
+ console.warn(normalizedOptions.reason)
515
+ return null
516
+ }
517
+
518
+ return initInstance(normalizedIframeId, normalizedOptions.value)
519
+ }
520
+
521
+ eturnityConsentBridge.destroy = (iframeId) => {
522
+ const normalizedIframeId = normalizeIframeId(iframeId)
523
+ if (!normalizedIframeId) {
524
+ console.warn(
525
+ '[eturnityConsentBridge] Missing iframe id for destroy(). Use eturnityConsentBridge.destroy("iframe-id").'
526
+ )
527
+ return false
528
+ }
529
+ return destroyInstance(normalizedIframeId)
530
+ }
531
+
532
+ eturnityConsentBridge.destroyAll = () => {
533
+ sharedState.instancesByIframeId.clear()
534
+ cleanupMessageListenerIfIdle()
535
+ }
536
+
537
+ // Expose API as both window.eturnityConsentBridge(...) and eturnityConsentBridge(...).
538
+ sharedState.api = eturnityConsentBridge
539
+ window[API_NAME] = eturnityConsentBridge
540
+ globalThis[API_NAME] = eturnityConsentBridge
541
+ })()