Task: Sync privacy settings between Hub and CMP

Task summary

Implement a function to sync privacy settings (cookie consent settings) between the CMP and the Hub. The function should address two cases:

  1. A visitor lands on an external website first, sets privacy preferences, then navigates to the Uberflip Hub where the existing preferences are applied
  2. A visitor lands on the Uberflip Hub first and needs to be prompted to set privacy preferences, which can be applied when they subsequently navigate to an external website

In both cases, the desired outcome is that the visitor sees the privacy banner (and sets their privacy preferences) only on the initial landing, and not subsequently.

Requirements

The code should be implemented as a custom code block on the target Hub. To avoid issues arising from a race condition, any JavaScript should be placed in a code block set to the Body Bottom placement.

The specific design of your code will vary according to the CMP you use, but in general, it should handle requests for your Hub as described below.

Terminology key:

  • Privacy Service: The CMP (or other service) that will serve as the source of truth for visitor-defined privacy settings.
  • External Privacy Settings: The visitor-defined privacy (cookie consent) settings stored in the Privacy Service.
  • Uberflip Privacy Settings: The visitor-defined privacy (cookie consent) settings defined in the target Hub using Uberflip's Privacy Groups functionality.
  • Mapping: The object that defines the relationship between Uberflip Privacy Settings and External Privacy Settings.
  • Privacy Banner: The interface used by visitors to define External Privacy Settings in the Privacy Service.
  • Sync: The action of synchronizing the External Privacy Settings and the Uberflip Privacy Settings according to the Mapping.

Initial Process

Your code should handle all requests for a Hub page using this process initially.

  1. Create a Mapping to define how Uberflip Privacy Settings correspond to External Privacy Settings to allow for comparison.
  2. On page initialization (the load Hub event), call the Privacy Service (GET request) to return the current visitor's External Privacy Settings.
    • If External Privacy Settings are returned: Handle as described in Case 1**.
    • If External Privacy Settings are not returned: Handle as described in Case 2.

📘

Note

Be aware of potential CORS issues that can arise when calling the External Privacy Service. You must allow CORS for any Hub domain that will contact the External Privacy Service.

Case 1: Sync cookie preferences set on an external website to the Uberflip Hub

In this case, the Privacy Service returns External Privacy Settings for the current visitor.

Your code should handle this case by comparing the received External Privacy Settings with the current visitor's Uberflip Privacy Settings, then performing a Sync if it is necessary to update the visitor's Uberflip Privacy Settings.

  1. Call the JS Privacy API to return the current visitor's Uberflip Privacy Settings.
  2. Using the Mapping, compare the current visitor's External Privacy Settings and Uberflip Privacy Settings* to identify any mismatches:
    • If there are any mismatches: Perform a Sync.
    • If there are no mismatches: Do not perform a Sync.
  3. In cases where there are no mismatches, make sure to handle the following possible causes:
    • The visitor never accepted/rejected the Privacy Banner: Display the Privacy Banner and proceed as in Case 2.
    • The visitor did accept/reject the Privacy Banner and a Sync was already performed: Do not display the Privacy Banner.

Case 2: Sync cookie preferences set on the Uberflip Hub to the External Privacy Service

In this case, the Privacy Service returns no External Privacy Settings for the current visitor, as none have been set on either an external website or the Hub yet.

Your code should handle this case by allowing the current visitor to set External Privacy Settings, then performing a Sync if it is necessary to update the visitor's Uberflip Privacy Settings.

  • Display the Privacy Banner for the current visitor:
    • If the Privacy Banner is accepted (visitor accepts default External Privacy Settings or changes them): Call the Privacy Service (POST request) to update External Privacy Settings. On receiving a success response, perform a Sync.
    • If the Privacy Banner is dismissed (visitor does not accept or change External Privacy Settings): Call the Privacy Service (POST request) to record dismissal of Privacy Banner. On receiving a success response, do not perform a Sync.

Sync process

