<div class="privacy-consent" hidden>
    <div class="privacy-consent__content">
        This website uses cookies to measure traffic and improve your experience. Declining tracking cookies will set a single cookie to remember your preference. You can manage your cookie preference at any time and learn more by visiting our <a href="/privacy-policy">Privacy Policy</a>.
    </div>
    <div class="privacy-consent__buttons">
        <button class="iastate22-button iastate22-button--reverse privacy-consent__accept" type="button">Accept<span class="arrow"></span></button>
        <button class="iastate22-button iastate22-button--reverse privacy-consent__decline" type="button">Decline<span class="arrow"></span></button>
    </div>
</div>
<div class="privacy-consent" hidden>
    <div class="privacy-consent__content">
        {{ body|raw }}
    </div>
    <div class="privacy-consent__buttons">
        <button class="iastate22-button iastate22-button--reverse privacy-consent__accept" type="button">Accept<span class="arrow"></span></button>
        <button class="iastate22-button iastate22-button--reverse privacy-consent__decline" type="button">Decline<span class="arrow"></span></button>
    </div>
</div>
{
  "body": "This website uses cookies to measure traffic and improve your experience. Declining tracking cookies will set a single cookie to remember your preference. You can manage your cookie preference at any time and learn more by visiting our <a href=\"/privacy-policy\">Privacy Policy</a>."
}
  • Content:
    .privacy-consent {
      background-color: $iastate-maroon;
      color: $white;
      box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
      padding: rem(24);
      position: fixed;
      bottom: 0;
      left: 0;
      width: 100%;
      z-index: 100;
      &[hidden],
      body:not(.js) & {
        display: none;
      }
    }
    
    .privacy-consent__content {
      a {
        color: $iastate-gold;
      }
    }
    
    .privacy-consent__buttons {
      margin-top: rem(10);
      .iastate22-button {
        display: inline-block;
        &:not(:last-child) {
          margin-right: rem(10);
        }
      }
    }
    
  • URL: /components/raw/privacy-consent/_privacy-consent.scss
  • Filesystem Path: src/components/privacy-consent/_privacy-consent.scss
  • Size: 504 Bytes
  • Content:
    import Cookies from "js-cookie";
    
    declare global {
      interface Window {
        yett: any;
      }
    }
    
    const cookieKey = "privacy-accepted";
    
    export class PrivacyConsent {
      protected element: HTMLElement;
      protected acceptButton: HTMLButtonElement;
      protected declineButton: HTMLButtonElement;
      protected toggleButton: HTMLButtonElement;
      protected userConsented: boolean = false;
    
      constructor(element: HTMLElement) {
        if (element) {
          this.element = element;
          this.acceptButton = element.querySelector(".privacy-consent__accept");
          this.declineButton = element.querySelector(".privacy-consent__decline");
          this.toggleButton = document.getElementById("privacy-toggle") as HTMLButtonElement;
          this.init();
        }
      }
    
      /**
       * Get the current page's domain without any subdomains
       * to use the privacy consent value across multiple sites
       * owned by the same company/organization.
       */
      protected get cookieDomain(): string {
        const blacklistedDomains = ["pantheonsite.io"];
        const domainParts = window.location.hostname.split(".") as string[];
        const cookieDomain = domainParts.slice(domainParts.length - 2).join(".");
    
        // Certain domains like pantheonsite.io prevent cookies from being shared
        // on the root domain. In those cases we need to include the subdomain.
        if (blacklistedDomains.indexOf(cookieDomain) === -1) {
          return cookieDomain;
        } else {
          return window.location.hostname;
        }
      }
    
      protected init() {
        this.checkForConsent();
        this.addAcceptListener();
        this.addDeclineListener();
        this.addToggleListener();
        if (this.userConsented) {
          this.setToggleState();
          window.addEventListener("DOMContentLoaded", () => {
            window.yett.unblock();
          });
        }
      }
    
      /**
       * Query local storage to check for previous consent by user.
       */
      protected checkForConsent() {
        const consented = Cookies.get(cookieKey, { domain: this.cookieDomain });
        if (!!consented) {
          this.userConsented = JSON.parse(consented);
        } else {
          // Show the element if the user hasn't provided a response yet
          this.element.removeAttribute("hidden");
        }
      }
    
      /**
       * Handles accept button click
       */
      protected addAcceptListener() {
        this.acceptButton.addEventListener("click", () => {
          this.handleAccept();
          this.setToggleState();
        });
      }
    
      /**
       * Sets the privacy-accepted value to true and loads privacy-dependent scripts.
       */
      protected handleAccept() {
        Cookies.set(cookieKey, "true", {
          domain: this.cookieDomain,
          expires: 365,
        });
        this.element.setAttribute("hidden", "hidden");
        window.yett.unblock();
      }
    
      /**
       * Handles decline button click
       */
      protected addDeclineListener() {
        this.declineButton.addEventListener("click", () => {
          this.handleDecline();
          this.setToggleState(false);
        });
      }
    
      /**
       * Sets the privacy-accepted value to false
       */
      protected handleDecline() {
        Cookies.set(cookieKey, "false", {
          domain: this.cookieDomain,
          expires: 365,
        });
        this.element.setAttribute("hidden", "hidden");
      }
    
      /**
       * Toggles privacy-accepted value on click
       */
      protected addToggleListener() {
        if (this.toggleButton) {
          this.toggleButton.addEventListener("click", () => {
            const pressState = this.toggleButton.getAttribute("aria-pressed");
            if (pressState === "true") {
              this.setToggleState(false);
              this.handleDecline();
            } else {
              this.setToggleState();
              this.handleAccept();
            }
          });
        }
      }
    
      /**
       * Updates privacy toggle aria-pressed attribute and inner html if toggle is on page
       */
      protected setToggleState(toPressed = true) {
        if (this.toggleButton) {
          if (toPressed) {
            this.toggleButton.setAttribute("aria-pressed", "true");
            this.toggleButton.innerHTML = "Tracking Cookies Enabled, Click to Disable";
          } else {
            this.toggleButton.setAttribute("aria-pressed", "false");
            this.toggleButton.innerHTML = "Tracking Cookies Disabled, Click to Enable";
          }
        }
      }
    
      /**
       * Logs any scripts that create cookies to the console for debugging
       * and determining which scripts need to be flagged.
       */
      public static findNonCompliantScripts() {
        let originalValue = document.cookie;
        Object.defineProperty(document, "cookie", {
          get() {
            return originalValue;
          },
          set(value) {
            console.trace();
            return (originalValue = value);
          },
        });
      }
    }
    
    export default function privacyConsentInit() {
      const widgets = document.querySelectorAll(".privacy-consent") as NodeListOf<HTMLElement>;
      for (let i = 0; i < widgets.length; i++) {
        new PrivacyConsent(widgets[i]);
      }
    }
    
  • URL: /components/raw/privacy-consent/privacy-consent.ts
  • Filesystem Path: src/components/privacy-consent/privacy-consent.ts
  • Size: 4.8 KB

