@consentx/angular 1.0.0

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.
@@ -0,0 +1,358 @@
1
+ import * as i0 from '@angular/core';
2
+ import { InjectionToken, PLATFORM_ID, Optional, Inject, Injectable, makeEnvironmentProviders, APP_INITIALIZER, SkipSelf, NgModule } from '@angular/core';
3
+ import { isPlatformBrowser, DOCUMENT } from '@angular/common';
4
+ import { BehaviorSubject } from 'rxjs';
5
+ import { map, distinctUntilChanged } from 'rxjs/operators';
6
+
7
+ /**
8
+ * DI token carrying the resolved {@link ConsentXConfig}. Provided by
9
+ * `ConsentXModule.forRoot(...)` or `provideConsentX(...)`.
10
+ */
11
+ const CONSENTX_CONFIG = new InjectionToken('CONSENTX_CONFIG');
12
+
13
+ /**
14
+ * Public types for @consentx/angular.
15
+ *
16
+ * ConsentX runs on the site-key model (Model C in the Connect spec): the user
17
+ * pastes the Site Key copied from the ConsentX dashboard (Websites -> their
18
+ * site) and ensures their domain is on that site's allowlist. The embed then
19
+ * loads with no server-side handshake.
20
+ */
21
+ /** Default ConsentX application host (no trailing slash). */
22
+ const CONSENTX_DEFAULT_APP_URL = 'https://app.consentx.io';
23
+
24
+ const SCRIPT_ID = 'consentx-embed';
25
+ const CONSENT_MODE_ID = 'consentx-consent-mode';
26
+ /**
27
+ * Injects the ConsentX embed script (`/api/SITE_KEY/embed.js`) into `<head>`
28
+ * and exposes the visitor's consent state, derived from the widget's
29
+ * `cx:consent` events.
30
+ *
31
+ * Site-key model (Connect spec, Model C): no redirect handshake. The Site Key
32
+ * is supplied via configuration; this service only performs clean embed
33
+ * injection plus a consent hook.
34
+ *
35
+ * The service is `providedIn: 'root'` so it is a singleton across the app. It
36
+ * is SSR-safe: all DOM access is guarded behind `isPlatformBrowser`, so it is a
37
+ * no-op on the server and runs once the app boots in the browser.
38
+ */
39
+ class ConsentXService {
40
+ config;
41
+ document;
42
+ zone;
43
+ appUrl;
44
+ isBrowser;
45
+ listenerBound = false;
46
+ boundConsentListener;
47
+ stateSubject = new BehaviorSubject({
48
+ loaded: false,
49
+ decided: false,
50
+ granted: [],
51
+ detail: null,
52
+ });
53
+ /** Full consent state as an observable. Emits an initial snapshot. */
54
+ state$ = this.stateSubject.asObservable();
55
+ /** Convenience stream of the granted category slugs. */
56
+ granted$ = this.state$.pipe(map((s) => s.granted), distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i])));
57
+ constructor(config, platformId, document, zone) {
58
+ this.config = config;
59
+ this.document = document;
60
+ this.zone = zone;
61
+ this.isBrowser = isPlatformBrowser(platformId);
62
+ this.appUrl = this.normalizeAppUrl(this.config?.appUrl);
63
+ if (!this.config?.siteKey) {
64
+ // Misconfiguration is loud but non-fatal: the app keeps working without
65
+ // the banner instead of crashing bootstrap.
66
+ // eslint-disable-next-line no-console
67
+ console.error('[ConsentX] No siteKey provided. Pass { siteKey } to ConsentXModule.forRoot() ' +
68
+ 'or provideConsentX(). Copy the Site Key from the ConsentX dashboard -> ' +
69
+ 'Websites -> your site.');
70
+ }
71
+ }
72
+ /**
73
+ * Auto-init entry point. Called once during app initialisation (via an
74
+ * APP_INITIALIZER registered by the module/provider) unless `manualLoad` is
75
+ * set. Binds the consent listener and injects the embed.
76
+ */
77
+ init() {
78
+ if (!this.isBrowser) {
79
+ return;
80
+ }
81
+ this.bindConsentListener();
82
+ if (!this.config?.manualLoad) {
83
+ this.load();
84
+ }
85
+ }
86
+ /**
87
+ * Inject the ConsentX embed into `<head>`. Idempotent: a second call is a
88
+ * no-op if the script is already present. Safe to call manually when
89
+ * `manualLoad` is enabled (e.g. after a cookie-policy route is reached).
90
+ */
91
+ load() {
92
+ if (!this.isBrowser) {
93
+ return;
94
+ }
95
+ const siteKey = this.config?.siteKey?.trim();
96
+ if (!siteKey) {
97
+ return;
98
+ }
99
+ this.bindConsentListener();
100
+ if (this.config?.consentMode) {
101
+ this.injectConsentModeDefaults();
102
+ }
103
+ // Expose the key the way embed.js expects (window.consentx_key), matching
104
+ // the canonical embed contract.
105
+ this.document.defaultView.consentx_key = siteKey;
106
+ if (this.document.getElementById(SCRIPT_ID)) {
107
+ // Already injected — make sure the loaded flag reflects reality.
108
+ this.patchState({ loaded: true });
109
+ return;
110
+ }
111
+ const head = this.document.head || this.document.getElementsByTagName('head')[0];
112
+ if (!head) {
113
+ return;
114
+ }
115
+ const script = this.document.createElement('script');
116
+ script.id = SCRIPT_ID;
117
+ script.type = 'module';
118
+ script.src = `${this.appUrl}/api/${encodeURIComponent(siteKey)}/embed.js`;
119
+ script.setAttribute('data-consentx', siteKey);
120
+ script.async = true;
121
+ script.addEventListener('load', () => {
122
+ this.zone.run(() => this.patchState({ loaded: true }));
123
+ });
124
+ script.addEventListener('error', () => {
125
+ // eslint-disable-next-line no-console
126
+ console.error(`[ConsentX] Failed to load embed from ${script.src}`);
127
+ });
128
+ head.appendChild(script);
129
+ }
130
+ /**
131
+ * Open the ConsentX preferences dialog. Requires the embed to have loaded and
132
+ * exposed `window.ConsentX`. Returns `true` if the call was dispatched.
133
+ */
134
+ openPreferences() {
135
+ if (!this.isBrowser) {
136
+ return false;
137
+ }
138
+ const api = this.document.defaultView?.ConsentX;
139
+ if (api && typeof api.open === 'function') {
140
+ api.open();
141
+ return true;
142
+ }
143
+ return false;
144
+ }
145
+ /** Latest granted category slugs (synchronous snapshot). */
146
+ getGranted() {
147
+ return this.stateSubject.value.granted;
148
+ }
149
+ /**
150
+ * Whether the visitor has granted a specific consent category slug, e.g.
151
+ * `hasConsent('analytics')`.
152
+ */
153
+ hasConsent(slug) {
154
+ return this.stateSubject.value.granted.includes(slug);
155
+ }
156
+ /** Current full state snapshot (synchronous). */
157
+ getState() {
158
+ return this.stateSubject.value;
159
+ }
160
+ ngOnDestroy() {
161
+ if (this.isBrowser && this.boundConsentListener) {
162
+ this.document.defaultView?.removeEventListener('cx:consent', this.boundConsentListener);
163
+ }
164
+ }
165
+ // --- internals -----------------------------------------------------------
166
+ /**
167
+ * Print the Google Consent Mode v2 denied-by-default stub before any
168
+ * analytics tag fires. Idempotent. Mirrors the canonical embed contract.
169
+ */
170
+ injectConsentModeDefaults() {
171
+ const win = this.document.defaultView;
172
+ if (!win) {
173
+ return;
174
+ }
175
+ if (this.document.getElementById(CONSENT_MODE_ID)) {
176
+ return;
177
+ }
178
+ win.dataLayer = win.dataLayer || [];
179
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
180
+ const gtag = (...args) => win.dataLayer.push(args);
181
+ gtag('consent', 'default', {
182
+ ad_storage: 'denied',
183
+ analytics_storage: 'denied',
184
+ ad_user_data: 'denied',
185
+ ad_personalization: 'denied',
186
+ functionality_storage: 'denied',
187
+ personalization_storage: 'denied',
188
+ security_storage: 'granted',
189
+ wait_for_update: 500,
190
+ });
191
+ // Marker so a second call (or a duplicate inline tag) is a no-op.
192
+ const marker = this.document.createElement('script');
193
+ marker.id = CONSENT_MODE_ID;
194
+ marker.type = 'application/json';
195
+ marker.textContent = '{"consentx":"consent-mode-defaults"}';
196
+ (this.document.head || this.document.documentElement).appendChild(marker);
197
+ }
198
+ /** Subscribe to the widget's `cx:consent` events exactly once. */
199
+ bindConsentListener() {
200
+ if (this.listenerBound || !this.isBrowser) {
201
+ return;
202
+ }
203
+ const win = this.document.defaultView;
204
+ if (!win) {
205
+ return;
206
+ }
207
+ this.boundConsentListener = (event) => {
208
+ const detail = event.detail;
209
+ const granted = Array.isArray(detail?.granted) ? detail.granted : [];
210
+ // Re-enter Angular so bound templates/observables update.
211
+ this.zone.run(() => this.patchState({
212
+ decided: true,
213
+ granted,
214
+ detail: detail ?? null,
215
+ }));
216
+ };
217
+ win.addEventListener('cx:consent', this.boundConsentListener);
218
+ this.listenerBound = true;
219
+ }
220
+ patchState(partial) {
221
+ this.stateSubject.next({ ...this.stateSubject.value, ...partial });
222
+ }
223
+ /** Resolve + sanitise the app host (strip trailing slash). */
224
+ normalizeAppUrl(url) {
225
+ const raw = (url || CONSENTX_DEFAULT_APP_URL).trim();
226
+ return raw.replace(/\/+$/, '');
227
+ }
228
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ConsentXService, deps: [{ token: CONSENTX_CONFIG, optional: true }, { token: PLATFORM_ID }, { token: DOCUMENT }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
229
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ConsentXService, providedIn: 'root' });
230
+ }
231
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ConsentXService, decorators: [{
232
+ type: Injectable,
233
+ args: [{ providedIn: 'root' }]
234
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
235
+ type: Optional
236
+ }, {
237
+ type: Inject,
238
+ args: [CONSENTX_CONFIG]
239
+ }] }, { type: undefined, decorators: [{
240
+ type: Inject,
241
+ args: [PLATFORM_ID]
242
+ }] }, { type: Document, decorators: [{
243
+ type: Inject,
244
+ args: [DOCUMENT]
245
+ }] }, { type: i0.NgZone }] });
246
+
247
+ /**
248
+ * Factory used by APP_INITIALIZER. Initialising on app boot means the embed is
249
+ * injected once the Angular application is ready (i.e. after hydration on SSR),
250
+ * which is the SPA-safe load point the Connect spec recommends so the
251
+ * framework's DOM reconciliation does not strip the appended
252
+ * `#consentx-cookie-consent` div.
253
+ */
254
+ function consentxInitFactory(service) {
255
+ return () => service.init();
256
+ }
257
+ /**
258
+ * Standalone-app provider. Use in `bootstrapApplication(...)`:
259
+ *
260
+ * ```ts
261
+ * bootstrapApplication(AppComponent, {
262
+ * providers: [provideConsentX({ siteKey: 'YOUR_SITE_KEY' })],
263
+ * });
264
+ * ```
265
+ */
266
+ function provideConsentX(config) {
267
+ return makeEnvironmentProviders([
268
+ { provide: CONSENTX_CONFIG, useValue: config },
269
+ ConsentXService,
270
+ {
271
+ provide: APP_INITIALIZER,
272
+ multi: true,
273
+ useFactory: consentxInitFactory,
274
+ deps: [ConsentXService],
275
+ },
276
+ ]);
277
+ }
278
+ /**
279
+ * Internal: the providers `ConsentXModule.forRoot()` contributes. Kept separate
280
+ * so both the NgModule and the standalone API share one source of truth.
281
+ */
282
+ function consentxRootProviders(config) {
283
+ return [
284
+ { provide: CONSENTX_CONFIG, useValue: config },
285
+ ConsentXService,
286
+ {
287
+ provide: APP_INITIALIZER,
288
+ multi: true,
289
+ useFactory: consentxInitFactory,
290
+ deps: [ConsentXService],
291
+ },
292
+ ];
293
+ }
294
+
295
+ /**
296
+ * The ConsentX module for classic NgModule-based applications.
297
+ *
298
+ * Import once at the application root via `forRoot`:
299
+ *
300
+ * ```ts
301
+ * @NgModule({
302
+ * imports: [
303
+ * BrowserModule,
304
+ * ConsentXModule.forRoot({ siteKey: 'YOUR_SITE_KEY' }),
305
+ * ],
306
+ * bootstrap: [AppComponent],
307
+ * })
308
+ * export class AppModule {}
309
+ * ```
310
+ *
311
+ * On app init the {@link ConsentXService} injects the ConsentX embed
312
+ * (`/api/SITE_KEY/embed.js`) into `<head>` and starts tracking `cx:consent`.
313
+ *
314
+ * For standalone (`bootstrapApplication`) apps use `provideConsentX()` instead.
315
+ */
316
+ class ConsentXModule {
317
+ /**
318
+ * Guard against importing `forRoot()` more than once — it would register the
319
+ * embed initialiser twice.
320
+ */
321
+ constructor(parent) {
322
+ if (parent) {
323
+ throw new Error('ConsentXModule.forRoot() was imported more than once. Import it only in ' +
324
+ 'your root AppModule.');
325
+ }
326
+ }
327
+ /**
328
+ * Configure ConsentX with your Site Key (and optional overrides). Returns the
329
+ * module plus the root providers (config token, service, APP_INITIALIZER).
330
+ */
331
+ static forRoot(config) {
332
+ return {
333
+ ngModule: ConsentXModule,
334
+ providers: consentxRootProviders(config),
335
+ };
336
+ }
337
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ConsentXModule, deps: [{ token: ConsentXModule, optional: true, skipSelf: true }], target: i0.ɵɵFactoryTarget.NgModule });
338
+ static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "17.3.12", ngImport: i0, type: ConsentXModule });
339
+ static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ConsentXModule });
340
+ }
341
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.3.12", ngImport: i0, type: ConsentXModule, decorators: [{
342
+ type: NgModule
343
+ }], ctorParameters: () => [{ type: ConsentXModule, decorators: [{
344
+ type: Optional
345
+ }, {
346
+ type: SkipSelf
347
+ }] }] });
348
+
349
+ /*
350
+ * Public API surface of @consentx/angular.
351
+ */
352
+
353
+ /**
354
+ * Generated bundle index. Do not edit.
355
+ */
356
+
357
+ export { CONSENTX_CONFIG, CONSENTX_DEFAULT_APP_URL, ConsentXModule, ConsentXService, consentxInitFactory, consentxRootProviders, provideConsentX };
358
+ //# sourceMappingURL=consentx-angular.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"consentx-angular.mjs","sources":["../../src/lib/consentx.tokens.ts","../../src/lib/consentx.types.ts","../../src/lib/consentx.service.ts","../../src/lib/consentx.providers.ts","../../src/lib/consentx.module.ts","../../src/public-api.ts","../../src/consentx-angular.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport { ConsentXConfig } from './consentx.types';\n\n/**\n * DI token carrying the resolved {@link ConsentXConfig}. Provided by\n * `ConsentXModule.forRoot(...)` or `provideConsentX(...)`.\n */\nexport const CONSENTX_CONFIG = new InjectionToken<ConsentXConfig>('CONSENTX_CONFIG');\n","/**\n * Public types for @consentx/angular.\n *\n * ConsentX runs on the site-key model (Model C in the Connect spec): the user\n * pastes the Site Key copied from the ConsentX dashboard (Websites -> their\n * site) and ensures their domain is on that site's allowlist. The embed then\n * loads with no server-side handshake.\n */\n\n/** Default ConsentX application host (no trailing slash). */\nexport const CONSENTX_DEFAULT_APP_URL = 'https://app.consentx.io';\n\n/**\n * Configuration passed to `ConsentXModule.forRoot(...)` (NgModule apps) or\n * `provideConsentX(...)` (standalone apps).\n */\nexport interface ConsentXConfig {\n /**\n * The ConsentX Site Key for this website. Copy it from the ConsentX\n * dashboard -> Websites -> your site. Required.\n */\n siteKey: string;\n\n /**\n * Override the ConsentX app host (scheme + host, no trailing slash).\n * Defaults to `https://app.consentx.io`. Use this for staging hosts such as\n * `https://consentx1.satyamrastogi.com`.\n */\n appUrl?: string;\n\n /**\n * When `true`, the service prints the Google Consent Mode v2\n * denied-by-default stub into `<head>` before the embed loads, so any\n * analytics tag that fires before consent starts denied. The ConsentX\n * widget emits the `gtag('consent','update',...)` calls on the visitor's\n * choice. Defaults to `false`.\n */\n consentMode?: boolean;\n\n /**\n * Disable automatic embed injection on app init. Set this to `true` if you\n * want to call `ConsentXService.load()` yourself at a custom point.\n * Defaults to `false` (auto-inject on init).\n */\n manualLoad?: boolean;\n}\n\n/** The category slugs a visitor has granted, e.g. `['analytics','marketing']`. */\nexport type ConsentGranted = string[];\n\n/**\n * The detail payload of the `cx:consent` browser event emitted by the ConsentX\n * widget. `granted` is the array of granted category slugs.\n */\nexport interface ConsentXConsentDetail {\n granted: ConsentGranted;\n [key: string]: unknown;\n}\n\n/** Snapshot of consent state exposed by `ConsentXService.state$`. */\nexport interface ConsentXState {\n /** Whether the embed script has loaded into the page. */\n loaded: boolean;\n /** Whether a `cx:consent` event has been received this session. */\n decided: boolean;\n /** Granted category slugs from the latest `cx:consent` event. */\n granted: ConsentGranted;\n /** Raw detail of the latest `cx:consent` event, if any. */\n detail: ConsentXConsentDetail | null;\n}\n\n/**\n * The `window.ConsentX` API the widget exposes once loaded. Currently used to\n * re-open the preferences dialog.\n */\nexport interface ConsentXWidgetApi {\n open?: () => void;\n [key: string]: unknown;\n}\n\ndeclare global {\n interface Window {\n /** Carries the connected Site Key for the embed (read by embed.js). */\n consentx_key?: string;\n /** The widget API, present once embed.js has initialised. */\n ConsentX?: ConsentXWidgetApi;\n dataLayer?: unknown[];\n }\n\n interface WindowEventMap {\n 'cx:consent': CustomEvent<ConsentXConsentDetail>;\n }\n}\n","import {\n Inject,\n Injectable,\n NgZone,\n OnDestroy,\n Optional,\n PLATFORM_ID,\n} from '@angular/core';\nimport { DOCUMENT, isPlatformBrowser } from '@angular/common';\nimport { BehaviorSubject, Observable } from 'rxjs';\nimport { distinctUntilChanged, map } from 'rxjs/operators';\n\nimport { CONSENTX_CONFIG } from './consentx.tokens';\nimport {\n CONSENTX_DEFAULT_APP_URL,\n ConsentGranted,\n ConsentXConfig,\n ConsentXConsentDetail,\n ConsentXState,\n} from './consentx.types';\n\nconst SCRIPT_ID = 'consentx-embed';\nconst CONSENT_MODE_ID = 'consentx-consent-mode';\n\n/**\n * Injects the ConsentX embed script (`/api/SITE_KEY/embed.js`) into `<head>`\n * and exposes the visitor's consent state, derived from the widget's\n * `cx:consent` events.\n *\n * Site-key model (Connect spec, Model C): no redirect handshake. The Site Key\n * is supplied via configuration; this service only performs clean embed\n * injection plus a consent hook.\n *\n * The service is `providedIn: 'root'` so it is a singleton across the app. It\n * is SSR-safe: all DOM access is guarded behind `isPlatformBrowser`, so it is a\n * no-op on the server and runs once the app boots in the browser.\n */\n@Injectable({ providedIn: 'root' })\nexport class ConsentXService implements OnDestroy {\n private readonly appUrl: string;\n private readonly isBrowser: boolean;\n private listenerBound = false;\n private boundConsentListener?: (event: Event) => void;\n\n private readonly stateSubject = new BehaviorSubject<ConsentXState>({\n loaded: false,\n decided: false,\n granted: [],\n detail: null,\n });\n\n /** Full consent state as an observable. Emits an initial snapshot. */\n readonly state$: Observable<ConsentXState> = this.stateSubject.asObservable();\n\n /** Convenience stream of the granted category slugs. */\n readonly granted$: Observable<ConsentGranted> = this.state$.pipe(\n map((s) => s.granted),\n distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i])),\n );\n\n constructor(\n @Optional() @Inject(CONSENTX_CONFIG) private readonly config: ConsentXConfig | null,\n @Inject(PLATFORM_ID) platformId: object,\n @Inject(DOCUMENT) private readonly document: Document,\n private readonly zone: NgZone,\n ) {\n this.isBrowser = isPlatformBrowser(platformId);\n this.appUrl = this.normalizeAppUrl(this.config?.appUrl);\n\n if (!this.config?.siteKey) {\n // Misconfiguration is loud but non-fatal: the app keeps working without\n // the banner instead of crashing bootstrap.\n // eslint-disable-next-line no-console\n console.error(\n '[ConsentX] No siteKey provided. Pass { siteKey } to ConsentXModule.forRoot() ' +\n 'or provideConsentX(). Copy the Site Key from the ConsentX dashboard -> ' +\n 'Websites -> your site.',\n );\n }\n }\n\n /**\n * Auto-init entry point. Called once during app initialisation (via an\n * APP_INITIALIZER registered by the module/provider) unless `manualLoad` is\n * set. Binds the consent listener and injects the embed.\n */\n init(): void {\n if (!this.isBrowser) {\n return;\n }\n this.bindConsentListener();\n if (!this.config?.manualLoad) {\n this.load();\n }\n }\n\n /**\n * Inject the ConsentX embed into `<head>`. Idempotent: a second call is a\n * no-op if the script is already present. Safe to call manually when\n * `manualLoad` is enabled (e.g. after a cookie-policy route is reached).\n */\n load(): void {\n if (!this.isBrowser) {\n return;\n }\n const siteKey = this.config?.siteKey?.trim();\n if (!siteKey) {\n return;\n }\n\n this.bindConsentListener();\n\n if (this.config?.consentMode) {\n this.injectConsentModeDefaults();\n }\n\n // Expose the key the way embed.js expects (window.consentx_key), matching\n // the canonical embed contract.\n this.document.defaultView!.consentx_key = siteKey;\n\n if (this.document.getElementById(SCRIPT_ID)) {\n // Already injected — make sure the loaded flag reflects reality.\n this.patchState({ loaded: true });\n return;\n }\n\n const head = this.document.head || this.document.getElementsByTagName('head')[0];\n if (!head) {\n return;\n }\n\n const script = this.document.createElement('script');\n script.id = SCRIPT_ID;\n script.type = 'module';\n script.src = `${this.appUrl}/api/${encodeURIComponent(siteKey)}/embed.js`;\n script.setAttribute('data-consentx', siteKey);\n script.async = true;\n\n script.addEventListener('load', () => {\n this.zone.run(() => this.patchState({ loaded: true }));\n });\n script.addEventListener('error', () => {\n // eslint-disable-next-line no-console\n console.error(`[ConsentX] Failed to load embed from ${script.src}`);\n });\n\n head.appendChild(script);\n }\n\n /**\n * Open the ConsentX preferences dialog. Requires the embed to have loaded and\n * exposed `window.ConsentX`. Returns `true` if the call was dispatched.\n */\n openPreferences(): boolean {\n if (!this.isBrowser) {\n return false;\n }\n const api = this.document.defaultView?.ConsentX;\n if (api && typeof api.open === 'function') {\n api.open();\n return true;\n }\n return false;\n }\n\n /** Latest granted category slugs (synchronous snapshot). */\n getGranted(): ConsentGranted {\n return this.stateSubject.value.granted;\n }\n\n /**\n * Whether the visitor has granted a specific consent category slug, e.g.\n * `hasConsent('analytics')`.\n */\n hasConsent(slug: string): boolean {\n return this.stateSubject.value.granted.includes(slug);\n }\n\n /** Current full state snapshot (synchronous). */\n getState(): ConsentXState {\n return this.stateSubject.value;\n }\n\n ngOnDestroy(): void {\n if (this.isBrowser && this.boundConsentListener) {\n this.document.defaultView?.removeEventListener(\n 'cx:consent',\n this.boundConsentListener as EventListener,\n );\n }\n }\n\n // --- internals -----------------------------------------------------------\n\n /**\n * Print the Google Consent Mode v2 denied-by-default stub before any\n * analytics tag fires. Idempotent. Mirrors the canonical embed contract.\n */\n private injectConsentModeDefaults(): void {\n const win = this.document.defaultView;\n if (!win) {\n return;\n }\n if (this.document.getElementById(CONSENT_MODE_ID)) {\n return;\n }\n win.dataLayer = win.dataLayer || [];\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const gtag = (...args: unknown[]) => (win.dataLayer as any[]).push(args);\n gtag('consent', 'default', {\n ad_storage: 'denied',\n analytics_storage: 'denied',\n ad_user_data: 'denied',\n ad_personalization: 'denied',\n functionality_storage: 'denied',\n personalization_storage: 'denied',\n security_storage: 'granted',\n wait_for_update: 500,\n });\n // Marker so a second call (or a duplicate inline tag) is a no-op.\n const marker = this.document.createElement('script');\n marker.id = CONSENT_MODE_ID;\n marker.type = 'application/json';\n marker.textContent = '{\"consentx\":\"consent-mode-defaults\"}';\n (this.document.head || this.document.documentElement).appendChild(marker);\n }\n\n /** Subscribe to the widget's `cx:consent` events exactly once. */\n private bindConsentListener(): void {\n if (this.listenerBound || !this.isBrowser) {\n return;\n }\n const win = this.document.defaultView;\n if (!win) {\n return;\n }\n this.boundConsentListener = (event: Event) => {\n const detail = (event as CustomEvent<ConsentXConsentDetail>).detail;\n const granted = Array.isArray(detail?.granted) ? detail!.granted : [];\n // Re-enter Angular so bound templates/observables update.\n this.zone.run(() =>\n this.patchState({\n decided: true,\n granted,\n detail: detail ?? null,\n }),\n );\n };\n win.addEventListener('cx:consent', this.boundConsentListener as EventListener);\n this.listenerBound = true;\n }\n\n private patchState(partial: Partial<ConsentXState>): void {\n this.stateSubject.next({ ...this.stateSubject.value, ...partial });\n }\n\n /** Resolve + sanitise the app host (strip trailing slash). */\n private normalizeAppUrl(url?: string): string {\n const raw = (url || CONSENTX_DEFAULT_APP_URL).trim();\n return raw.replace(/\\/+$/, '');\n }\n}\n","import {\n APP_INITIALIZER,\n EnvironmentProviders,\n Provider,\n makeEnvironmentProviders,\n} from '@angular/core';\n\nimport { CONSENTX_CONFIG } from './consentx.tokens';\nimport { ConsentXService } from './consentx.service';\nimport { ConsentXConfig } from './consentx.types';\n\n/**\n * Factory used by APP_INITIALIZER. Initialising on app boot means the embed is\n * injected once the Angular application is ready (i.e. after hydration on SSR),\n * which is the SPA-safe load point the Connect spec recommends so the\n * framework's DOM reconciliation does not strip the appended\n * `#consentx-cookie-consent` div.\n */\nexport function consentxInitFactory(service: ConsentXService): () => void {\n return () => service.init();\n}\n\n/**\n * Standalone-app provider. Use in `bootstrapApplication(...)`:\n *\n * ```ts\n * bootstrapApplication(AppComponent, {\n * providers: [provideConsentX({ siteKey: 'YOUR_SITE_KEY' })],\n * });\n * ```\n */\nexport function provideConsentX(config: ConsentXConfig): EnvironmentProviders {\n return makeEnvironmentProviders([\n { provide: CONSENTX_CONFIG, useValue: config },\n ConsentXService,\n {\n provide: APP_INITIALIZER,\n multi: true,\n useFactory: consentxInitFactory,\n deps: [ConsentXService],\n },\n ]);\n}\n\n/**\n * Internal: the providers `ConsentXModule.forRoot()` contributes. Kept separate\n * so both the NgModule and the standalone API share one source of truth.\n */\nexport function consentxRootProviders(config: ConsentXConfig): Provider[] {\n return [\n { provide: CONSENTX_CONFIG, useValue: config },\n ConsentXService,\n {\n provide: APP_INITIALIZER,\n multi: true,\n useFactory: consentxInitFactory,\n deps: [ConsentXService],\n },\n ];\n}\n","import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';\n\nimport { ConsentXService } from './consentx.service';\nimport { consentxRootProviders } from './consentx.providers';\nimport { ConsentXConfig } from './consentx.types';\n\n/**\n * The ConsentX module for classic NgModule-based applications.\n *\n * Import once at the application root via `forRoot`:\n *\n * ```ts\n * @NgModule({\n * imports: [\n * BrowserModule,\n * ConsentXModule.forRoot({ siteKey: 'YOUR_SITE_KEY' }),\n * ],\n * bootstrap: [AppComponent],\n * })\n * export class AppModule {}\n * ```\n *\n * On app init the {@link ConsentXService} injects the ConsentX embed\n * (`/api/SITE_KEY/embed.js`) into `<head>` and starts tracking `cx:consent`.\n *\n * For standalone (`bootstrapApplication`) apps use `provideConsentX()` instead.\n */\n@NgModule()\nexport class ConsentXModule {\n /**\n * Guard against importing `forRoot()` more than once — it would register the\n * embed initialiser twice.\n */\n constructor(@Optional() @SkipSelf() parent?: ConsentXModule) {\n if (parent) {\n throw new Error(\n 'ConsentXModule.forRoot() was imported more than once. Import it only in ' +\n 'your root AppModule.',\n );\n }\n }\n\n /**\n * Configure ConsentX with your Site Key (and optional overrides). Returns the\n * module plus the root providers (config token, service, APP_INITIALIZER).\n */\n static forRoot(config: ConsentXConfig): ModuleWithProviders<ConsentXModule> {\n return {\n ngModule: ConsentXModule,\n providers: consentxRootProviders(config),\n };\n }\n}\n","/*\n * Public API surface of @consentx/angular.\n */\n\nexport { ConsentXModule } from './lib/consentx.module';\nexport { ConsentXService } from './lib/consentx.service';\nexport {\n provideConsentX,\n consentxInitFactory,\n consentxRootProviders,\n} from './lib/consentx.providers';\nexport { CONSENTX_CONFIG } from './lib/consentx.tokens';\nexport {\n CONSENTX_DEFAULT_APP_URL,\n ConsentXConfig,\n ConsentXState,\n ConsentGranted,\n ConsentXConsentDetail,\n ConsentXWidgetApi,\n} from './lib/consentx.types';\n","/**\n * Generated bundle index. Do not edit.\n */\n\nexport * from './public-api';\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Generated bundle index. Do not edit.
3
+ */
4
+ /// <amd-module name="@consentx/angular" />
5
+ export * from './public-api';
@@ -0,0 +1,39 @@
1
+ import { ModuleWithProviders } from '@angular/core';
2
+ import { ConsentXConfig } from './consentx.types';
3
+ import * as i0 from "@angular/core";
4
+ /**
5
+ * The ConsentX module for classic NgModule-based applications.
6
+ *
7
+ * Import once at the application root via `forRoot`:
8
+ *
9
+ * ```ts
10
+ * @NgModule({
11
+ * imports: [
12
+ * BrowserModule,
13
+ * ConsentXModule.forRoot({ siteKey: 'YOUR_SITE_KEY' }),
14
+ * ],
15
+ * bootstrap: [AppComponent],
16
+ * })
17
+ * export class AppModule {}
18
+ * ```
19
+ *
20
+ * On app init the {@link ConsentXService} injects the ConsentX embed
21
+ * (`/api/SITE_KEY/embed.js`) into `<head>` and starts tracking `cx:consent`.
22
+ *
23
+ * For standalone (`bootstrapApplication`) apps use `provideConsentX()` instead.
24
+ */
25
+ export declare class ConsentXModule {
26
+ /**
27
+ * Guard against importing `forRoot()` more than once — it would register the
28
+ * embed initialiser twice.
29
+ */
30
+ constructor(parent?: ConsentXModule);
31
+ /**
32
+ * Configure ConsentX with your Site Key (and optional overrides). Returns the
33
+ * module plus the root providers (config token, service, APP_INITIALIZER).
34
+ */
35
+ static forRoot(config: ConsentXConfig): ModuleWithProviders<ConsentXModule>;
36
+ static ɵfac: i0.ɵɵFactoryDeclaration<ConsentXModule, [{ optional: true; skipSelf: true; }]>;
37
+ static ɵmod: i0.ɵɵNgModuleDeclaration<ConsentXModule, never, never, never>;
38
+ static ɵinj: i0.ɵɵInjectorDeclaration<ConsentXModule>;
39
+ }
@@ -0,0 +1,26 @@
1
+ import { EnvironmentProviders, Provider } from '@angular/core';
2
+ import { ConsentXService } from './consentx.service';
3
+ import { ConsentXConfig } from './consentx.types';
4
+ /**
5
+ * Factory used by APP_INITIALIZER. Initialising on app boot means the embed is
6
+ * injected once the Angular application is ready (i.e. after hydration on SSR),
7
+ * which is the SPA-safe load point the Connect spec recommends so the
8
+ * framework's DOM reconciliation does not strip the appended
9
+ * `#consentx-cookie-consent` div.
10
+ */
11
+ export declare function consentxInitFactory(service: ConsentXService): () => void;
12
+ /**
13
+ * Standalone-app provider. Use in `bootstrapApplication(...)`:
14
+ *
15
+ * ```ts
16
+ * bootstrapApplication(AppComponent, {
17
+ * providers: [provideConsentX({ siteKey: 'YOUR_SITE_KEY' })],
18
+ * });
19
+ * ```
20
+ */
21
+ export declare function provideConsentX(config: ConsentXConfig): EnvironmentProviders;
22
+ /**
23
+ * Internal: the providers `ConsentXModule.forRoot()` contributes. Kept separate
24
+ * so both the NgModule and the standalone API share one source of truth.
25
+ */
26
+ export declare function consentxRootProviders(config: ConsentXConfig): Provider[];
@@ -0,0 +1,71 @@
1
+ import { NgZone, OnDestroy } from '@angular/core';
2
+ import { Observable } from 'rxjs';
3
+ import { ConsentGranted, ConsentXConfig, ConsentXState } from './consentx.types';
4
+ import * as i0 from "@angular/core";
5
+ /**
6
+ * Injects the ConsentX embed script (`/api/SITE_KEY/embed.js`) into `<head>`
7
+ * and exposes the visitor's consent state, derived from the widget's
8
+ * `cx:consent` events.
9
+ *
10
+ * Site-key model (Connect spec, Model C): no redirect handshake. The Site Key
11
+ * is supplied via configuration; this service only performs clean embed
12
+ * injection plus a consent hook.
13
+ *
14
+ * The service is `providedIn: 'root'` so it is a singleton across the app. It
15
+ * is SSR-safe: all DOM access is guarded behind `isPlatformBrowser`, so it is a
16
+ * no-op on the server and runs once the app boots in the browser.
17
+ */
18
+ export declare class ConsentXService implements OnDestroy {
19
+ private readonly config;
20
+ private readonly document;
21
+ private readonly zone;
22
+ private readonly appUrl;
23
+ private readonly isBrowser;
24
+ private listenerBound;
25
+ private boundConsentListener?;
26
+ private readonly stateSubject;
27
+ /** Full consent state as an observable. Emits an initial snapshot. */
28
+ readonly state$: Observable<ConsentXState>;
29
+ /** Convenience stream of the granted category slugs. */
30
+ readonly granted$: Observable<ConsentGranted>;
31
+ constructor(config: ConsentXConfig | null, platformId: object, document: Document, zone: NgZone);
32
+ /**
33
+ * Auto-init entry point. Called once during app initialisation (via an
34
+ * APP_INITIALIZER registered by the module/provider) unless `manualLoad` is
35
+ * set. Binds the consent listener and injects the embed.
36
+ */
37
+ init(): void;
38
+ /**
39
+ * Inject the ConsentX embed into `<head>`. Idempotent: a second call is a
40
+ * no-op if the script is already present. Safe to call manually when
41
+ * `manualLoad` is enabled (e.g. after a cookie-policy route is reached).
42
+ */
43
+ load(): void;
44
+ /**
45
+ * Open the ConsentX preferences dialog. Requires the embed to have loaded and
46
+ * exposed `window.ConsentX`. Returns `true` if the call was dispatched.
47
+ */
48
+ openPreferences(): boolean;
49
+ /** Latest granted category slugs (synchronous snapshot). */
50
+ getGranted(): ConsentGranted;
51
+ /**
52
+ * Whether the visitor has granted a specific consent category slug, e.g.
53
+ * `hasConsent('analytics')`.
54
+ */
55
+ hasConsent(slug: string): boolean;
56
+ /** Current full state snapshot (synchronous). */
57
+ getState(): ConsentXState;
58
+ ngOnDestroy(): void;
59
+ /**
60
+ * Print the Google Consent Mode v2 denied-by-default stub before any
61
+ * analytics tag fires. Idempotent. Mirrors the canonical embed contract.
62
+ */
63
+ private injectConsentModeDefaults;
64
+ /** Subscribe to the widget's `cx:consent` events exactly once. */
65
+ private bindConsentListener;
66
+ private patchState;
67
+ /** Resolve + sanitise the app host (strip trailing slash). */
68
+ private normalizeAppUrl;
69
+ static ɵfac: i0.ɵɵFactoryDeclaration<ConsentXService, [{ optional: true; }, null, null, null]>;
70
+ static ɵprov: i0.ɵɵInjectableDeclaration<ConsentXService>;
71
+ }
@@ -0,0 +1,7 @@
1
+ import { InjectionToken } from '@angular/core';
2
+ import { ConsentXConfig } from './consentx.types';
3
+ /**
4
+ * DI token carrying the resolved {@link ConsentXConfig}. Provided by
5
+ * `ConsentXModule.forRoot(...)` or `provideConsentX(...)`.
6
+ */
7
+ export declare const CONSENTX_CONFIG: InjectionToken<ConsentXConfig>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Public types for @consentx/angular.
3
+ *
4
+ * ConsentX runs on the site-key model (Model C in the Connect spec): the user
5
+ * pastes the Site Key copied from the ConsentX dashboard (Websites -> their
6
+ * site) and ensures their domain is on that site's allowlist. The embed then
7
+ * loads with no server-side handshake.
8
+ */
9
+ /** Default ConsentX application host (no trailing slash). */
10
+ export declare const CONSENTX_DEFAULT_APP_URL = "https://app.consentx.io";
11
+ /**
12
+ * Configuration passed to `ConsentXModule.forRoot(...)` (NgModule apps) or
13
+ * `provideConsentX(...)` (standalone apps).
14
+ */
15
+ export interface ConsentXConfig {
16
+ /**
17
+ * The ConsentX Site Key for this website. Copy it from the ConsentX
18
+ * dashboard -> Websites -> your site. Required.
19
+ */
20
+ siteKey: string;
21
+ /**
22
+ * Override the ConsentX app host (scheme + host, no trailing slash).
23
+ * Defaults to `https://app.consentx.io`. Use this for staging hosts such as
24
+ * `https://consentx1.satyamrastogi.com`.
25
+ */
26
+ appUrl?: string;
27
+ /**
28
+ * When `true`, the service prints the Google Consent Mode v2
29
+ * denied-by-default stub into `<head>` before the embed loads, so any
30
+ * analytics tag that fires before consent starts denied. The ConsentX
31
+ * widget emits the `gtag('consent','update',...)` calls on the visitor's
32
+ * choice. Defaults to `false`.
33
+ */
34
+ consentMode?: boolean;
35
+ /**
36
+ * Disable automatic embed injection on app init. Set this to `true` if you
37
+ * want to call `ConsentXService.load()` yourself at a custom point.
38
+ * Defaults to `false` (auto-inject on init).
39
+ */
40
+ manualLoad?: boolean;
41
+ }
42
+ /** The category slugs a visitor has granted, e.g. `['analytics','marketing']`. */
43
+ export type ConsentGranted = string[];
44
+ /**
45
+ * The detail payload of the `cx:consent` browser event emitted by the ConsentX
46
+ * widget. `granted` is the array of granted category slugs.
47
+ */
48
+ export interface ConsentXConsentDetail {
49
+ granted: ConsentGranted;
50
+ [key: string]: unknown;
51
+ }
52
+ /** Snapshot of consent state exposed by `ConsentXService.state$`. */
53
+ export interface ConsentXState {
54
+ /** Whether the embed script has loaded into the page. */
55
+ loaded: boolean;
56
+ /** Whether a `cx:consent` event has been received this session. */
57
+ decided: boolean;
58
+ /** Granted category slugs from the latest `cx:consent` event. */
59
+ granted: ConsentGranted;
60
+ /** Raw detail of the latest `cx:consent` event, if any. */
61
+ detail: ConsentXConsentDetail | null;
62
+ }
63
+ /**
64
+ * The `window.ConsentX` API the widget exposes once loaded. Currently used to
65
+ * re-open the preferences dialog.
66
+ */
67
+ export interface ConsentXWidgetApi {
68
+ open?: () => void;
69
+ [key: string]: unknown;
70
+ }
71
+ declare global {
72
+ interface Window {
73
+ /** Carries the connected Site Key for the embed (read by embed.js). */
74
+ consentx_key?: string;
75
+ /** The widget API, present once embed.js has initialised. */
76
+ ConsentX?: ConsentXWidgetApi;
77
+ dataLayer?: unknown[];
78
+ }
79
+ interface WindowEventMap {
80
+ 'cx:consent': CustomEvent<ConsentXConsentDetail>;
81
+ }
82
+ }
@@ -0,0 +1,5 @@
1
+ export { ConsentXModule } from './lib/consentx.module';
2
+ export { ConsentXService } from './lib/consentx.service';
3
+ export { provideConsentX, consentxInitFactory, consentxRootProviders, } from './lib/consentx.providers';
4
+ export { CONSENTX_CONFIG } from './lib/consentx.tokens';
5
+ export { CONSENTX_DEFAULT_APP_URL, ConsentXConfig, ConsentXState, ConsentGranted, ConsentXConsentDetail, ConsentXWidgetApi, } from './lib/consentx.types';
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "./node_modules/ng-packagr/ng-package.schema.json",
3
+ "dest": "dist",
4
+ "assets": [
5
+ "assets/consentx-logo.svg"
6
+ ],
7
+ "lib": {
8
+ "entryFile": "src/public-api.ts"
9
+ }
10
+ }