codevise/pageflow

View on GitHub
package/src/frontend/Consent/index.js

Summary

Maintainability
A
3 hrs
Test Coverage
import {Persistence} from './Persistence';
import {cookies} from '../cookies';
import BackboneEvents from 'backbone-events-standalone';

const supportedParadigms = ['external opt-out', 'opt-in', 'lazy opt-in', 'skip'];

export class Consent {
  constructor({cookies, inEditor}) {
    this.requestedPromise = new Promise((resolve) => {
      this.requestedPromiseResolve = resolve;
    });

    this.vendors = [];
    this.persistence = new Persistence({cookies});
    this.emitter = {...BackboneEvents};
    this.inEditor = inEditor;
  }

  registerVendor(name, {displayName, description, paradigm,
                        cookieName, cookieKey, cookieDomain}) {
    if (this.vendorRegistrationClosed) {
      throw new Error(`Vendor ${name} has been registered after ` +
                      'registration has been closed.');
    }

    if (!name.match(/^[a-z0-9-_]+$/i)) {
      throw new Error(`Invalid vendor name '${name}'. ` +
                      'Only letters, numbers, hyphens and underscores are allowed.');
    }

    if (supportedParadigms.indexOf(paradigm) < 0) {
      throw new Error(`unknown paradigm ${paradigm}`);
    }

    this.vendors.push({
      displayName,
      description,
      name,
      paradigm,
      cookieName: cookieName || 'pageflow_consent',
      cookieKey,
      cookieDomain
    });
  }

  closeVendorRegistration() {
    this.vendorRegistrationClosed = true;

    if (!this.getUndecidedOptInVendors().length) {
      this.triggerDecisionEvents();
      return;
    }

    const vendors = this.getRequestedVendors();

    this.requestedPromiseResolve({
      vendors: this.withState(vendors),

      acceptAll: () => {
        this.persistence.store(vendors, 'accepted');
        this.triggerDecisionEvents();
      },
      denyAll: () => {
        this.persistence.store(vendors, 'denied');
        this.triggerDecisionEvents();
      },
      save: (vendorConsent) => {
        this.persistence.store(vendors, vendorConsent);
        this.triggerDecisionEvents();
      }
    });
  }

  relevantVendors({include: additionalVendorNames} = {}) {
    return this.withState(
      this.vendors.filter((vendor) => {
        return additionalVendorNames?.includes(vendor.name) ||
          vendor.paradigm === 'opt-in' ||
          vendor.paradigm === 'external opt-out' ||
          (vendor.paradigm === 'lazy opt-in' &&
           this.persistence.read(vendor) !== 'undecided');
      }),
      {applyDefaults: true}
    );
  }

  require(vendorName) {
    if (this.inEditor) {
      return Promise.resolve('fulfilled');
    }

    const vendor = this.findVendor(vendorName, 'require consent for');

    switch (vendor.paradigm) {
    case 'opt-in':
    case 'lazy opt-in':
      if (this.getUndecidedOptInVendors().length) {
        return new Promise(resolve => {
          this.emitter.once(`${vendor.name}:accepted`, () => resolve('fulfilled'));
          this.emitter.once(`${vendor.name}:denied`, () => resolve('failed'));
        });
      }

      if (this.persistence.read(vendor) === 'accepted') {
        return Promise.resolve('fulfilled');
      } else {
        return Promise.resolve('failed');
      }
    case 'external opt-out':
      if (this.persistence.read(vendor) === 'denied') {
        return Promise.resolve('failed');
      }
      return Promise.resolve('fulfilled');
    case 'skip':
      return Promise.resolve('fulfilled');
    default: // should not be used
      return null;
    }
  }

  requireAccepted(vendorName) {
    if (this.inEditor) {
      return Promise.resolve('fulfilled');
    }

    const vendor = this.findVendor(vendorName, 'require consent for');

    if (vendor.paradigm === 'opt-in' || vendor.paradigm === 'lazy opt-in') {
      if (this.getUndecidedOptInVendors().length ||
          this.persistence.read(vendor) !== 'accepted') {
        return new Promise(resolve => {
          this.emitter.once(`${vendor.name}:accepted`, () => resolve('fulfilled'));
        });
      }

      return Promise.resolve('fulfilled');
    }
    else {
      return this.require(vendorName);
    }
  }

  requested() {
    return this.requestedPromise;
  }

  accept(vendorName) {
    const vendor = this.findVendor(vendorName, 'accept');

    this.persistence.update(vendor, true);
    this.emitter.trigger(`${vendor.name}:accepted`);
  }

  deny(vendorName) {
    const vendor = this.findVendor(vendorName, 'deny');

    this.persistence.update(vendor, false);
  }

  getRequestedVendors() {
    return this.vendors.filter((vendor) => {
      return vendor.paradigm !== 'skip';
    });
  }

  getUndecidedOptInVendors() {
    return this.vendors.filter((vendor) => {
      return vendor.paradigm === 'opt-in' &&
        this.persistence.read(vendor) === 'undecided';
    });
  }

  triggerDecisionEvents() {
    this.vendors
      .filter((vendor) => {
        return vendor.paradigm !== 'skip';
      })
      .forEach((vendor) => {
        this.emitter.trigger(`${vendor.name}:${this.persistence.read(vendor)}`);
      });
  }

  findVendor(vendorName, actionForErrorMessage) {
    const vendor = this.vendors.find(vendor => vendor.name === vendorName);

    if (!vendor) {
      throw new Error(`Cannot ${actionForErrorMessage} unknown vendor "${vendorName}". ` +
                      'Consider using consent.registerVendor.');
    }

    return vendor;
  }

  withState(vendors, {applyDefaults} = {}) {
    return vendors.map((vendor) => {
      const state = this.persistence.read(vendor);

      return {
        ...vendor,
        state: state === 'undecided' && applyDefaults ? this.getDefaultState(vendor) : state
      };
    });
  }

  getDefaultState(vendor) {
    if (vendor.paradigm === 'external opt-out') {
      return 'accepted';
    }

    return 'undecided';
  }
}

Consent.create = function() {
  const inEditor = typeof PAGEFLOW_EDITOR !== 'undefined' && PAGEFLOW_EDITOR;
  return new Consent({cookies, inEditor});
};