Privacy Consent

This component prevents any scripts that create cookies from initially executing until a user has accepted the terms of the site’s Privacy Policy.

Determining Which Scripts To Block

The PrivacyConsent class exposes a static public method called findNonCompliantScripts that can be invoked in order to determine which scripts are actually creating cookies. An example implementation would be appending the following to src/js/index.ts:

import { PrivacyConsent } from  "../components/privacy-consent/privacy-consent";
...
PrivacyConsent.findNonCompliantScripts();

Upon invoking this function, console.trace stack traces will appear in the JS Console showing which scripts have modified document.cookie.

Blocking Scripts

Once the scripts that need to be blocked have been determined, you’ll need to add a YETT_BLACKLIST array as high in the head tag as possible with offending domains specified. You’ll also need to add the script tag to load Yett in from a CDN. This will prevent any scripts loaded from the provided domains from loading until the user has accepted the terms. An example implementation would be:

<script>
    window.YETT_BLACKLIST = [
        /addthis\.com/
    ];
</script>
<script src="//unpkg.com/yett"></script>

If possible, adding type="javascript/blocked" to any necessary script tags is preferred and, in the cases of inline script tags that aren’t pulling in code from an external URL, it’s required to provide this attribute to prevent execution. An example of a script where this is required would be a Facebook Tracking Pixel.

For more information and documentation on Yett, please consult the Github Project.

Implementing the Component

The Privacy Consent component should be placed as close to the bottom of the body tag as possible.