@osano/osano-cmp-react-native 0.18.6 → 0.19.1
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 +32 -1
- package/lib/commonjs/ConsentManager.js.map +1 -1
- package/lib/commonjs/components/PoweredByOsano.js +72 -0
- package/lib/commonjs/components/PoweredByOsano.js.map +1 -0
- package/lib/commonjs/iab/tcf/iab-storage-utils.js +53 -49
- package/lib/commonjs/iab/tcf/iab-storage-utils.js.map +1 -1
- package/lib/commonjs/iab/tcf/middleware.js +20 -23
- package/lib/commonjs/iab/tcf/middleware.js.map +1 -1
- package/lib/commonjs/iab/tcf/reducer.js +0 -22
- package/lib/commonjs/iab/tcf/reducer.js.map +1 -1
- package/lib/commonjs/index.js +16 -2
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/reducer.js +5 -1
- package/lib/commonjs/reducer.js.map +1 -1
- package/lib/commonjs/services/consent.js +178 -77
- package/lib/commonjs/services/consent.js.map +1 -1
- package/lib/commonjs/views/Dialog/index.js +15 -2
- package/lib/commonjs/views/Dialog/index.js.map +1 -1
- package/lib/commonjs/views/Drawer/Drawer.ConsentCategories.js +160 -12
- package/lib/commonjs/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
- package/lib/module/ConsentManager.js +32 -1
- package/lib/module/ConsentManager.js.map +1 -1
- package/lib/module/components/PoweredByOsano.js +68 -0
- package/lib/module/components/PoweredByOsano.js.map +1 -0
- package/lib/module/iab/tcf/iab-storage-utils.js +53 -49
- package/lib/module/iab/tcf/iab-storage-utils.js.map +1 -1
- package/lib/module/iab/tcf/middleware.js +22 -25
- package/lib/module/iab/tcf/middleware.js.map +1 -1
- package/lib/module/iab/tcf/reducer.js +1 -23
- package/lib/module/iab/tcf/reducer.js.map +1 -1
- package/lib/module/index.js +17 -3
- package/lib/module/index.js.map +1 -1
- package/lib/module/reducer.js +5 -1
- package/lib/module/reducer.js.map +1 -1
- package/lib/module/services/consent.js +174 -75
- package/lib/module/services/consent.js.map +1 -1
- package/lib/module/views/Dialog/index.js +16 -3
- package/lib/module/views/Dialog/index.js.map +1 -1
- package/lib/module/views/Drawer/Drawer.ConsentCategories.js +162 -14
- package/lib/module/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
- package/lib/typescript/src/ConsentManager.d.ts +13 -1
- package/lib/typescript/src/ConsentManager.d.ts.map +1 -1
- package/lib/typescript/src/components/PoweredByOsano.d.ts +5 -0
- package/lib/typescript/src/components/PoweredByOsano.d.ts.map +1 -0
- package/lib/typescript/src/iab/tcf/iab-storage-utils.d.ts +7 -7
- package/lib/typescript/src/iab/tcf/iab-storage-utils.d.ts.map +1 -1
- package/lib/typescript/src/iab/tcf/middleware.d.ts.map +1 -1
- package/lib/typescript/src/iab/tcf/reducer.d.ts +1 -0
- package/lib/typescript/src/iab/tcf/reducer.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/reducer.d.ts.map +1 -1
- package/lib/typescript/src/services/consent.d.ts +28 -0
- package/lib/typescript/src/services/consent.d.ts.map +1 -1
- package/lib/typescript/src/views/Dialog/index.d.ts.map +1 -1
- package/lib/typescript/src/views/Drawer/Drawer.ConsentCategories.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/ConsentManager.ts +33 -2
- package/src/components/PoweredByOsano.tsx +79 -0
- package/src/iab/tcf/iab-storage-utils.ts +101 -56
- package/src/iab/tcf/middleware.ts +36 -32
- package/src/iab/tcf/reducer.ts +2 -29
- package/src/index.tsx +15 -1
- package/src/reducer.ts +4 -0
- package/src/services/consent.ts +220 -86
- package/src/types/consent-config.d.ts +11 -1
- package/src/types/store.d.ts +5 -0
- package/src/views/Dialog/index.tsx +19 -1
- package/src/views/Drawer/Drawer.ConsentCategories.tsx +168 -11
package/src/services/consent.ts
CHANGED
|
@@ -94,6 +94,36 @@ const lastConsentDateKey = (configId: string, userSuffix: string) =>
|
|
|
94
94
|
const legacyLastConsentDateKey = (userSuffix: string) =>
|
|
95
95
|
`${STORAGE_KEY}_last_recorded_consent_date${userSuffix}`;
|
|
96
96
|
|
|
97
|
+
const parseStoredTimestamp = (json: string | null | undefined): string => {
|
|
98
|
+
if (!json) {
|
|
99
|
+
return '';
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return JSON.parse(json) as string;
|
|
103
|
+
} catch {
|
|
104
|
+
return '';
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const retrieveTimestampForSuffix = async (
|
|
109
|
+
configId: string,
|
|
110
|
+
userSuffix: string,
|
|
111
|
+
): Promise<string> => {
|
|
112
|
+
try {
|
|
113
|
+
const scoped = parseStoredTimestamp(
|
|
114
|
+
await getStorage(lastConsentDateKey(configId, userSuffix)),
|
|
115
|
+
);
|
|
116
|
+
if (scoped) {
|
|
117
|
+
return scoped;
|
|
118
|
+
}
|
|
119
|
+
return parseStoredTimestamp(
|
|
120
|
+
await getStorage(legacyLastConsentDateKey(userSuffix)),
|
|
121
|
+
);
|
|
122
|
+
} catch {
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
97
127
|
function consentRecordedRemotely(
|
|
98
128
|
configId: string,
|
|
99
129
|
success: boolean,
|
|
@@ -208,48 +238,47 @@ export const migrateConsentStorage = async (
|
|
|
208
238
|
return null;
|
|
209
239
|
}
|
|
210
240
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
241
|
+
if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
214
244
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (oldConsent) {
|
|
221
|
-
let consent = JSON.parse(oldConsent) as ConsentCategoryType[];
|
|
222
|
-
if (consent.length > 0) {
|
|
223
|
-
consent = formatConsentArray(
|
|
224
|
-
normalizeDoNotSellConsent(
|
|
225
|
-
makeConsentFromCategories(consent),
|
|
226
|
-
{
|
|
227
|
-
enableDoNotSell: cmp.config.enableDoNotSell,
|
|
228
|
-
doNotSellCategories: cmp.config.doNotSellCategories,
|
|
229
|
-
},
|
|
230
|
-
),
|
|
231
|
-
);
|
|
232
|
-
// Store with new user-specific key
|
|
233
|
-
storeConsent(cmp, consent).then(() => {
|
|
234
|
-
// Clean up old storage
|
|
235
|
-
clearStorage(oldKey);
|
|
236
|
-
clearStorage(oldTimestampKey);
|
|
237
|
-
resolve(consent);
|
|
238
|
-
});
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
resolve(null); // No old consent found
|
|
243
|
-
})
|
|
244
|
-
.catch(() => {
|
|
245
|
-
resolve(null); // Old key not found
|
|
246
|
-
});
|
|
247
|
-
break;
|
|
248
|
-
default:
|
|
249
|
-
resolve(null);
|
|
250
|
-
break;
|
|
245
|
+
const oldKey = `${STORAGE_KEY}_${cmp.configId}`;
|
|
246
|
+
try {
|
|
247
|
+
const oldConsent = await getStorage(oldKey);
|
|
248
|
+
if (!oldConsent) {
|
|
249
|
+
return null;
|
|
251
250
|
}
|
|
252
|
-
|
|
251
|
+
let consent = JSON.parse(oldConsent) as ConsentCategoryType[];
|
|
252
|
+
if (consent.length === 0) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
consent = formatConsentArray(
|
|
256
|
+
normalizeDoNotSellConsent(makeConsentFromCategories(consent), {
|
|
257
|
+
enableDoNotSell: cmp.config.enableDoNotSell,
|
|
258
|
+
doNotSellCategories: cmp.config.doNotSellCategories,
|
|
259
|
+
}),
|
|
260
|
+
);
|
|
261
|
+
const sourceTimestamp = await retrieveTimestampForSuffix(cmp.configId, '');
|
|
262
|
+
const parsedSourceTimestamp = new Date(sourceTimestamp);
|
|
263
|
+
const migrationTimestamp = Number.isNaN(parsedSourceTimestamp.getTime())
|
|
264
|
+
? new Date(0)
|
|
265
|
+
: parsedSourceTimestamp;
|
|
266
|
+
await storeConsent(cmp, consent, undefined, migrationTimestamp);
|
|
267
|
+
await Promise.all(
|
|
268
|
+
[
|
|
269
|
+
oldKey,
|
|
270
|
+
lastConsentDateKey(cmp.configId, ''),
|
|
271
|
+
legacyLastConsentDateKey(''),
|
|
272
|
+
].map(async (key) => {
|
|
273
|
+
try {
|
|
274
|
+
await clearStorage(key);
|
|
275
|
+
} catch {}
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
return consent;
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
253
282
|
};
|
|
254
283
|
|
|
255
284
|
// The consent to fall back to when a visitor has no stored consent yet. Flavor 7
|
|
@@ -284,6 +313,25 @@ const clearToFirstLoadDefault = async (
|
|
|
284
313
|
return seed;
|
|
285
314
|
};
|
|
286
315
|
|
|
316
|
+
const retrieveStoredConsent = async (
|
|
317
|
+
cmp: ConsentManager,
|
|
318
|
+
): Promise<ConsentCategoryType[] | null> => {
|
|
319
|
+
const hashedExtUsrData = hashExtUsrData(
|
|
320
|
+
cmp.customerId,
|
|
321
|
+
cmp.configId,
|
|
322
|
+
cmp.extUsrData,
|
|
323
|
+
);
|
|
324
|
+
const userSuffix = hashedExtUsrData ? `_${hashedExtUsrData}` : '';
|
|
325
|
+
try {
|
|
326
|
+
const stored = await getStorage(
|
|
327
|
+
`${STORAGE_KEY}_${cmp.configId}${userSuffix}`,
|
|
328
|
+
);
|
|
329
|
+
return stored ? (JSON.parse(stored) as ConsentCategoryType[]) : null;
|
|
330
|
+
} catch {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
|
|
287
335
|
// After an expired/invalid record, settle on a persisted essential-only state.
|
|
288
336
|
// Persisting (rather than only clearing the payload with clearConsent) keeps a
|
|
289
337
|
// subsequent load in this safe state instead of falling back to the first-load
|
|
@@ -293,17 +341,47 @@ const clearToFirstLoadDefault = async (
|
|
|
293
341
|
// storage would otherwise re-seed the first-load default.
|
|
294
342
|
const settleEssentialOnly = (
|
|
295
343
|
cmp: ConsentManager,
|
|
296
|
-
|
|
297
|
-
|
|
344
|
+
// Keep invalidation sticky until the visitor records a new choice.
|
|
345
|
+
consentDate?: string | Date,
|
|
346
|
+
): Promise<ConsentCategoryType[]> => {
|
|
347
|
+
const categories: ConsentCategoryType[] = [ConsentCategory.ESSENTIAL];
|
|
348
|
+
if (
|
|
349
|
+
cmp.config?.isDoNotSellVisible &&
|
|
350
|
+
(cmp.config.enableDoNotSellDefault ||
|
|
351
|
+
cmp.consent?.includes(ConsentCategory.OPT_OUT))
|
|
352
|
+
) {
|
|
353
|
+
categories.push(ConsentCategory.OPT_OUT);
|
|
354
|
+
}
|
|
355
|
+
return storeConsent(cmp, categories, undefined, consentDate);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const markConsentInvalidated = (cmp: ConsentManager) => {
|
|
359
|
+
cmp.consentInvalidated = true;
|
|
360
|
+
cmp.consentPromptRequired = true;
|
|
361
|
+
};
|
|
298
362
|
|
|
299
363
|
const retrieveConsent = async (cmp: ConsentManager) => {
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
// essential-only
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
364
|
+
// Invalidate stored consent for either independent reason — the record
|
|
365
|
+
// predates the forceReconsent stamp, or it aged past its expiration window —
|
|
366
|
+
// and settle on essential-only. Only read the timestamp when there is a stamp
|
|
367
|
+
// or window to evaluate, so a no-signal load skips the read as the previous
|
|
368
|
+
// expiry-only path did. `consentInvalidated` (read in index.tsx) drives the
|
|
369
|
+
// auto re-prompt; it is set only for a previously recorded consent, never on
|
|
370
|
+
// the first-run path.
|
|
371
|
+
const forceSeconds = effectiveForceReconsentSeconds(cmp);
|
|
372
|
+
const expirationSeconds = await effectiveExpirationSeconds(cmp);
|
|
373
|
+
if (forceSeconds !== null || expirationSeconds !== null) {
|
|
374
|
+
const timestamp = await retrieveLastConsentTimestamp(cmp);
|
|
375
|
+
const expired = isTimestampExpired(timestamp, expirationSeconds);
|
|
376
|
+
const forced = predatesForceReconsent(cmp, timestamp);
|
|
377
|
+
if (expired || forced) {
|
|
378
|
+
const storedConsent = await retrieveStoredConsent(cmp);
|
|
379
|
+
if (storedConsent?.length) {
|
|
380
|
+
cmp.consent = storedConsent;
|
|
381
|
+
markConsentInvalidated(cmp);
|
|
382
|
+
return settleEssentialOnly(cmp, timestamp);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
307
385
|
}
|
|
308
386
|
return new Promise<ConsentCategoryType[]>((resolve) => {
|
|
309
387
|
const resConsent = (consentJson: string | null | undefined) => {
|
|
@@ -337,7 +415,15 @@ const retrieveConsent = async (cmp: ConsentManager) => {
|
|
|
337
415
|
// Try migration if new key not found
|
|
338
416
|
const migratedConsent = await migrateConsentStorage(cmp);
|
|
339
417
|
if (migratedConsent) {
|
|
340
|
-
|
|
418
|
+
const timestamp = await retrieveLastConsentTimestamp(cmp);
|
|
419
|
+
const expired = isTimestampExpired(timestamp, expirationSeconds);
|
|
420
|
+
const forced = predatesForceReconsent(cmp, timestamp);
|
|
421
|
+
if (expired || forced) {
|
|
422
|
+
markConsentInvalidated(cmp);
|
|
423
|
+
resolve(await settleEssentialOnly(cmp, timestamp));
|
|
424
|
+
} else {
|
|
425
|
+
resolve(migratedConsent);
|
|
426
|
+
}
|
|
341
427
|
} else {
|
|
342
428
|
// No consent found, seed the first-load default
|
|
343
429
|
clearToFirstLoadDefault(cmp).then(resolve);
|
|
@@ -365,33 +451,7 @@ const retrieveLastConsentTimestamp = async (
|
|
|
365
451
|
cmp.extUsrData,
|
|
366
452
|
);
|
|
367
453
|
const userSuffix = hashedExtUsrData ? `_${hashedExtUsrData}` : '';
|
|
368
|
-
|
|
369
|
-
const parse = (json: string | null | undefined): string => {
|
|
370
|
-
if (!json) {
|
|
371
|
-
return '';
|
|
372
|
-
}
|
|
373
|
-
try {
|
|
374
|
-
return JSON.parse(json) as string;
|
|
375
|
-
} catch {
|
|
376
|
-
return '';
|
|
377
|
-
}
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
try {
|
|
381
|
-
const scoped = parse(
|
|
382
|
-
await getStorage(lastConsentDateKey(cmp.configId, userSuffix)),
|
|
383
|
-
);
|
|
384
|
-
if (scoped) {
|
|
385
|
-
return scoped;
|
|
386
|
-
}
|
|
387
|
-
// Fall back to the legacy un-scoped key for installs written before the
|
|
388
|
-
// timestamp was scoped by configId.
|
|
389
|
-
return parse(await getStorage(legacyLastConsentDateKey(userSuffix)));
|
|
390
|
-
} catch {
|
|
391
|
-
// Fail closed: a storage rejection resolves to no timestamp instead of
|
|
392
|
-
// hanging the consent load.
|
|
393
|
-
return '';
|
|
394
|
-
}
|
|
454
|
+
return retrieveTimestampForSuffix(cmp.configId, userSuffix);
|
|
395
455
|
};
|
|
396
456
|
|
|
397
457
|
/**
|
|
@@ -436,6 +496,72 @@ const isTimestampExpired = (
|
|
|
436
496
|
return Date.now() - consentedAtMs > seconds * 1000;
|
|
437
497
|
};
|
|
438
498
|
|
|
499
|
+
/**
|
|
500
|
+
* Resolves the effective `forceReconsent` signal in whole seconds, validating
|
|
501
|
+
* before trusting it. The raw field ships over the network and is human-edited,
|
|
502
|
+
* so treat any malformed value as "no signal" (null) rather than invalidating
|
|
503
|
+
* every consent:
|
|
504
|
+
*
|
|
505
|
+
* - absent / null → null (no signal)
|
|
506
|
+
* - <= 0 → null (guards zero and negatives)
|
|
507
|
+
* - in the future → null (clock skew, or a value mistakenly stamped in
|
|
508
|
+
* milliseconds, which reads as ~1000× "now" ⇒ future)
|
|
509
|
+
*
|
|
510
|
+
* Failing toward *not* invalidating means the worst a bad value causes is a
|
|
511
|
+
* missed prompt, never a permanent lockout.
|
|
512
|
+
*/
|
|
513
|
+
export const effectiveForceReconsentSeconds = (
|
|
514
|
+
cmp: ConsentManager,
|
|
515
|
+
): number | null => {
|
|
516
|
+
const raw: unknown = cmp.config?.forceReconsent;
|
|
517
|
+
if (raw == null) {
|
|
518
|
+
return null;
|
|
519
|
+
}
|
|
520
|
+
const seconds =
|
|
521
|
+
typeof raw === 'number'
|
|
522
|
+
? raw
|
|
523
|
+
: typeof raw === 'string' && raw.trim() !== ''
|
|
524
|
+
? Number(raw)
|
|
525
|
+
: Number.NaN;
|
|
526
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
if (seconds > Math.floor(Date.now() / 1000)) {
|
|
530
|
+
return null;
|
|
531
|
+
}
|
|
532
|
+
return seconds;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* True when a consent record predates the server's `forceReconsent` stamp.
|
|
537
|
+
* `timestamp` is the recorded-consent instant (stored as an ISO/ms string).
|
|
538
|
+
*
|
|
539
|
+
* Unit alignment is the subtle part: the signal is in SECONDS and the timestamp
|
|
540
|
+
* is in MILLISECONDS, so the signal is scaled up (`* 1000`). The boundary is
|
|
541
|
+
* strict `<` — consent recorded exactly at the stamp instant stays valid.
|
|
542
|
+
*
|
|
543
|
+
* A missing/unparseable timestamp fails closed (returns false), so first-run
|
|
544
|
+
* (no timestamp) is not wrongly invalidated. `storeConsent` always co-stamps,
|
|
545
|
+
* so a consented-but-timestampless record is unreachable here anyway.
|
|
546
|
+
*/
|
|
547
|
+
export const predatesForceReconsent = (
|
|
548
|
+
cmp: ConsentManager,
|
|
549
|
+
timestamp: string | number | Date | null | undefined,
|
|
550
|
+
): boolean => {
|
|
551
|
+
const forceSeconds = effectiveForceReconsentSeconds(cmp);
|
|
552
|
+
if (forceSeconds === null) {
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
if (!timestamp) {
|
|
556
|
+
return false;
|
|
557
|
+
}
|
|
558
|
+
const consentedAtMs = new Date(timestamp).getTime();
|
|
559
|
+
if (Number.isNaN(consentedAtMs)) {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
return consentedAtMs < forceSeconds * 1000;
|
|
563
|
+
};
|
|
564
|
+
|
|
439
565
|
/**
|
|
440
566
|
* Returns true when the locally stored consent has aged past its expiration
|
|
441
567
|
* window (see {@link effectiveExpirationSeconds}).
|
|
@@ -455,6 +581,8 @@ export const recordConsent = (
|
|
|
455
581
|
cmp: ConsentManager,
|
|
456
582
|
consent?: ConsentCategoryType[],
|
|
457
583
|
) => {
|
|
584
|
+
cmp.consentInvalidated = false;
|
|
585
|
+
cmp.consentPromptRequired = false;
|
|
458
586
|
const payload = createConsentRecordPayload(cmp, consent);
|
|
459
587
|
return API.post(`${CONSENT_URI}/record`, payload, {
|
|
460
588
|
referrer: cmp.consentingDomain,
|
|
@@ -539,18 +667,22 @@ export const fetchConsent = async (cmp: ConsentManager) => {
|
|
|
539
667
|
|
|
540
668
|
// Evaluate the remote record against the server's own timestamp (when the
|
|
541
669
|
// visitor actually consented), not "now", so an already-expired remote
|
|
542
|
-
// record is never restored and re-dated as fresh.
|
|
670
|
+
// record is never restored and re-dated as fresh. A record that predates
|
|
671
|
+
// the server's forceReconsent stamp is stale for the same reason — applying
|
|
672
|
+
// it would silently re-grant the very consent the signal invalidated.
|
|
543
673
|
const seconds = await effectiveExpirationSeconds(cmp);
|
|
544
|
-
const
|
|
674
|
+
const remoteForced = predatesForceReconsent(cmp, lastConsentTimestamp);
|
|
675
|
+
const remoteStale =
|
|
676
|
+
isTimestampExpired(lastConsentTimestamp, seconds) || remoteForced;
|
|
545
677
|
|
|
546
678
|
// If extUsrData is not null and not empty, always use remote consent (cross-device consent enabled)
|
|
547
679
|
if (extUsrData && extUsrData.trim() !== '') {
|
|
548
|
-
if (
|
|
549
|
-
// The authoritative remote record
|
|
550
|
-
// essential-only
|
|
551
|
-
//
|
|
552
|
-
|
|
553
|
-
return settleEssentialOnly(cmp);
|
|
680
|
+
if (remoteStale) {
|
|
681
|
+
// The authoritative remote record is stale (expired or predates the
|
|
682
|
+
// stamp); settle essential-only and flag the invalidation rather than
|
|
683
|
+
// restoring it.
|
|
684
|
+
markConsentInvalidated(cmp);
|
|
685
|
+
return settleEssentialOnly(cmp, lastConsentTimestamp);
|
|
554
686
|
}
|
|
555
687
|
const remoteConsentCategories = formatConsentArray(
|
|
556
688
|
normalizeDoNotSellConsent(
|
|
@@ -573,6 +705,7 @@ export const fetchConsent = async (cmp: ConsentManager) => {
|
|
|
573
705
|
true,
|
|
574
706
|
lastConsentTimestamp,
|
|
575
707
|
);
|
|
708
|
+
cmp.consentPromptRequired = false;
|
|
576
709
|
return remoteConsentCategories;
|
|
577
710
|
}
|
|
578
711
|
|
|
@@ -580,7 +713,7 @@ export const fetchConsent = async (cmp: ConsentManager) => {
|
|
|
580
713
|
const internalTimestamp = await retrieveLastConsentTimestamp(cmp);
|
|
581
714
|
try {
|
|
582
715
|
if (
|
|
583
|
-
!
|
|
716
|
+
!remoteStale &&
|
|
584
717
|
(new Date(lastConsentTimestamp) > new Date(internalTimestamp) ||
|
|
585
718
|
internalTimestamp === '')
|
|
586
719
|
) {
|
|
@@ -598,6 +731,7 @@ export const fetchConsent = async (cmp: ConsentManager) => {
|
|
|
598
731
|
true,
|
|
599
732
|
lastConsentTimestamp,
|
|
600
733
|
);
|
|
734
|
+
cmp.consentPromptRequired = false;
|
|
601
735
|
|
|
602
736
|
return remoteConsentCategories;
|
|
603
737
|
}
|
|
@@ -56,7 +56,11 @@ export type Config = {
|
|
|
56
56
|
customerId: string;
|
|
57
57
|
domains: Array<string>;
|
|
58
58
|
forceManagePreferences: boolean;
|
|
59
|
-
|
|
59
|
+
// Server-stamped Unix timestamp in whole SECONDS. When present, consent
|
|
60
|
+
// recorded before this instant is treated as no longer valid, re-prompting the
|
|
61
|
+
// visitor. Read-only on the client (the server authors it); absent/undefined
|
|
62
|
+
// means "no signal". See src/services/consent.ts effectiveForceReconsentSeconds.
|
|
63
|
+
forceReconsent?: number;
|
|
60
64
|
gdprApplies?: boolean;
|
|
61
65
|
googleConsent: boolean;
|
|
62
66
|
gpcSupport: boolean;
|
|
@@ -71,6 +75,12 @@ export type Config = {
|
|
|
71
75
|
policyLinkText: string;
|
|
72
76
|
region: string;
|
|
73
77
|
remoteConsent: boolean;
|
|
78
|
+
// When true, the categories drawer shows a row with the visitor's consent
|
|
79
|
+
// UUID (the device identifier). Defaults false.
|
|
80
|
+
showConsentUuid: boolean;
|
|
81
|
+
// When true, the consent banner shows the "Powered by Osano" logo. Defaults
|
|
82
|
+
// false. Does not affect the categories drawer, which always shows the logo.
|
|
83
|
+
showBannerLogo: boolean;
|
|
74
84
|
storagePolicyHref: string;
|
|
75
85
|
timeoutSeconds: number;
|
|
76
86
|
type?: Flavor;
|
package/src/types/store.d.ts
CHANGED
|
@@ -30,6 +30,11 @@ export type OsanoState = {
|
|
|
30
30
|
scrollToType: string;
|
|
31
31
|
scrollToId: number;
|
|
32
32
|
stack: string[];
|
|
33
|
+
// Transient marker set by a RELOAD whose consent was invalidated (expired or
|
|
34
|
+
// predating the forceReconsent stamp). The IAB TCF middleware reads it on
|
|
35
|
+
// RELOAD/SET_CONFIG to wipe stale TC strings instead of rehydrating them.
|
|
36
|
+
// Reset on SAVE_BEGIN (the user re-consented) and on the next clean load.
|
|
37
|
+
consentInvalidated?: boolean;
|
|
33
38
|
};
|
|
34
39
|
|
|
35
40
|
export type ActionCreator<S, T> = (payload?: T) => OsanoAction<S, T>;
|
|
@@ -7,7 +7,9 @@ import React, {
|
|
|
7
7
|
useRef,
|
|
8
8
|
} from 'react';
|
|
9
9
|
import {
|
|
10
|
+
Linking,
|
|
10
11
|
Modal,
|
|
12
|
+
Pressable,
|
|
11
13
|
StyleSheet,
|
|
12
14
|
Text,
|
|
13
15
|
useWindowDimensions,
|
|
@@ -23,6 +25,7 @@ import { getConsent, isCategoryDisabled, isDialogOpen } from '../../selectors';
|
|
|
23
25
|
|
|
24
26
|
import { CloseButton } from '../../components/Button';
|
|
25
27
|
import Locale from '../../components/Locale';
|
|
28
|
+
import PoweredByOsano from '../../components/PoweredByOsano';
|
|
26
29
|
import StoragePolicyLink from '../../components/StoragePolicyLink';
|
|
27
30
|
import Toggle from '../../components/Toggle';
|
|
28
31
|
import { type ConsentCategoryType } from '../../constants';
|
|
@@ -34,7 +37,7 @@ import Message from './Message';
|
|
|
34
37
|
import PreferenceManagementDeepLinks from './PreferenceManagementDeepLinks';
|
|
35
38
|
|
|
36
39
|
export const Dialog = (): ReactElement<Modal> => {
|
|
37
|
-
const { cmp, config, dispatch, getState, locale } =
|
|
40
|
+
const { cmp, config, dispatch, getState, locale, translate } =
|
|
38
41
|
useContext(OsanoPrivateContext);
|
|
39
42
|
const impliedConsentTimeout = useRef<ReturnType<typeof setTimeout> | null>(
|
|
40
43
|
null,
|
|
@@ -260,6 +263,17 @@ export const Dialog = (): ReactElement<Modal> => {
|
|
|
260
263
|
windowWidth >= 768 ? styles.buttonRow : styles.buttonCol
|
|
261
264
|
}
|
|
262
265
|
/>
|
|
266
|
+
{config?.showBannerLogo ? (
|
|
267
|
+
<Pressable
|
|
268
|
+
accessibilityRole="link"
|
|
269
|
+
accessibilityLabel={`${translate('messaging.poweredBy')}`}
|
|
270
|
+
onPress={() => Linking.openURL('https://www.osano.com')}
|
|
271
|
+
style={styles.bannerLogo}
|
|
272
|
+
testID="dialog-banner-logo"
|
|
273
|
+
>
|
|
274
|
+
<PoweredByOsano />
|
|
275
|
+
</Pressable>
|
|
276
|
+
) : null}
|
|
263
277
|
{(config?.showDismissButton ?? true) && (
|
|
264
278
|
<CloseButton
|
|
265
279
|
testID="close-dialog"
|
|
@@ -313,6 +327,10 @@ const styles = StyleSheet.create({
|
|
|
313
327
|
dialogContent: {
|
|
314
328
|
marginBottom: 16,
|
|
315
329
|
},
|
|
330
|
+
bannerLogo: {
|
|
331
|
+
alignItems: 'center',
|
|
332
|
+
paddingTop: 28,
|
|
333
|
+
},
|
|
316
334
|
buttonCol: {
|
|
317
335
|
maxWidth: Number.MAX_SAFE_INTEGER,
|
|
318
336
|
},
|