Any request that requires a Sync to be performed should be handled using this process. Note that this is always unidirectional (Uberflip Privacy Settings are always set to match External Privacy Settings, never vice-versa).

  1. Call the JS Privacy API and set the current visitor's Uberflip Privacy Settings so that they match their External Privacy Settings.
    • Various methods are available to accept/reject all, or accept/reject by Privacy Group ID.
  2. Call the JS Privacy API to trigger a reload of the Hub in order to apply the updated Uberflip Privacy Settings to all elements on the current and subsequent pages.
    • The Privacy.applyChanges method is specifically intended for this purpose.

Flowchart

The flowchart below illustrates this workflow in full:

881

Example code

You can use the following example code snippet as a template for your own code. It works by sending a GET request to an external CMP, and then uses the JS Privacy API to compare the response with the visitor’s Hub Privacy Preferences.

🚧

Important

This code is provided "as-is", and is intended only as an example. While you can use it for guidance, you will need to develop your own solution. Uberflip is not able to provide support for this example code, or your own code.

<script>
  /**
   * H1. UBERFLIP -- "JS PRIVACY API" CODE EXAMPLE
   * 
   * The purpose of this code is to give an example of how Uberflip's 
   * JS Privacy API can be used to interface with your external Privacy Banner
   * and Privacy Policy page.
   * 
   * By using the JS Privacy API, you can manage the Privacy Groups on your
   * Hub to reflect the Privacy Preferences that have been selected by a
   * visitor on your External Website.
   * 
   * There are two cases that need to be covered:
   * 
   * - visitor has selected Preferences on your external website, and now
   *   those preferences must be synced with the Hub (your Privacy Banner does
   *   not need to be displayed in this case). Or,
   * 
   * - the visitor has not made any Preference selections, and your
   *   external Privacy Banner must now be displayed.
   * 
   * When a visitor has not made any Privacy Preference selection, Privacy
   * Protection features will be active, and no identifying information will
   * be collected about the visitor.
   * 
   */
  (function(w) {
    /**
     * Examples:
     *  - New Visitor: https://run.mocky.io/v3/3e0c1951-14d2-45a7-ab66-047dc905d8e1
     *  - External "Accepted": https://run.mocky.io/v3/258de2ae-8268-4bc1-b7d1-dd048c2c0b7d
     *  - External "Rejected": https://run.mocky.io/v3/473dee97-38d8-4247-b49a-a0b93fb4f8b9
     */
    var REMOTE_PRIVACY_PREFERENCES_URL = 'https://run.mocky.io/v3/258de2ae-8268-4bc1-b7d1-dd048c2c0b7d';

    /**
     * Example mapping to demonstrate relationship between External Privacy 
     * Groups/Categories and Hub Privacy Groups. 
     * 
     * Add an object {} to this list for each of your external Privacy Groups/
     * Categories. 
     * 
     *  - remotePrivacyGroupId: id of your Privacy Service's group/category;
     *
     *  - hubPrivacyGroupIds: a list of Hub Privacy Group Ids that should be
     *     mapped to the value of your Privacy Service's group. You can find
     *     these id's by typing `Hubs.Privacy.getAll()` in Developer Console
     *     while browsing your Hub.
     */
    var REMOTE_TO_HUB_PRIVACY_GROUPS = [{
        remotePrivacyGroupId: 10001000,
        hubPrivacyGroupIds: [390109]
      },
      {
        remotePrivacyGroupId: 10002000,
        hubPrivacyGroupIds: [390108]
      },
      {
        remotePrivacyGroupId: 10003000,
        hubPrivacyGroupIds: [390110, 390111]
      },
    ];

    var HubPrivacy = null;

    initialize();

    //
    // Functions
    //------------------------------------------------------------

    function initialize() {
      if (isPrivacyApiReady()) {
        bindPrivacy();
        callRemoteServer();
      } else {
        var HUBS_LOADED_EVENT = 'load';

        // Fallback: may need to wait for Hub to initialize 
        w.addEventListener(HUBS_LOADED_EVENT, function() {
          if (isPrivacyApiReady()) {
            bindPrivacy();
            callRemoteServer();
          }
        });
      }
    }

    // Check that the Expected/Required Globals are ready for consumption
    function isPrivacyApiReady() {
      return Boolean(
        (typeof w.Hubs !== 'undefined' && typeof w.Hubs.Privacy !== 'undefined') ||
        (typeof w.uberflip !== 'undefined' && typeof w.uberflip.Privacy !== 'undefined')
      );
    }

    // Find and bind the Privacy interface
    function bindPrivacy() {
      HubPrivacy = (w.Hubs || w.uberflip).Privacy;
    }

    // Note: Your service requires CORS configuration to allow request from Hub domain
    function callRemoteServer() {
      // Note: JQuery is used for this API Request example, but is not required. You
      // may need to wait for jQuery's "ready" event
      $.get(REMOTE_PRIVACY_PREFERENCES_URL).done(handleSuccess).fail(handleFailure);
    }

    function handleSuccess(data) {
      console.log('$.get: Success', data);

      if (typeof data !== 'object') {
        console.warn('Response data is not expected type of `object`');
        return;
      }

      // Using example Response JSON that has a property `privacy_groups`
      if (typeof data.privacy_groups === 'object' && isSyncRequired(data.privacy_groups)) {
        syncToRemotePreferences(data.privacy_groups);
      }

      // Using example Response JSON that has a property `is_banner_dismissed`
      if (!data.is_banner_dismissed) {
        showCustomPrivacyBanner(data.banner_version);
      }
    }

    /**
     * Compare all Hub Privacy Groups to determine if the "Accepted" value
     * matches the Expected Value of the Remote Privacy Group.
     * 
     * If one of the values does not match, then a Sync is required.
     */
    function isSyncRequired(remotePrivacyGroups) {
      for (var id in remotePrivacyGroups) {
        var remotePrivacyGroup = remotePrivacyGroups[id];
        var expectedValue = remotePrivacyGroup.accepted ? 1 : 0;
        var hubPrivacyGroupIds = getHubPrivacyGroupsByRemoteId(id);

        for (var key in hubPrivacyGroupIds) {
          var hubPrivacyGroup = HubPrivacy.getById(hubPrivacyGroupIds[key]);
          if (hubPrivacyGroup) {
            var hubPrivacyGroupCurrentValue = hubPrivacyGroup.isAccepted ? 1 : 0;

            if (expectedValue !== hubPrivacyGroupCurrentValue) {
              console.log('JS Privacy: Sync is required!');
              return true;
            }
          }
        }
      }

      console.log('JS Privacy: Sync is NOT required');
      return false;
    }

    /**
     * Update all Hub Privacy Groups to match the "Accepted" value of the 
     * Remote Privacy Group "Accepted" value.
     * 
     * Once the preferences have been updated, we must `applyChanges()` to
     * have the Hub reload all privacy components accordingly.
     */
    function syncToRemotePreferences(remotePrivacyGroups) {
      Object.keys(remotePrivacyGroups)
        .forEach(function(key) {
          var remotePrivacyGroup = remotePrivacyGroups[key];
          var methodName = remotePrivacyGroup.accepted ? 'acceptById' : 'rejectById';
          var hubPrivacyGroupIds = getHubPrivacyGroupsByRemoteId(key);

          hubPrivacyGroupIds.forEach(function(hubPrivacyGroupId) {
            HubPrivacy[methodName](hubPrivacyGroupId);
          });
        });

      // When all Preferences were updated, Save Changes and Reload Hub page
      HubPrivacy.applyChanges();

      console.log('JS Privacy: Sync Completed!');
    }

    function getHubPrivacyGroupsByRemoteId(remotePrivacyGroupId) {
      for (var key in REMOTE_TO_HUB_PRIVACY_GROUPS) {
        var mappingItem = REMOTE_TO_HUB_PRIVACY_GROUPS[key];
        if (parseInt(remotePrivacyGroupId, 10) === parseInt(mappingItem.remotePrivacyGroupId, 10)) {
          return mappingItem.hubPrivacyGroupIds;
        }
      }

      return [];
    }

    function showCustomPrivacyBanner(bannerVersion) {
      // Code to control your Custom Privacy Banner...
      console.log('JS Privacy: Show custom Privacy Banner...');
    }

    function handleFailure(response) {
      console.warn('$.get: Failed. Status Code:', response.status);
    }
  })(window);
</script>