@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.
Files changed (68) hide show
  1. package/lib/commonjs/ConsentManager.js +32 -1
  2. package/lib/commonjs/ConsentManager.js.map +1 -1
  3. package/lib/commonjs/components/PoweredByOsano.js +72 -0
  4. package/lib/commonjs/components/PoweredByOsano.js.map +1 -0
  5. package/lib/commonjs/iab/tcf/iab-storage-utils.js +53 -49
  6. package/lib/commonjs/iab/tcf/iab-storage-utils.js.map +1 -1
  7. package/lib/commonjs/iab/tcf/middleware.js +20 -23
  8. package/lib/commonjs/iab/tcf/middleware.js.map +1 -1
  9. package/lib/commonjs/iab/tcf/reducer.js +0 -22
  10. package/lib/commonjs/iab/tcf/reducer.js.map +1 -1
  11. package/lib/commonjs/index.js +16 -2
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/reducer.js +5 -1
  14. package/lib/commonjs/reducer.js.map +1 -1
  15. package/lib/commonjs/services/consent.js +178 -77
  16. package/lib/commonjs/services/consent.js.map +1 -1
  17. package/lib/commonjs/views/Dialog/index.js +15 -2
  18. package/lib/commonjs/views/Dialog/index.js.map +1 -1
  19. package/lib/commonjs/views/Drawer/Drawer.ConsentCategories.js +160 -12
  20. package/lib/commonjs/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
  21. package/lib/module/ConsentManager.js +32 -1
  22. package/lib/module/ConsentManager.js.map +1 -1
  23. package/lib/module/components/PoweredByOsano.js +68 -0
  24. package/lib/module/components/PoweredByOsano.js.map +1 -0
  25. package/lib/module/iab/tcf/iab-storage-utils.js +53 -49
  26. package/lib/module/iab/tcf/iab-storage-utils.js.map +1 -1
  27. package/lib/module/iab/tcf/middleware.js +22 -25
  28. package/lib/module/iab/tcf/middleware.js.map +1 -1
  29. package/lib/module/iab/tcf/reducer.js +1 -23
  30. package/lib/module/iab/tcf/reducer.js.map +1 -1
  31. package/lib/module/index.js +17 -3
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/reducer.js +5 -1
  34. package/lib/module/reducer.js.map +1 -1
  35. package/lib/module/services/consent.js +174 -75
  36. package/lib/module/services/consent.js.map +1 -1
  37. package/lib/module/views/Dialog/index.js +16 -3
  38. package/lib/module/views/Dialog/index.js.map +1 -1
  39. package/lib/module/views/Drawer/Drawer.ConsentCategories.js +162 -14
  40. package/lib/module/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
  41. package/lib/typescript/src/ConsentManager.d.ts +13 -1
  42. package/lib/typescript/src/ConsentManager.d.ts.map +1 -1
  43. package/lib/typescript/src/components/PoweredByOsano.d.ts +5 -0
  44. package/lib/typescript/src/components/PoweredByOsano.d.ts.map +1 -0
  45. package/lib/typescript/src/iab/tcf/iab-storage-utils.d.ts +7 -7
  46. package/lib/typescript/src/iab/tcf/iab-storage-utils.d.ts.map +1 -1
  47. package/lib/typescript/src/iab/tcf/middleware.d.ts.map +1 -1
  48. package/lib/typescript/src/iab/tcf/reducer.d.ts +1 -0
  49. package/lib/typescript/src/iab/tcf/reducer.d.ts.map +1 -1
  50. package/lib/typescript/src/index.d.ts.map +1 -1
  51. package/lib/typescript/src/reducer.d.ts.map +1 -1
  52. package/lib/typescript/src/services/consent.d.ts +28 -0
  53. package/lib/typescript/src/services/consent.d.ts.map +1 -1
  54. package/lib/typescript/src/views/Dialog/index.d.ts.map +1 -1
  55. package/lib/typescript/src/views/Drawer/Drawer.ConsentCategories.d.ts.map +1 -1
  56. package/package.json +5 -4
  57. package/src/ConsentManager.ts +33 -2
  58. package/src/components/PoweredByOsano.tsx +79 -0
  59. package/src/iab/tcf/iab-storage-utils.ts +101 -56
  60. package/src/iab/tcf/middleware.ts +36 -32
  61. package/src/iab/tcf/reducer.ts +2 -29
  62. package/src/index.tsx +15 -1
  63. package/src/reducer.ts +4 -0
  64. package/src/services/consent.ts +220 -86
  65. package/src/types/consent-config.d.ts +11 -1
  66. package/src/types/store.d.ts +5 -0
  67. package/src/views/Dialog/index.tsx +19 -1
  68. package/src/views/Drawer/Drawer.ConsentCategories.tsx +168 -11
@@ -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
- return new Promise<ConsentCategoryType[] | null>((resolve) => {
212
- const oldKey = `${STORAGE_KEY}_${cmp.configId}`; // Legacy key without user suffix
213
- const oldTimestampKey = `${STORAGE_KEY}_last_recorded_consent_date`; // Legacy timestamp key
241
+ if (Platform.OS !== 'ios' && Platform.OS !== 'android') {
242
+ return null;
243
+ }
214
244
 
215
- switch (Platform.OS) {
216
- case 'ios':
217
- case 'android':
218
- getStorage(oldKey)
219
- .then((oldConsent) => {
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
- ): Promise<ConsentCategoryType[]> =>
297
- storeConsent(cmp, [ConsentCategory.ESSENTIAL]);
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
- // If stored consent has aged past its expiration window (production
301
- // per-country interval, or the debug testing override), settle on a persisted
302
- // essential-only state so callers see essential-only and a later load stays
303
- // here (mirrors Android's clearExpiredConsent() + essential-only
304
- // getConsentedCategories()).
305
- if (await isConsentExpired(cmp)) {
306
- return settleEssentialOnly(cmp);
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
- resolve(migratedConsent);
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 remoteExpired = isTimestampExpired(lastConsentTimestamp, seconds);
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 (remoteExpired) {
549
- // The authoritative remote record has expired; settle on a persisted
550
- // essential-only state rather than restoring stale consent, so a
551
- // subsequent load stays here instead of falling back to the
552
- // first-load default.
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
- !remoteExpired &&
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
- forceReconsent?: Date;
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;
@@ -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
  },