@osano/osano-cmp-react-native 0.18.5 → 0.19.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.
Files changed (67) hide show
  1. package/lib/commonjs/ConsentManager.js +28 -2
  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/constants.js +2 -3
  6. package/lib/commonjs/constants.js.map +1 -1
  7. package/lib/commonjs/doNotSell.js +95 -0
  8. package/lib/commonjs/doNotSell.js.map +1 -0
  9. package/lib/commonjs/index.js +9 -1
  10. package/lib/commonjs/index.js.map +1 -1
  11. package/lib/commonjs/reducer.js +41 -41
  12. package/lib/commonjs/reducer.js.map +1 -1
  13. package/lib/commonjs/selectors.js +20 -2
  14. package/lib/commonjs/selectors.js.map +1 -1
  15. package/lib/commonjs/services/consent.js +34 -5
  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 +161 -13
  20. package/lib/commonjs/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
  21. package/lib/module/ConsentManager.js +28 -2
  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/constants.js +2 -3
  26. package/lib/module/constants.js.map +1 -1
  27. package/lib/module/doNotSell.js +87 -0
  28. package/lib/module/doNotSell.js.map +1 -0
  29. package/lib/module/index.js +9 -1
  30. package/lib/module/index.js.map +1 -1
  31. package/lib/module/reducer.js +41 -41
  32. package/lib/module/reducer.js.map +1 -1
  33. package/lib/module/selectors.js +16 -1
  34. package/lib/module/selectors.js.map +1 -1
  35. package/lib/module/services/consent.js +34 -5
  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 +164 -16
  40. package/lib/module/views/Drawer/Drawer.ConsentCategories.js.map +1 -1
  41. package/lib/typescript/src/ConsentManager.d.ts +7 -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/constants.d.ts.map +1 -1
  46. package/lib/typescript/src/doNotSell.d.ts +48 -0
  47. package/lib/typescript/src/doNotSell.d.ts.map +1 -0
  48. package/lib/typescript/src/index.d.ts.map +1 -1
  49. package/lib/typescript/src/reducer.d.ts.map +1 -1
  50. package/lib/typescript/src/selectors.d.ts +3 -0
  51. package/lib/typescript/src/selectors.d.ts.map +1 -1
  52. package/lib/typescript/src/services/consent.d.ts +2 -2
  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 +28 -5
  58. package/src/components/PoweredByOsano.tsx +79 -0
  59. package/src/constants.ts +2 -3
  60. package/src/doNotSell.ts +122 -0
  61. package/src/index.tsx +10 -1
  62. package/src/reducer.ts +53 -56
  63. package/src/selectors.ts +25 -2
  64. package/src/services/consent.ts +49 -9
  65. package/src/types/consent-config.d.ts +12 -0
  66. package/src/views/Dialog/index.tsx +19 -1
  67. package/src/views/Drawer/Drawer.ConsentCategories.tsx +170 -12
@@ -4,13 +4,16 @@ import React, {
4
4
  useEffect,
5
5
  useMemo,
6
6
  useRef,
7
+ useState,
7
8
  } from 'react';
8
9
  import {
9
10
  ActivityIndicator,
10
11
  FlatList,
11
12
  type LayoutChangeEvent,
13
+ Linking,
12
14
  type ListRenderItemInfo,
13
15
  Platform,
16
+ Pressable,
14
17
  type StyleProp,
15
18
  StyleSheet,
16
19
  Text,
@@ -37,6 +40,7 @@ import { Divider } from '../../components/Divider';
37
40
  import { ExpansionPanel } from '../../components/ExpansionPanel';
38
41
  import { Link } from '../../components/Link';
39
42
  import Locale from '../../components/Locale';
43
+ import PoweredByOsano from '../../components/PoweredByOsano';
40
44
  import StoragePolicyLink from '../../components/StoragePolicyLink';
41
45
  import Toggle from '../../components/Toggle';
42
46
  import { ConsentCategory, type ConsentCategoryType } from '../../constants';
@@ -50,12 +54,13 @@ import {
50
54
  getStack,
51
55
  isCategoryAccepted,
52
56
  isCategoryDisabled,
57
+ shouldRenderOptOut,
53
58
  } from '../../selectors';
54
59
  import type { Disclosure } from '../../types/store';
55
60
  import DrawerHeader from './Drawer.Header';
56
61
 
57
62
  type ConsentCategoryItem = {
58
- type: 'info' | 'category' | 'specialFeature';
63
+ type: 'info' | 'category' | 'specialFeature' | 'uuid';
59
64
  id: string;
60
65
  headerHeight?: number;
61
66
  };
@@ -511,21 +516,144 @@ const CategoryListHeader: React.FC<{ style: StyleProp<ViewStyle> }> = ({
511
516
  );
512
517
  };
513
518
 
519
+ const OSANO_URL =
520
+ 'https://www.osano.com/?utm_campaign=cmp&utm_source=cmp-dialog&utm_medium=drawer';
521
+
514
522
  const CategoryListFooter: React.FC = () => {
515
- const { cmp, dispatch } = useContext(OsanoPrivateContext);
523
+ const { cmp, dispatch, translate } = useContext(OsanoPrivateContext);
516
524
 
517
525
  const handleSave = useCallback(() => {
518
526
  cmp && dispatch(saveConsent(cmp));
519
527
  dispatch(close());
520
- }, [cmp]);
528
+ }, [cmp, dispatch]);
521
529
 
