@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.
- package/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +221 -0
- package/assets/consentx-logo.svg +550 -0
- package/dist/LICENSE +21 -0
- package/dist/README.md +221 -0
- package/dist/assets/consentx-logo.svg +550 -0
- package/dist/esm2022/consentx-angular.mjs +5 -0
- package/dist/esm2022/lib/consentx.module.mjs +57 -0
- package/dist/esm2022/lib/consentx.providers.mjs +51 -0
- package/dist/esm2022/lib/consentx.service.mjs +230 -0
- package/dist/esm2022/lib/consentx.tokens.mjs +7 -0
- package/dist/esm2022/lib/consentx.types.mjs +11 -0
- package/dist/esm2022/public-api.mjs +9 -0
- package/dist/fesm2022/consentx-angular.mjs +358 -0
- package/dist/fesm2022/consentx-angular.mjs.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/lib/consentx.module.d.ts +39 -0
- package/dist/lib/consentx.providers.d.ts +26 -0
- package/dist/lib/consentx.service.d.ts +71 -0
- package/dist/lib/consentx.tokens.d.ts +7 -0
- package/dist/lib/consentx.types.d.ts +82 -0
- package/dist/public-api.d.ts +5 -0
- package/ng-package.json +10 -0
- package/package.json +53 -0
- package/src/index.ts +1 -0
- package/src/lib/consentx.module.ts +53 -0
- package/src/lib/consentx.providers.ts +60 -0
- package/src/lib/consentx.service.ts +262 -0
- package/src/lib/consentx.tokens.ts +8 -0
- package/src/lib/consentx.types.ts +93 -0
- package/src/public-api.ts +20 -0
- package/tsconfig.json +32 -0
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@consentx/angular",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ConsentX cookie consent & CMP for Angular — GDPR, CCPA, DPDPA. Inject the ConsentX embed and read consent state from a service. Site-key model, no server handshake.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"consentx",
|
|
7
|
+
"angular",
|
|
8
|
+
"cookie-consent",
|
|
9
|
+
"cmp",
|
|
10
|
+
"gdpr",
|
|
11
|
+
"ccpa",
|
|
12
|
+
"dpdpa",
|
|
13
|
+
"consent-mode",
|
|
14
|
+
"privacy"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://consentx.io/integrations/angular",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/consentx/consentx-plugins/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/consentx/consentx-plugins.git",
|
|
23
|
+
"directory": "packages/angular"
|
|
24
|
+
},
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"author": "ConsentX <hello@consentx.io> (https://consentx.io)",
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "ng-packagr -p ng-package.json",
|
|
30
|
+
"prepublishOnly": "npm run build"
|
|
31
|
+
},
|
|
32
|
+
"peerDependencies": {
|
|
33
|
+
"@angular/common": ">=15.0.0",
|
|
34
|
+
"@angular/core": ">=15.0.0",
|
|
35
|
+
"rxjs": ">=7.0.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"tslib": "^2.6.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@angular/common": "^17.3.0",
|
|
42
|
+
"@angular/compiler": "^17.3.0",
|
|
43
|
+
"@angular/compiler-cli": "^17.3.0",
|
|
44
|
+
"@angular/core": "^17.3.0",
|
|
45
|
+
"ng-packagr": "^17.3.0",
|
|
46
|
+
"rxjs": "^7.8.0",
|
|
47
|
+
"typescript": "~5.4.0",
|
|
48
|
+
"zone.js": "~0.14.0"
|
|
49
|
+
},
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './public-api';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ModuleWithProviders, NgModule, Optional, SkipSelf } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
import { ConsentXService } from './consentx.service';
|
|
4
|
+
import { consentxRootProviders } from './consentx.providers';
|
|
5
|
+
import { ConsentXConfig } from './consentx.types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The ConsentX module for classic NgModule-based applications.
|
|
9
|
+
*
|
|
10
|
+
* Import once at the application root via `forRoot`:
|
|
11
|
+
*
|
|
12
|
+
* ```ts
|
|
13
|
+
* @NgModule({
|
|
14
|
+
* imports: [
|
|
15
|
+
* BrowserModule,
|
|
16
|
+
* ConsentXModule.forRoot({ siteKey: 'YOUR_SITE_KEY' }),
|
|
17
|
+
* ],
|
|
18
|
+
* bootstrap: [AppComponent],
|
|
19
|
+
* })
|
|
20
|
+
* export class AppModule {}
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* On app init the {@link ConsentXService} injects the ConsentX embed
|
|
24
|
+
* (`/api/SITE_KEY/embed.js`) into `<head>` and starts tracking `cx:consent`.
|
|
25
|
+
*
|
|
26
|
+
* For standalone (`bootstrapApplication`) apps use `provideConsentX()` instead.
|
|
27
|
+
*/
|
|
28
|
+
@NgModule()
|
|
29
|
+
export class ConsentXModule {
|
|
30
|
+
/**
|
|
31
|
+
* Guard against importing `forRoot()` more than once — it would register the
|
|
32
|
+
* embed initialiser twice.
|
|
33
|
+
*/
|
|
34
|
+
constructor(@Optional() @SkipSelf() parent?: ConsentXModule) {
|
|
35
|
+
if (parent) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
'ConsentXModule.forRoot() was imported more than once. Import it only in ' +
|
|
38
|
+
'your root AppModule.',
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Configure ConsentX with your Site Key (and optional overrides). Returns the
|
|
45
|
+
* module plus the root providers (config token, service, APP_INITIALIZER).
|
|
46
|
+
*/
|
|
47
|
+
static forRoot(config: ConsentXConfig): ModuleWithProviders<ConsentXModule> {
|
|
48
|
+
return {
|
|
49
|
+
ngModule: ConsentXModule,
|
|
50
|
+
providers: consentxRootProviders(config),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
APP_INITIALIZER,
|
|
3
|
+
EnvironmentProviders,
|
|
4
|
+
Provider,
|
|
5
|
+
makeEnvironmentProviders,
|
|
6
|
+
} from '@angular/core';
|
|
7
|
+
|
|
8
|
+
import { CONSENTX_CONFIG } from './consentx.tokens';
|
|
9
|
+
import { ConsentXService } from './consentx.service';
|
|
10
|
+
import { ConsentXConfig } from './consentx.types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Factory used by APP_INITIALIZER. Initialising on app boot means the embed is
|
|
14
|
+
* injected once the Angular application is ready (i.e. after hydration on SSR),
|
|
15
|
+
* which is the SPA-safe load point the Connect spec recommends so the
|
|
16
|
+
* framework's DOM reconciliation does not strip the appended
|
|
17
|
+
* `#consentx-cookie-consent` div.
|
|
18
|
+
*/
|
|
19
|
+
export function consentxInitFactory(service: ConsentXService): () => void {
|
|
20
|
+
return () => service.init();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Standalone-app provider. Use in `bootstrapApplication(...)`:
|
|
25
|
+
*
|
|
26
|
+
* ```ts
|
|
27
|
+
* bootstrapApplication(AppComponent, {
|
|
28
|
+
* providers: [provideConsentX({ siteKey: 'YOUR_SITE_KEY' })],
|
|
29
|
+
* });
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function provideConsentX(config: ConsentXConfig): EnvironmentProviders {
|
|
33
|
+
return makeEnvironmentProviders([
|
|
34
|
+
{ provide: CONSENTX_CONFIG, useValue: config },
|
|
35
|
+
ConsentXService,
|
|
36
|
+
{
|
|
37
|
+
provide: APP_INITIALIZER,
|
|
38
|
+
multi: true,
|
|
39
|
+
useFactory: consentxInitFactory,
|
|
40
|
+
deps: [ConsentXService],
|
|
41
|
+
},
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Internal: the providers `ConsentXModule.forRoot()` contributes. Kept separate
|
|
47
|
+
* so both the NgModule and the standalone API share one source of truth.
|
|
48
|
+
*/
|
|
49
|
+
export function consentxRootProviders(config: ConsentXConfig): Provider[] {
|
|
50
|
+
return [
|
|
51
|
+
{ provide: CONSENTX_CONFIG, useValue: config },
|
|
52
|
+
ConsentXService,
|
|
53
|
+
{
|
|
54
|
+
provide: APP_INITIALIZER,
|
|
55
|
+
multi: true,
|
|
56
|
+
useFactory: consentxInitFactory,
|
|
57
|
+
deps: [ConsentXService],
|
|
58
|
+
},
|
|
59
|
+
];
|
|
60
|
+
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Inject,
|
|
3
|
+
Injectable,
|
|
4
|
+
NgZone,
|
|
5
|
+
OnDestroy,
|
|
6
|
+
Optional,
|
|
7
|
+
PLATFORM_ID,
|
|
8
|
+
} from '@angular/core';
|
|
9
|
+
import { DOCUMENT, isPlatformBrowser } from '@angular/common';
|
|
10
|
+
import { BehaviorSubject, Observable } from 'rxjs';
|
|
11
|
+
import { distinctUntilChanged, map } from 'rxjs/operators';
|
|
12
|
+
|
|
13
|
+
import { CONSENTX_CONFIG } from './consentx.tokens';
|
|
14
|
+
import {
|
|
15
|
+
CONSENTX_DEFAULT_APP_URL,
|
|
16
|
+
ConsentGranted,
|
|
17
|
+
ConsentXConfig,
|
|
18
|
+
ConsentXConsentDetail,
|
|
19
|
+
ConsentXState,
|
|
20
|
+
} from './consentx.types';
|
|
21
|
+
|
|
22
|
+
const SCRIPT_ID = 'consentx-embed';
|
|
23
|
+
const CONSENT_MODE_ID = 'consentx-consent-mode';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Injects the ConsentX embed script (`/api/SITE_KEY/embed.js`) into `<head>`
|
|
27
|
+
* and exposes the visitor's consent state, derived from the widget's
|
|
28
|
+
* `cx:consent` events.
|
|
29
|
+
*
|
|
30
|
+
* Site-key model (Connect spec, Model C): no redirect handshake. The Site Key
|
|
31
|
+
* is supplied via configuration; this service only performs clean embed
|
|
32
|
+
* injection plus a consent hook.
|
|
33
|
+
*
|
|
34
|
+
* The service is `providedIn: 'root'` so it is a singleton across the app. It
|
|
35
|
+
* is SSR-safe: all DOM access is guarded behind `isPlatformBrowser`, so it is a
|
|
36
|
+
* no-op on the server and runs once the app boots in the browser.
|
|
37
|
+
*/
|
|
38
|
+
@Injectable({ providedIn: 'root' })
|
|
39
|
+
export class ConsentXService implements OnDestroy {
|
|
40
|
+
private readonly appUrl: string;
|
|
41
|
+
private readonly isBrowser: boolean;
|
|
42
|
+
private listenerBound = false;
|
|
43
|
+
private boundConsentListener?: (event: Event) => void;
|
|
44
|
+
|
|
45
|
+
private readonly stateSubject = new BehaviorSubject<ConsentXState>({
|
|
46
|
+
loaded: false,
|
|
47
|
+
decided: false,
|
|
48
|
+
granted: [],
|
|
49
|
+
detail: null,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** Full consent state as an observable. Emits an initial snapshot. */
|
|
53
|
+
readonly state$: Observable<ConsentXState> = this.stateSubject.asObservable();
|
|
54
|
+
|
|
55
|
+
/** Convenience stream of the granted category slugs. */
|
|
56
|
+
readonly granted$: Observable<ConsentGranted> = this.state$.pipe(
|
|
57
|
+
map((s) => s.granted),
|
|
58
|
+
distinctUntilChanged((a, b) => a.length === b.length && a.every((v, i) => v === b[i])),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
constructor(
|
|
62
|
+
@Optional() @Inject(CONSENTX_CONFIG) private readonly config: ConsentXConfig | null,
|
|
63
|
+
@Inject(PLATFORM_ID) platformId: object,
|
|
64
|
+
@Inject(DOCUMENT) private readonly document: Document,
|
|
65
|
+
private readonly zone: NgZone,
|
|
66
|
+
) {
|
|
67
|
+
this.isBrowser = isPlatformBrowser(platformId);
|
|
68
|
+
this.appUrl = this.normalizeAppUrl(this.config?.appUrl);
|
|
69
|
+
|
|
70
|
+
if (!this.config?.siteKey) {
|
|
71
|
+
// Misconfiguration is loud but non-fatal: the app keeps working without
|
|
72
|
+
// the banner instead of crashing bootstrap.
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.error(
|
|
75
|
+
'[ConsentX] No siteKey provided. Pass { siteKey } to ConsentXModule.forRoot() ' +
|
|
76
|
+
'or provideConsentX(). Copy the Site Key from the ConsentX dashboard -> ' +
|
|
77
|
+
'Websites -> your site.',
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Auto-init entry point. Called once during app initialisation (via an
|
|
84
|
+
* APP_INITIALIZER registered by the module/provider) unless `manualLoad` is
|
|
85
|
+
* set. Binds the consent listener and injects the embed.
|
|
86
|
+
*/
|
|
87
|
+
init(): void {
|
|
88
|
+
if (!this.isBrowser) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
this.bindConsentListener();
|
|
92
|
+
if (!this.config?.manualLoad) {
|
|
93
|
+
this.load();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Inject the ConsentX embed into `<head>`. Idempotent: a second call is a
|
|
99
|
+
* no-op if the script is already present. Safe to call manually when
|
|
100
|
+
* `manualLoad` is enabled (e.g. after a cookie-policy route is reached).
|
|
101
|
+
*/
|
|
102
|
+
load(): void {
|
|
103
|
+
if (!this.isBrowser) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const siteKey = this.config?.siteKey?.trim();
|
|
107
|
+
if (!siteKey) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.bindConsentListener();
|
|
112
|
+
|
|
113
|
+
if (this.config?.consentMode) {
|
|
114
|
+
this.injectConsentModeDefaults();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Expose the key the way embed.js expects (window.consentx_key), matching
|
|
118
|
+
// the canonical embed contract.
|
|
119
|
+
this.document.defaultView!.consentx_key = siteKey;
|
|
120
|
+
|
|
121
|
+
if (this.document.getElementById(SCRIPT_ID)) {
|
|
122
|
+
// Already injected — make sure the loaded flag reflects reality.
|
|
123
|
+
this.patchState({ loaded: true });
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const head = this.document.head || this.document.getElementsByTagName('head')[0];
|
|
128
|
+
if (!head) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const script = this.document.createElement('script');
|
|
133
|
+
script.id = SCRIPT_ID;
|
|
134
|
+
script.type = 'module';
|
|
135
|
+
script.src = `${this.appUrl}/api/${encodeURIComponent(siteKey)}/embed.js`;
|
|
136
|
+
script.setAttribute('data-consentx', siteKey);
|
|
137
|
+
script.async = true;
|
|
138
|
+
|
|
139
|
+
script.addEventListener('load', () => {
|
|
140
|
+
this.zone.run(() => this.patchState({ loaded: true }));
|
|
141
|
+
});
|
|
142
|
+
script.addEventListener('error', () => {
|
|
143
|
+
// eslint-disable-next-line no-console
|
|
144
|
+
console.error(`[ConsentX] Failed to load embed from ${script.src}`);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
head.appendChild(script);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Open the ConsentX preferences dialog. Requires the embed to have loaded and
|
|
152
|
+
* exposed `window.ConsentX`. Returns `true` if the call was dispatched.
|
|
153
|
+
*/
|
|
154
|
+
openPreferences(): boolean {
|
|
155
|
+
if (!this.isBrowser) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const api = this.document.defaultView?.ConsentX;
|
|
159
|
+
if (api && typeof api.open === 'function') {
|
|
160
|
+
api.open();
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Latest granted category slugs (synchronous snapshot). */
|
|
167
|
+
getGranted(): ConsentGranted {
|
|
168
|
+
return this.stateSubject.value.granted;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Whether the visitor has granted a specific consent category slug, e.g.
|
|
173
|
+
* `hasConsent('analytics')`.
|
|
174
|
+
*/
|
|
175
|
+
hasConsent(slug: string): boolean {
|
|
176
|
+
return this.stateSubject.value.granted.includes(slug);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Current full state snapshot (synchronous). */
|
|
180
|
+
getState(): ConsentXState {
|
|
181
|
+
return this.stateSubject.value;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
ngOnDestroy(): void {
|
|
185
|
+
if (this.isBrowser && this.boundConsentListener) {
|
|
186
|
+
this.document.defaultView?.removeEventListener(
|
|
187
|
+
'cx:consent',
|
|
188
|
+
this.boundConsentListener as EventListener,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// --- internals -----------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Print the Google Consent Mode v2 denied-by-default stub before any
|
|
197
|
+
* analytics tag fires. Idempotent. Mirrors the canonical embed contract.
|
|
198
|
+
*/
|
|
199
|
+
private injectConsentModeDefaults(): void {
|
|
200
|
+
const win = this.document.defaultView;
|
|
201
|
+
if (!win) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (this.document.getElementById(CONSENT_MODE_ID)) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
win.dataLayer = win.dataLayer || [];
|
|
208
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
209
|
+
const gtag = (...args: unknown[]) => (win.dataLayer as any[]).push(args);
|
|
210
|
+
gtag('consent', 'default', {
|
|
211
|
+
ad_storage: 'denied',
|
|
212
|
+
analytics_storage: 'denied',
|
|
213
|
+
ad_user_data: 'denied',
|
|
214
|
+
ad_personalization: 'denied',
|
|
215
|
+
functionality_storage: 'denied',
|
|
216
|
+
personalization_storage: 'denied',
|
|
217
|
+
security_storage: 'granted',
|
|
218
|
+
wait_for_update: 500,
|
|
219
|
+
});
|
|
220
|
+
// Marker so a second call (or a duplicate inline tag) is a no-op.
|
|
221
|
+
const marker = this.document.createElement('script');
|
|
222
|
+
marker.id = CONSENT_MODE_ID;
|
|
223
|
+
marker.type = 'application/json';
|
|
224
|
+
marker.textContent = '{"consentx":"consent-mode-defaults"}';
|
|
225
|
+
(this.document.head || this.document.documentElement).appendChild(marker);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Subscribe to the widget's `cx:consent` events exactly once. */
|
|
229
|
+
private bindConsentListener(): void {
|
|
230
|
+
if (this.listenerBound || !this.isBrowser) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const win = this.document.defaultView;
|
|
234
|
+
if (!win) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
this.boundConsentListener = (event: Event) => {
|
|
238
|
+
const detail = (event as CustomEvent<ConsentXConsentDetail>).detail;
|
|
239
|
+
const granted = Array.isArray(detail?.granted) ? detail!.granted : [];
|
|
240
|
+
// Re-enter Angular so bound templates/observables update.
|
|
241
|
+
this.zone.run(() =>
|
|
242
|
+
this.patchState({
|
|
243
|
+
decided: true,
|
|
244
|
+
granted,
|
|
245
|
+
detail: detail ?? null,
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
};
|
|
249
|
+
win.addEventListener('cx:consent', this.boundConsentListener as EventListener);
|
|
250
|
+
this.listenerBound = true;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private patchState(partial: Partial<ConsentXState>): void {
|
|
254
|
+
this.stateSubject.next({ ...this.stateSubject.value, ...partial });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Resolve + sanitise the app host (strip trailing slash). */
|
|
258
|
+
private normalizeAppUrl(url?: string): string {
|
|
259
|
+
const raw = (url || CONSENTX_DEFAULT_APP_URL).trim();
|
|
260
|
+
return raw.replace(/\/+$/, '');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { InjectionToken } from '@angular/core';
|
|
2
|
+
import { ConsentXConfig } from './consentx.types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* DI token carrying the resolved {@link ConsentXConfig}. Provided by
|
|
6
|
+
* `ConsentXModule.forRoot(...)` or `provideConsentX(...)`.
|
|
7
|
+
*/
|
|
8
|
+
export const CONSENTX_CONFIG = new InjectionToken<ConsentXConfig>('CONSENTX_CONFIG');
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
|
|
10
|
+
/** Default ConsentX application host (no trailing slash). */
|
|
11
|
+
export const CONSENTX_DEFAULT_APP_URL = 'https://app.consentx.io';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Configuration passed to `ConsentXModule.forRoot(...)` (NgModule apps) or
|
|
15
|
+
* `provideConsentX(...)` (standalone apps).
|
|
16
|
+
*/
|
|
17
|
+
export interface ConsentXConfig {
|
|
18
|
+
/**
|
|
19
|
+
* The ConsentX Site Key for this website. Copy it from the ConsentX
|
|
20
|
+
* dashboard -> Websites -> your site. Required.
|
|
21
|
+
*/
|
|
22
|
+
siteKey: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Override the ConsentX app host (scheme + host, no trailing slash).
|
|
26
|
+
* Defaults to `https://app.consentx.io`. Use this for staging hosts such as
|
|
27
|
+
* `https://consentx1.satyamrastogi.com`.
|
|
28
|
+
*/
|
|
29
|
+
appUrl?: string;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* When `true`, the service prints the Google Consent Mode v2
|
|
33
|
+
* denied-by-default stub into `<head>` before the embed loads, so any
|
|
34
|
+
* analytics tag that fires before consent starts denied. The ConsentX
|
|
35
|
+
* widget emits the `gtag('consent','update',...)` calls on the visitor's
|
|
36
|
+
* choice. Defaults to `false`.
|
|
37
|
+
*/
|
|
38
|
+
consentMode?: boolean;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Disable automatic embed injection on app init. Set this to `true` if you
|
|
42
|
+
* want to call `ConsentXService.load()` yourself at a custom point.
|
|
43
|
+
* Defaults to `false` (auto-inject on init).
|
|
44
|
+
*/
|
|
45
|
+
manualLoad?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The category slugs a visitor has granted, e.g. `['analytics','marketing']`. */
|
|
49
|
+
export type ConsentGranted = string[];
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The detail payload of the `cx:consent` browser event emitted by the ConsentX
|
|
53
|
+
* widget. `granted` is the array of granted category slugs.
|
|
54
|
+
*/
|
|
55
|
+
export interface ConsentXConsentDetail {
|
|
56
|
+
granted: ConsentGranted;
|
|
57
|
+
[key: string]: unknown;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Snapshot of consent state exposed by `ConsentXService.state$`. */
|
|
61
|
+
export interface ConsentXState {
|
|
62
|
+
/** Whether the embed script has loaded into the page. */
|
|
63
|
+
loaded: boolean;
|
|
64
|
+
/** Whether a `cx:consent` event has been received this session. */
|
|
65
|
+
decided: boolean;
|
|
66
|
+
/** Granted category slugs from the latest `cx:consent` event. */
|
|
67
|
+
granted: ConsentGranted;
|
|
68
|
+
/** Raw detail of the latest `cx:consent` event, if any. */
|
|
69
|
+
detail: ConsentXConsentDetail | null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The `window.ConsentX` API the widget exposes once loaded. Currently used to
|
|
74
|
+
* re-open the preferences dialog.
|
|
75
|
+
*/
|
|
76
|
+
export interface ConsentXWidgetApi {
|
|
77
|
+
open?: () => void;
|
|
78
|
+
[key: string]: unknown;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
declare global {
|
|
82
|
+
interface Window {
|
|
83
|
+
/** Carries the connected Site Key for the embed (read by embed.js). */
|
|
84
|
+
consentx_key?: string;
|
|
85
|
+
/** The widget API, present once embed.js has initialised. */
|
|
86
|
+
ConsentX?: ConsentXWidgetApi;
|
|
87
|
+
dataLayer?: unknown[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface WindowEventMap {
|
|
91
|
+
'cx:consent': CustomEvent<ConsentXConsentDetail>;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Public API surface of @consentx/angular.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export { ConsentXModule } from './lib/consentx.module';
|
|
6
|
+
export { ConsentXService } from './lib/consentx.service';
|
|
7
|
+
export {
|
|
8
|
+
provideConsentX,
|
|
9
|
+
consentxInitFactory,
|
|
10
|
+
consentxRootProviders,
|
|
11
|
+
} from './lib/consentx.providers';
|
|
12
|
+
export { CONSENTX_CONFIG } from './lib/consentx.tokens';
|
|
13
|
+
export {
|
|
14
|
+
CONSENTX_DEFAULT_APP_URL,
|
|
15
|
+
ConsentXConfig,
|
|
16
|
+
ConsentXState,
|
|
17
|
+
ConsentGranted,
|
|
18
|
+
ConsentXConsentDetail,
|
|
19
|
+
ConsentXWidgetApi,
|
|
20
|
+
} from './lib/consentx.types';
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"outDir": "./dist/out-tsc",
|
|
5
|
+
"declaration": true,
|
|
6
|
+
"declarationMap": true,
|
|
7
|
+
"experimentalDecorators": true,
|
|
8
|
+
"emitDecoratorMetadata": true,
|
|
9
|
+
"moduleResolution": "node",
|
|
10
|
+
"importHelpers": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noImplicitOverride": true,
|
|
13
|
+
"noImplicitReturns": true,
|
|
14
|
+
"noFallthroughCasesInSwitch": true,
|
|
15
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"esModuleInterop": true,
|
|
19
|
+
"module": "ES2022",
|
|
20
|
+
"target": "ES2022",
|
|
21
|
+
"useDefineForClassFields": false,
|
|
22
|
+
"lib": ["ES2022", "dom"]
|
|
23
|
+
},
|
|
24
|
+
"angularCompilerOptions": {
|
|
25
|
+
"enableI18nLegacyMessageIdFormat": false,
|
|
26
|
+
"strictInjectionParameters": true,
|
|
27
|
+
"strictInputAccessModifiers": true,
|
|
28
|
+
"strictTemplates": true,
|
|
29
|
+
"compilationMode": "partial"
|
|
30
|
+
},
|
|
31
|
+
"files": ["src/public-api.ts"]
|
|
32
|
+
}
|