@osano/osano-cmp-react-native 0.18.1 → 0.18.3
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/lib/commonjs/ConsentManager.js +12 -10
- package/lib/commonjs/ConsentManager.js.map +1 -1
- package/lib/commonjs/context.js +1 -0
- package/lib/commonjs/context.js.map +1 -1
- package/lib/commonjs/index.js +4 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/services/consent.js +111 -39
- package/lib/commonjs/services/consent.js.map +1 -1
- package/lib/module/ConsentManager.js +13 -11
- package/lib/module/ConsentManager.js.map +1 -1
- package/lib/module/context.js +1 -0
- package/lib/module/context.js.map +1 -1
- package/lib/module/index.js +4 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/services/consent.js +107 -37
- package/lib/module/services/consent.js.map +1 -1
- package/lib/typescript/src/ConsentManager.d.ts +6 -0
- package/lib/typescript/src/ConsentManager.d.ts.map +1 -1
- package/lib/typescript/src/context.d.ts +1 -0
- package/lib/typescript/src/context.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/services/consent.d.ts +6 -11
- package/lib/typescript/src/services/consent.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/ConsentManager.ts +13 -18
- package/src/context.ts +2 -0
- package/src/index.tsx +5 -0
- package/src/services/consent.ts +176 -39
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@osano/osano-cmp-react-native",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.3",
|
|
4
4
|
"description": "Package and example for the react-native Osano CMP SDK",
|
|
5
5
|
"main": "lib/commonjs/index",
|
|
6
6
|
"module": "lib/module/index",
|
|
@@ -31,9 +31,9 @@
|
|
|
31
31
|
],
|
|
32
32
|
"scripts": {
|
|
33
33
|
"example": "yarn workspace osano-cmp-sdk-example",
|
|
34
|
-
"test": "jest src/services/__tests__/consent.test.ts src/__tests__/index.test.tsx src/services/__tests__/google-consent-mode.test.ts src/__tests__/OsanoLogger.test.ts src/__tests__/constants.test.ts src/__tests__/reducer.test.ts src/__tests__/selectors.test.ts src/__tests__/ConsentManager.test.ts src/__tests__/ConsentUiBuilder.test.tsx src/utils/__tests__/WebStorage.test.ts src/native/__tests__/OsanoCmp.test.ts",
|
|
35
|
-
"test:legacy": "REACT_NATIVE_NEW_ARCH_ENABLED=false jest src/services/__tests__/consent.test.ts src/__tests__/index.test.tsx src/services/__tests__/google-consent-mode.test.ts src/__tests__/OsanoLogger.test.ts src/__tests__/constants.test.ts src/__tests__/reducer.test.ts src/__tests__/selectors.test.ts src/__tests__/ConsentManager.test.ts src/__tests__/ConsentUiBuilder.test.tsx src/utils/__tests__/WebStorage.test.ts src/native/__tests__/OsanoCmp.test.ts",
|
|
36
|
-
"test:newarch": "REACT_NATIVE_NEW_ARCH_ENABLED=true jest src/services/__tests__/consent.test.ts src/__tests__/index.test.tsx src/services/__tests__/google-consent-mode.test.ts src/__tests__/OsanoLogger.test.ts src/__tests__/constants.test.ts src/__tests__/reducer.test.ts src/__tests__/selectors.test.ts src/__tests__/ConsentManager.test.ts src/__tests__/ConsentUiBuilder.test.tsx src/utils/__tests__/WebStorage.test.ts src/native/__tests__/OsanoCmp.test.ts",
|
|
34
|
+
"test": "jest src/services/__tests__/consent.test.ts src/services/__tests__/fetchConfig.test.ts src/__tests__/index.test.tsx src/services/__tests__/google-consent-mode.test.ts src/__tests__/OsanoLogger.test.ts src/__tests__/constants.test.ts src/__tests__/reducer.test.ts src/__tests__/selectors.test.ts src/__tests__/ConsentManager.test.ts src/__tests__/ConsentUiBuilder.test.tsx src/utils/__tests__/WebStorage.test.ts src/native/__tests__/OsanoCmp.test.ts",
|
|
35
|
+
"test:legacy": "REACT_NATIVE_NEW_ARCH_ENABLED=false jest src/services/__tests__/consent.test.ts src/services/__tests__/fetchConfig.test.ts src/__tests__/index.test.tsx src/services/__tests__/google-consent-mode.test.ts src/__tests__/OsanoLogger.test.ts src/__tests__/constants.test.ts src/__tests__/reducer.test.ts src/__tests__/selectors.test.ts src/__tests__/ConsentManager.test.ts src/__tests__/ConsentUiBuilder.test.tsx src/utils/__tests__/WebStorage.test.ts src/native/__tests__/OsanoCmp.test.ts",
|
|
36
|
+
"test:newarch": "REACT_NATIVE_NEW_ARCH_ENABLED=true jest src/services/__tests__/consent.test.ts src/services/__tests__/fetchConfig.test.ts src/__tests__/index.test.tsx src/services/__tests__/google-consent-mode.test.ts src/__tests__/OsanoLogger.test.ts src/__tests__/constants.test.ts src/__tests__/reducer.test.ts src/__tests__/selectors.test.ts src/__tests__/ConsentManager.test.ts src/__tests__/ConsentUiBuilder.test.tsx src/utils/__tests__/WebStorage.test.ts src/native/__tests__/OsanoCmp.test.ts",
|
|
37
37
|
"test:google-consent-mode": "jest src/services/__tests__/google-consent-mode.test.ts",
|
|
38
38
|
"typecheck": "tsc --noEmit",
|
|
39
39
|
"lint": "eslint",
|
package/src/ConsentManager.ts
CHANGED
|
@@ -28,9 +28,6 @@ import {
|
|
|
28
28
|
clearConsent as clearStoredConsent,
|
|
29
29
|
fetchConfig,
|
|
30
30
|
fetchConsent,
|
|
31
|
-
getStoredConfig,
|
|
32
|
-
hasStaleConfig,
|
|
33
|
-
storeConfig,
|
|
34
31
|
} from './services/consent';
|
|
35
32
|
import { GoogleConsentModeMapper } from './services/google-consent-mode-mapper';
|
|
36
33
|
import type { OsanoOverrides } from './types';
|
|
@@ -153,17 +150,6 @@ async function fetchConfigWithRetries(
|
|
|
153
150
|
retries: number = 3,
|
|
154
151
|
delay: number = 500,
|
|
155
152
|
): Promise<Config | null> {
|
|
156
|
-
const isStale = await hasStaleConfig(configId);
|
|
157
|
-
|
|
158
|
-
if (!isStale) {
|
|
159
|
-
// If the config is not stale, we can return the cached config
|
|
160
|
-
const cachedConfig = await getStoredConfig(configId);
|
|
161
|
-
|
|
162
|
-
if (cachedConfig) {
|
|
163
|
-
return cachedConfig;
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
153
|
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
168
154
|
try {
|
|
169
155
|
const config = await fetchConfig(customerId, configId);
|
|
@@ -221,7 +207,6 @@ export class ConsentConfig extends EventEmitter {
|
|
|
221
207
|
config.palette,
|
|
222
208
|
) as Palette;
|
|
223
209
|
this._config = merge(this._config, config) as Config;
|
|
224
|
-
storeConfig(this._configId, this._config);
|
|
225
210
|
this.emit('loadSuccess', this._config);
|
|
226
211
|
})
|
|
227
212
|
.catch((err) => {
|
|
@@ -256,10 +241,11 @@ export class ConsentConfig extends EventEmitter {
|
|
|
256
241
|
return [...new Set(categories)];
|
|
257
242
|
}
|
|
258
243
|
get jurisdiction(): string {
|
|
244
|
+
// Lowercased to match the web SDK (Osano.cm.jurisdiction), e.g. "us" / "us-ca".
|
|
245
|
+
const composite = `${this.country}${this.region ? `-${this.region}` : ''}`;
|
|
259
246
|
return (
|
|
260
|
-
mappedJurisdictions[`${this.country}`.toLowerCase()] ||
|
|
261
|
-
|
|
262
|
-
);
|
|
247
|
+
mappedJurisdictions[`${this.country}`.toLowerCase()] || composite
|
|
248
|
+
).toLowerCase();
|
|
263
249
|
}
|
|
264
250
|
get ccpaRelaxed(): boolean {
|
|
265
251
|
return this._config.ccpaRelaxed;
|
|
@@ -663,6 +649,15 @@ export class ConsentManager {
|
|
|
663
649
|
return this._config;
|
|
664
650
|
}
|
|
665
651
|
|
|
652
|
+
/**
|
|
653
|
+
* The visitor's jurisdiction as a lowercase composite of country and region,
|
|
654
|
+
* e.g. "us" or "us-ca". Mirrors the web SDK's `Osano.cm.jurisdiction` and the
|
|
655
|
+
* Android SDK's `getJurisdiction()`.
|
|
656
|
+
*/
|
|
657
|
+
public get jurisdiction(): string {
|
|
658
|
+
return this._config.jurisdiction;
|
|
659
|
+
}
|
|
660
|
+
|
|
666
661
|
constructor(options?: OsanoOverrides, extUsrData?: string) {
|
|
667
662
|
const { locale, flavor, iabEnabled, iabFrameworks, iabVersions, palette } =
|
|
668
663
|
options || {};
|
package/src/context.ts
CHANGED
|
@@ -24,6 +24,7 @@ type OsanoPublicContextType = {
|
|
|
24
24
|
category?: ConsentCategoryType,
|
|
25
25
|
) => boolean | ConsentCategoryType[];
|
|
26
26
|
getDeviceId: () => string;
|
|
27
|
+
getJurisdiction: () => string;
|
|
27
28
|
clearConsent: () => void;
|
|
28
29
|
consent: (
|
|
29
30
|
consent?: ConsentCategoryType | ConsentCategoryType[],
|
|
@@ -63,6 +64,7 @@ export const OsanoPublicContext = React.createContext<OsanoPublicContextType>({
|
|
|
63
64
|
return false;
|
|
64
65
|
},
|
|
65
66
|
getDeviceId: () => '',
|
|
67
|
+
getJurisdiction: () => '',
|
|
66
68
|
consent: () => Promise.resolve([ConsentCategory.ESSENTIAL]),
|
|
67
69
|
clearConsent: noop,
|
|
68
70
|
setLocale: noop,
|
package/src/index.tsx
CHANGED
|
@@ -157,11 +157,16 @@ export const Osano: React.FunctionComponent<
|
|
|
157
157
|
return consentManager.current.deviceId;
|
|
158
158
|
}, []);
|
|
159
159
|
|
|
160
|
+
const handleGetJurisdiction = useCallback(() => {
|
|
161
|
+
return consentManager.current.jurisdiction;
|
|
162
|
+
}, []);
|
|
163
|
+
|
|
160
164
|
const context = useMemo(() => {
|
|
161
165
|
return {
|
|
162
166
|
locale: consentManager.current.translator.locale,
|
|
163
167
|
getConsent: handleGetConsent,
|
|
164
168
|
getDeviceId: handleGetDeviceId,
|
|
169
|
+
getJurisdiction: handleGetJurisdiction,
|
|
165
170
|
consent: handleRecordConsent,
|
|
166
171
|
clearConsent: handleClearConsent,
|
|
167
172
|
setLocale: handleSetLocale,
|
package/src/services/consent.ts
CHANGED
|
@@ -422,35 +422,17 @@ export const fetchConsent = async (cmp: ConsentManager) => {
|
|
|
422
422
|
}
|
|
423
423
|
};
|
|
424
424
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
.then((response) => {
|
|
428
|
-
const { headers, json } = response as {
|
|
429
|
-
json: Config;
|
|
430
|
-
headers: Headers;
|
|
431
|
-
};
|
|
432
|
-
return merge(
|
|
433
|
-
{
|
|
434
|
-
...json,
|
|
435
|
-
country: headers.get('x-osano-country'),
|
|
436
|
-
region: headers.get('x-osano-region'),
|
|
437
|
-
variant: headers.get('x-osano-variant'),
|
|
438
|
-
gdprApplies: headers.get('x-osano-gdpr-applies') === 'true',
|
|
439
|
-
},
|
|
440
|
-
overrideConfig,
|
|
441
|
-
) as Config;
|
|
442
|
-
})
|
|
443
|
-
.catch(() => {
|
|
444
|
-
// Consent not found or network/server error
|
|
445
|
-
throw new Error('Error fetching config');
|
|
446
|
-
});
|
|
447
|
-
};
|
|
425
|
+
const configCacheKey = (customerId: string, configId: string, suffix: string) =>
|
|
426
|
+
`${STORAGE_KEY}_${customerId}_${configId}_${suffix}`;
|
|
448
427
|
|
|
449
428
|
export const getStoredConfig = async (
|
|
429
|
+
customerId: string,
|
|
450
430
|
configId: string,
|
|
451
431
|
): Promise<Config | null> => {
|
|
452
432
|
try {
|
|
453
|
-
const config = await getStorage(
|
|
433
|
+
const config = await getStorage(
|
|
434
|
+
configCacheKey(customerId, configId, 'config'),
|
|
435
|
+
);
|
|
454
436
|
if (config) {
|
|
455
437
|
return JSON.parse(config) as Config;
|
|
456
438
|
}
|
|
@@ -461,14 +443,18 @@ export const getStoredConfig = async (
|
|
|
461
443
|
};
|
|
462
444
|
|
|
463
445
|
export const storeConfig = async (
|
|
446
|
+
customerId: string,
|
|
464
447
|
configId: string,
|
|
465
448
|
config: Config,
|
|
466
449
|
): Promise<void> => {
|
|
467
450
|
try {
|
|
468
451
|
await Promise.all([
|
|
469
|
-
setStorage(`${STORAGE_KEY}_${configId}_config`, JSON.stringify(config)),
|
|
470
452
|
setStorage(
|
|
471
|
-
|
|
453
|
+
configCacheKey(customerId, configId, 'config'),
|
|
454
|
+
JSON.stringify(config),
|
|
455
|
+
),
|
|
456
|
+
setStorage(
|
|
457
|
+
configCacheKey(customerId, configId, 'config_last_stored_at'),
|
|
472
458
|
JSON.stringify(new Date()),
|
|
473
459
|
),
|
|
474
460
|
]);
|
|
@@ -478,11 +464,12 @@ export const storeConfig = async (
|
|
|
478
464
|
};
|
|
479
465
|
|
|
480
466
|
export const getConfigLastStoredAt = async (
|
|
467
|
+
customerId: string,
|
|
481
468
|
configId: string,
|
|
482
469
|
): Promise<Date | null> => {
|
|
483
470
|
try {
|
|
484
471
|
const lastStoredAt = await getStorage(
|
|
485
|
-
|
|
472
|
+
configCacheKey(customerId, configId, 'config_last_stored_at'),
|
|
486
473
|
);
|
|
487
474
|
if (lastStoredAt) {
|
|
488
475
|
return new Date(JSON.parse(lastStoredAt));
|
|
@@ -493,20 +480,170 @@ export const getConfigLastStoredAt = async (
|
|
|
493
480
|
return null;
|
|
494
481
|
};
|
|
495
482
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
* @returns {Promise<boolean>}
|
|
502
|
-
*/
|
|
503
|
-
export const hasStaleConfig = async (configId: string): Promise<boolean> => {
|
|
504
|
-
const lastStoredAt = await getConfigLastStoredAt(configId);
|
|
483
|
+
export const hasStaleConfig = async (
|
|
484
|
+
customerId: string,
|
|
485
|
+
configId: string,
|
|
486
|
+
): Promise<boolean> => {
|
|
487
|
+
const lastStoredAt = await getConfigLastStoredAt(customerId, configId);
|
|
505
488
|
if (!lastStoredAt) {
|
|
506
489
|
return true;
|
|
507
490
|
}
|
|
508
491
|
const now = new Date();
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
492
|
+
// Treat clock skew (future timestamp) as expired so we force a full refetch
|
|
493
|
+
if (lastStoredAt.getTime() > now.getTime()) {
|
|
494
|
+
return true;
|
|
495
|
+
}
|
|
496
|
+
return (
|
|
497
|
+
lastStoredAt.getFullYear() !== now.getFullYear() ||
|
|
498
|
+
lastStoredAt.getMonth() !== now.getMonth() ||
|
|
499
|
+
lastStoredAt.getDate() !== now.getDate()
|
|
500
|
+
);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
export const getStoredEtag = async (
|
|
504
|
+
customerId: string,
|
|
505
|
+
configId: string,
|
|
506
|
+
): Promise<string | null> => {
|
|
507
|
+
try {
|
|
508
|
+
return (
|
|
509
|
+
(await getStorage(configCacheKey(customerId, configId, 'config_etag'))) ??
|
|
510
|
+
null
|
|
511
|
+
);
|
|
512
|
+
} catch {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
export const getStoredLastModified = async (
|
|
518
|
+
customerId: string,
|
|
519
|
+
configId: string,
|
|
520
|
+
): Promise<string | null> => {
|
|
521
|
+
try {
|
|
522
|
+
return (
|
|
523
|
+
(await getStorage(
|
|
524
|
+
configCacheKey(customerId, configId, 'config_last_modified'),
|
|
525
|
+
)) ?? null
|
|
526
|
+
);
|
|
527
|
+
} catch {
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
export const fetchConfig = async (
|
|
533
|
+
customerId: string,
|
|
534
|
+
configId: string,
|
|
535
|
+
): Promise<Config> => {
|
|
536
|
+
const [cachedConfig, stale, storedEtag, storedLastModified] =
|
|
537
|
+
await Promise.all([
|
|
538
|
+
getStoredConfig(customerId, configId),
|
|
539
|
+
hasStaleConfig(customerId, configId),
|
|
540
|
+
getStoredEtag(customerId, configId),
|
|
541
|
+
getStoredLastModified(customerId, configId),
|
|
542
|
+
]);
|
|
543
|
+
|
|
544
|
+
const conditionalHeaders: Record<string, string> = {};
|
|
545
|
+
if (cachedConfig && !stale) {
|
|
546
|
+
if (storedEtag) conditionalHeaders['If-None-Match'] = storedEtag;
|
|
547
|
+
if (storedLastModified)
|
|
548
|
+
conditionalHeaders['If-Modified-Since'] = storedLastModified;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
try {
|
|
552
|
+
const response = await fetch(
|
|
553
|
+
`${OSANO_ORIGIN}/${customerId}/${configId}/config.json`,
|
|
554
|
+
{
|
|
555
|
+
method: 'GET',
|
|
556
|
+
headers: {
|
|
557
|
+
Accept: 'application/json',
|
|
558
|
+
'Content-Type': 'application/json',
|
|
559
|
+
'Cache-Control': 'no-cache',
|
|
560
|
+
...conditionalHeaders,
|
|
561
|
+
},
|
|
562
|
+
mode: 'cors',
|
|
563
|
+
referrer: 'no-referrer',
|
|
564
|
+
},
|
|
565
|
+
);
|
|
566
|
+
|
|
567
|
+
if (response.status === 304) {
|
|
568
|
+
if (!cachedConfig) {
|
|
569
|
+
throw new Error('304 with no cached config');
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// 304 may still carry updated geo/jurisdiction headers (e.g. user
|
|
573
|
+
// moved between regions). Apply them on top of the cached body, but
|
|
574
|
+
// fall back to the previously-cached values when a header is absent
|
|
575
|
+
// rather than the first-fetch defaults.
|
|
576
|
+
const variant =
|
|
577
|
+
response.headers.get('x-osano-variant') ?? cachedConfig.variant;
|
|
578
|
+
const country =
|
|
579
|
+
response.headers.get('x-osano-country') ?? cachedConfig.country;
|
|
580
|
+
const region =
|
|
581
|
+
response.headers.get('x-osano-region') ?? cachedConfig.region;
|
|
582
|
+
const gdprAppliesHeader = response.headers.get('x-osano-gdpr-applies');
|
|
583
|
+
const gdprApplies =
|
|
584
|
+
gdprAppliesHeader != null
|
|
585
|
+
? gdprAppliesHeader === 'true'
|
|
586
|
+
: cachedConfig.gdprApplies;
|
|
587
|
+
|
|
588
|
+
const updated = merge(
|
|
589
|
+
{ ...cachedConfig, country, region, variant, gdprApplies },
|
|
590
|
+
overrideConfig,
|
|
591
|
+
) as Config;
|
|
592
|
+
|
|
593
|
+
// Persist the body update without bumping lastUsage — TTL stays
|
|
594
|
+
// anchored to the last 200 response.
|
|
595
|
+
await setStorage(
|
|
596
|
+
configCacheKey(customerId, configId, 'config'),
|
|
597
|
+
JSON.stringify(updated),
|
|
598
|
+
);
|
|
599
|
+
|
|
600
|
+
return updated;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
if (!response.ok) {
|
|
604
|
+
throw response;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
const json = (await response.json()) as Config;
|
|
608
|
+
|
|
609
|
+
const variant = response.headers.get('x-osano-variant') ?? 'two';
|
|
610
|
+
const country = response.headers.get('x-osano-country') ?? 'us';
|
|
611
|
+
const region = response.headers.get('x-osano-region') ?? '';
|
|
612
|
+
const gdprApplies = response.headers.get('x-osano-gdpr-applies') === 'true';
|
|
613
|
+
const newEtag = response.headers.get('etag');
|
|
614
|
+
const newLastModified = response.headers.get('last-modified');
|
|
615
|
+
|
|
616
|
+
const config = merge(
|
|
617
|
+
{ ...json, country, region, variant, gdprApplies },
|
|
618
|
+
overrideConfig,
|
|
619
|
+
) as Config;
|
|
620
|
+
|
|
621
|
+
const writes: Promise<unknown>[] = [
|
|
622
|
+
storeConfig(customerId, configId, config),
|
|
623
|
+
];
|
|
624
|
+
if (newEtag) {
|
|
625
|
+
writes.push(
|
|
626
|
+
setStorage(
|
|
627
|
+
configCacheKey(customerId, configId, 'config_etag'),
|
|
628
|
+
newEtag,
|
|
629
|
+
),
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
if (newLastModified) {
|
|
633
|
+
writes.push(
|
|
634
|
+
setStorage(
|
|
635
|
+
configCacheKey(customerId, configId, 'config_last_modified'),
|
|
636
|
+
newLastModified,
|
|
637
|
+
),
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
await Promise.all(writes);
|
|
641
|
+
|
|
642
|
+
return config;
|
|
643
|
+
} catch {
|
|
644
|
+
if (cachedConfig) {
|
|
645
|
+
return cachedConfig;
|
|
646
|
+
}
|
|
647
|
+
throw new Error('Error fetching config');
|
|
648
|
+
}
|
|
512
649
|
};
|