530
+ // Always shown here; showBannerLogo only gates the logo on the consent banner.
522
531
  return (
523
532
  <View style={[styles.listItem, styles.listFooter]}>
524
533
  <SaveButton onPress={handleSave} />
525
- <View style={styles.poweredBy}>
526
- <Link href="https://www.osano.com/?utm_campaign=cmp&utm_source=cmp-dialog&utm_medium=drawer">
527
- <Locale path="messaging.poweredBy" />
528
- </Link>
534
+ <Pressable
535
+ accessibilityRole="link"
536
+ accessibilityLabel={`${translate('messaging.poweredBy')}`}
537
+ onPress={() => Linking.openURL(OSANO_URL)}
538
+ style={styles.poweredBy}
539
+ >
540
+ <PoweredByOsano testID="drawer-banner-logo" />
541
+ </Pressable>
542
+ </View>
543
+ );
544
+ };
545
+
546
+ // Falls back to translucent black when hex is missing or malformed.
547
+ const hexToRgba = (hex: string | undefined, alpha: number): string => {
548
+ if (!hex || !hex.startsWith('#')) {
549
+ return `rgba(0, 0, 0, ${alpha})`;
550
+ }
551
+ let h = hex.slice(1);
552
+ if (h.length === 3) {
553
+ h = h
554
+ .split('')
555
+ .map((c) => c + c)
556
+ .join('');
557
+ }
558
+ // Reject any non-hex input up front so parseInt can't yield NaN channels.
559
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) {
560
+ return `rgba(0, 0, 0, ${alpha})`;
561
+ }
562
+ const r = parseInt(h.slice(0, 2), 16);
563
+ const g = parseInt(h.slice(2, 4), 16);
564
+ const b = parseInt(h.slice(4, 6), 16);
565
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
566
+ };
567
+
568
+ // Resolve the clipboard native module lazily. The require is guarded so the
569
+ // drawer still renders where the module is not linked (e.g. Expo Go), instead
570
+ // of throwing at import time; the Copy button is hidden when it is unavailable.
571
+ const getClipboard = (): { setString: (value: string) => void } | null => {
572
+ try {
573
+ // Guarded require (not a static import) so an unlinked module degrades
574
+ // instead of throwing when this file loads.
575
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
576
+ return require('@react-native-clipboard/clipboard').default;
577
+ } catch {
578
+ return null;
579
+ }
580
+ };
581
+
582
+ // Shows the visitor's consent UUID with a Copy button that writes it to the
583
+ // clipboard. Renders nothing when no UUID is available.
584
+ const ConsentUuidRow: React.FC<{ style: StyleProp<ViewStyle> }> = ({
585
+ style,
586
+ }) => {
587
+ const { cmp, config, translate } = useContext(OsanoPrivateContext);
588
+ const uuid = cmp?.deviceId ?? '';
589
+ const palette = config?.palette;
590
+ const foregroundColor = palette?.infoDialogForegroundColor;
591
+ const buttonBackgroundColor = palette?.buttonBackgroundColor;
592
+ const buttonForegroundColor = palette?.buttonForegroundColor;
593
+ const clipboard = useMemo(() => getClipboard(), []);
594
+ const [copied, setCopied] = useState(false);
595
+ const copiedTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
596
+
597
+ useEffect(
598
+ () => () => {
599
+ if (copiedTimeout.current) {
600
+ clearTimeout(copiedTimeout.current);
601
+ }
602
+ },
603
+ [],
604
+ );
605
+
606
+ const handleCopy = useCallback(() => {
607
+ if (!uuid || !clipboard) {
608
+ return;
609
+ }
610
+ clipboard.setString(uuid);
611
+ setCopied(true);
612
+ if (copiedTimeout.current) {
613
+ clearTimeout(copiedTimeout.current);
614
+ }
615
+ // Revert the label back to "Copy" so the button is reusable.
616
+ copiedTimeout.current = setTimeout(() => setCopied(false), 2000);
617
+ }, [uuid, clipboard]);
618
+
619
+ if (!uuid) {
620
+ return null;
621
+ }
622
+ const copyLabel = `${translate('drawer.copy')}`;
623
+ const copiedLabel = `${translate('drawer.copied')}`;
624
+ return (
625
+ <View style={[styles.listItem, styles.uuidRow, style]}>
626
+ <View
627
+ style={[
628
+ styles.uuidBox,
629
+ { backgroundColor: hexToRgba(foregroundColor, 0.05) },
630
+ ]}
631
+ >
632
+ <Text
633
+ selectable
634
+ numberOfLines={1}
635
+ ellipsizeMode="tail"
636
+ style={[styles.uuidText, { color: foregroundColor }]}
637
+ testID="drawer-consent-uuid"
638
+ >
639
+ {`UUID: ${uuid}`}
640
+ </Text>
641
+ {clipboard ? (
642
+ <Pressable
643
+ accessibilityRole="button"
644
+ accessibilityLabel={copyLabel}
645
+ onPress={handleCopy}
646
+ style={[
647
+ styles.copyButton,
648
+ { backgroundColor: buttonBackgroundColor },
649
+ ]}
650
+ testID="drawer-consent-uuid-copy"
651
+ >
652
+ <Text style={[styles.copyText, { color: buttonForegroundColor }]}>
653
+ {copied ? copiedLabel : copyLabel}
654
+ </Text>
655
+ </Pressable>
656
+ ) : null}
529
657
  </View>
530
658
  </View>
531
659
  );
@@ -550,6 +678,8 @@ const ListItemRenderer = ({
550
678
  );
551
679
  case 'specialFeature':
552
680
  return <SpecialFeatureListItem style={style} specialFeatureId={id} />;
681
+ case 'uuid':
682
+ return <ConsentUuidRow style={style} />;
553
683
  default:
554
684
  return null;
555
685
  }
@@ -598,7 +728,7 @@ export const ConsentCategoriesDrawer: React.FC = () => {
598
728
  return 'none';
599
729
  }, [previousStack, stack]);
600
730
  const useTcf = config?.tcfIsApplicable;
601
- const needsOptOut = config?.shouldRenderOptOut;
731
+ const needsOptOut = shouldRenderOptOut(state);
602
732
  const categories: ConsentCategoryItem[] = useMemo(() => {
603
733
  const newCategories: ConsentCategoryItem[] = (
604
734
  useTcf ? config.iabCategories : config?.categories || []
@@ -626,6 +756,9 @@ export const ConsentCategoriesDrawer: React.FC = () => {
626
756
  }),
627
757
  );
628
758
  }
759
+ if (config?.showConsentUuid) {
760
+ newCategories.push({ type: 'uuid', id: 'uuid', headerHeight });
761
+ }
629
762
  newCategories.unshift({
630
763
  type: 'info',
631
764
  id: 'info',
@@ -674,9 +807,8 @@ const styles = StyleSheet.create({
674
807
  poweredBy: {
675
808
  alignItems: 'center',
676
809
  justifyContent: 'center',
677
- flexDirection: 'column',
678
- fontWeight: '700',
679
- marginVertical: 16,
810
+ marginTop: 24,
811
+ marginBottom: 16,
680
812
  },
681
813
  switch: {
682
814
  marginBottom: 8,
@@ -697,12 +829,38 @@ const styles = StyleSheet.create({
697
829
  paddingVertical: 32,
698
830
  },
699
831
  listFooter: {
700
- paddingVertical: 32,
832
+ paddingTop: 16,
833
+ paddingBottom: 8,
701
834
  },
702
835
  listItem: {
703
836
  fontSize: 12,
704
837
  paddingHorizontal: 24,
705
838
  },
839
+ uuidRow: {
840
+ paddingVertical: 8,
841
+ },
842
+ uuidBox: {
843
+ borderRadius: 8,
844
+ paddingHorizontal: 12,
845
+ paddingVertical: 8,
846
+ flexDirection: 'row',
847
+ alignItems: 'center',
848
+ justifyContent: 'space-between',
849
+ gap: 12,
850
+ },
851
+ uuidText: {
852
+ fontSize: 14,
853
+ flex: 1,
854
+ },
855
+ copyButton: {
856
+ borderRadius: 8,
857
+ paddingHorizontal: 16,
858
+ paddingVertical: 8,
859
+ },
860
+ copyText: {
861
+ fontSize: 14,
862
+ fontWeight: '700',
863
+ },
706
864
  paragraph: {
707
865
  marginBottom: 24,
708
866
  },