Skip to content

Commit

Permalink
MWPW-131211 Personalization Entitlement Support (adobecom#1104)
Browse files Browse the repository at this point in the history
Co-authored-by: Sunil Kamat <107644736+sukamat@users.noreply.github.com>
  • Loading branch information
2 people authored and narcis-radu committed Sep 6, 2023
1 parent 61f7e72 commit 9bd6dba
Show file tree
Hide file tree
Showing 6 changed files with 158 additions and 50 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const getQueryParameters = (params) => {

const emptyEntitlements = () => ({
clouds: {},
arrangment_codes: {},
arrangement_codes: {},
fulfilled_codes: {},
offer_families: {},
offers: {},
Expand All @@ -47,7 +47,7 @@ const mapSubscriptionCodes = (allOffers) => {

const {
clouds,
arrangment_codes,
arrangement_codes,
fulfilled_codes,
offer_families,
offers,
Expand All @@ -66,7 +66,7 @@ const mapSubscriptionCodes = (allOffers) => {
}

if (offer.product_arrangement_code) {
arrangment_codes[offer.product_arrangement_code] = true;
arrangement_codes[offer.product_arrangement_code] = true;
}

if (Array.isArray(fulfilled_items)) {
Expand All @@ -86,7 +86,7 @@ const mapSubscriptionCodes = (allOffers) => {

return {
clouds,
arrangment_codes,
arrangement_codes,
fulfilled_codes,
offer_families,
offers,
Expand Down
174 changes: 140 additions & 34 deletions libs/features/personalization/personalization.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
/* eslint-disable no-console */
import { createTag, getConfig, loadLink, loadScript, updateConfig } from '../../utils/utils.js';
import {
createTag, getConfig, loadIms, loadLink, loadScript, updateConfig,
} from '../../utils/utils.js';

const CLASS_EL_DELETE = 'p13n-deleted';
const CLASS_EL_REPLACE = 'p13n-replaced';
const LS_ENT_KEY = 'milo:entitlements';
const LS_ENT_EXPIRE_KEY = 'milo:entitlements:expire';
const ENT_CACHE_EXPIRE = 1000 * 60 * 60 * 3; // 3 hours
const ENT_CACHE_REFRESH = 1000 * 60 * 3; // 3 minutes
const PAGE_URL = new URL(window.location.href);

/* c8 ignore start */
export const PERSONALIZATION_TAGS = {
all: () => true,
chrome: () => navigator.userAgent.includes('Chrome') && !navigator.userAgent.includes('Mobile'),
firefox: () => navigator.userAgent.includes('Firefox') && !navigator.userAgent.includes('Mobile'),
android: () => navigator.userAgent.includes('Android'),
Expand All @@ -16,10 +23,18 @@ export const PERSONALIZATION_TAGS = {
darkmode: () => window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches,
lightmode: () => !PERSONALIZATION_TAGS.darkmode(),
};

export const ENTITLEMENT_TAGS = {
photoshop: (ents) => ents.photoshop_cc,
lightroom: (ents) => ents.lightroom_cc,
};
/* c8 ignore stop */

const personalizationKeys = Object.keys(PERSONALIZATION_TAGS);
const entitlementKeys = Object.keys(ENTITLEMENT_TAGS);

// Replace any non-alpha chars except comma, space and hyphen
const RE_KEY_REPLACE = /[^a-z0-9\- ,=]/g;
const RE_KEY_REPLACE = /[^a-z0-9\- _,=]/g;

const MANIFEST_KEYS = [
'action',
Expand All @@ -45,7 +60,8 @@ const createFrag = (url, manifestId) => {
const COMMANDS = {
insertcontentafter: (el, target, manifestId) => el
.insertAdjacentElement('afterend', createFrag(target, manifestId)),
insertcontentbefore: (el, target, manifestId) => el.insertAdjacentElement('beforebegin', createFrag(target, manifestId)),
insertcontentbefore: (el, target, manifestId) => el
.insertAdjacentElement('beforebegin', createFrag(target, manifestId)),
removecontent: (el, target, manifestId) => {
if (target === 'false') return;
if (manifestId) {
Expand Down Expand Up @@ -83,7 +99,7 @@ const fetchData = async (url, type = DATA_TYPE.JSON) => {
}
return await resp[type]();
} catch (e) {
/* c8 ignore next 2 */
/* c8 ignore next 3 */
console.log(`Error loading content: ${url}`, e.message || e);
}
return null;
Expand Down Expand Up @@ -117,7 +133,6 @@ function normalizePath(p) {
}
return path;
}
/* c8 ignore stop */

const matchGlob = (searchStr, inputStr) => {
const pattern = searchStr.replace(/\*\*/g, '.*');
Expand All @@ -135,17 +150,7 @@ export async function replaceInner(path, element) {
element.innerHTML = html;
return true;
}

const checkForParamMatch = (paramStr) => {
const [name, val] = paramStr.split('param-')[1].split('=');
if (!name) return false;
const searchParamVal = PAGE_URL.searchParams.get(name);
if (searchParamVal !== null) {
if (val) return val === searchParamVal;
return true; // if no val is set, just check for existence of param
}
return false;
};
/* c8 ignore stop */

const setMetadata = (metadata) => {
const { selector, val } = metadata;
Expand Down Expand Up @@ -185,7 +190,7 @@ function handleCommands(commands, manifestId, rootEl = document) {

COMMANDS[cmd.action](selectorEl, cmd.target, manifestId);
} else {
/* c8 ignore next */
/* c8 ignore next 2 */
console.log('Invalid command found: ', cmd);
}
});
Expand Down Expand Up @@ -251,6 +256,7 @@ export function parseConfig(data) {
return null;
}

/* c8 ignore start */
function parsePlaceholders(placeholders, config, selectedVariantName = '') {
if (!placeholders?.length || selectedVariantName === 'no changes') return config;
const valueNames = [
Expand All @@ -271,30 +277,128 @@ function parsePlaceholders(placeholders, config, selectedVariantName = '') {
return config;
}

function getPersonalizationVariant(manifestPath, variantNames = [], variantLabel = null) {
const fetchEntitlements = async () => {
const [{ default: getUserEntitlements }] = await Promise.all([
import('../../blocks/global-navigation/utilities/getUserEntitlements.js'),
loadIms(),
]);
return getUserEntitlements();
};

const setEntLocalStorage = (ents) => {
localStorage.setItem(LS_ENT_KEY, JSON.stringify(ents));
localStorage.setItem(LS_ENT_EXPIRE_KEY, Date.now());
};

const loadEntsFromLocalStorage = () => {
const ents = localStorage.getItem(LS_ENT_KEY);
const expireDate = localStorage.getItem(LS_ENT_EXPIRE_KEY);
const now = Date.now();
if (!ents || !expireDate || (now - expireDate) > ENT_CACHE_EXPIRE) return null;
if ((now - expireDate) > ENT_CACHE_REFRESH) {
// refresh entitlements in background
setTimeout(() => {
fetchEntitlements().then((newEnts) => {
setEntLocalStorage(newEnts);
});
}, 5000);
}
return JSON.parse(ents);
};

const clearEntLocalStorage = () => {
localStorage.removeItem(LS_ENT_KEY);
localStorage.removeItem(LS_ENT_EXPIRE_KEY);
};

export const getEntitlements = (() => {
let ents;
return (async () => {
if (window.adobeIMS && !window.adobeIMS.isSignedInUser()) {
clearEntLocalStorage();
return {};
}
if (!ents) {
ents = loadEntsFromLocalStorage();
}
if (!ents) {
ents = await fetchEntitlements();
setEntLocalStorage(ents);
}
return ents;
});
})();

const getFlatEntitlements = async () => {
const ents = await getEntitlements();
return {
...ents.arrangement_codes,
...ents.clouds,
...ents.fulfilled_codes,
};
};

const checkForEntitlementMatch = (name, entitlements) => {
const entName = name.split('ent-')[1];
if (!entName) return false;
return entitlements[entName];
};
/* c8 ignore stop */

const checkForParamMatch = (paramStr) => {
const [name, val] = paramStr.split('param-')[1].split('=');
if (!name) return false;
const searchParamVal = PAGE_URL.searchParams.get(name);
if (searchParamVal !== null) {
if (val) return val === searchParamVal;
return true; // if no val is set, just check for existence of param
}
return false;
};

async function getPersonalizationVariant(manifestPath, variantNames = [], variantLabel = null) {
const config = getConfig();
let manifestFound = false;
if (config.mep?.override !== '') {
config.mep?.override.split(',').forEach((item) => {
let manifest;
/* c8 ignore start */
config.mep?.override.split(',').some((item) => {
const pair = item.trim().split('--');
if (pair[0] === manifestPath && pair.length > 1) {
// eslint-disable-next-line prefer-destructuring
manifestFound = pair[1];
[, manifest] = pair;
return true;
}
return false;
});
if (manifestFound) return manifestFound;
/* c8 ignore stop */
if (manifest) return manifest;
}

const tagNames = Object.keys(PERSONALIZATION_TAGS);
const matchingVariant = variantNames.find((variant) => {
// handle multiple variants that are space / comma delimited
const names = variant.split(',').map((v) => v.trim()).filter(Boolean);
return names.some((name) => {
if (name === variantLabel) return true;
if (name.startsWith('param-')) return checkForParamMatch(name);
return tagNames.includes(name) && PERSONALIZATION_TAGS[name]();
});
});
const variantInfo = variantNames.reduce((acc, name) => {
const vNames = name.split(',').map((v) => v.trim()).filter(Boolean);
acc[name] = vNames;
acc.allNames = [...acc.allNames, ...vNames];
return acc;
}, { allNames: [] });

const hasEntitlementPrefix = variantInfo.allNames.some((name) => name.startsWith('ent-'));
const hasEntitlementTag = entitlementKeys.some((tag) => variantInfo.allNames.includes(tag));

let entitlements = {};
if (hasEntitlementPrefix || hasEntitlementTag) {
entitlements = await getFlatEntitlements();
}

const matchVariant = (name) => {
if (name === variantLabel) return true;
if (name.startsWith('param-')) return checkForParamMatch(name);
if (name.startsWith('ent-')) return checkForEntitlementMatch(name, entitlements);
if (entitlementKeys.includes(name)) {
return ENTITLEMENT_TAGS[name](entitlements);
}
return personalizationKeys.includes(name) && PERSONALIZATION_TAGS[name]();
};

const matchingVariant = variantNames.find((variant) => variantInfo[variant].some(matchVariant));
return matchingVariant;
}

Expand All @@ -319,7 +423,7 @@ export async function getPersConfig(name, variantLabel, manifestData, manifestPa
return null;
}

const selectedVariantName = getPersonalizationVariant(
const selectedVariantName = await getPersonalizationVariant(
manifestPath,
config.variantNames,
variantLabel,
Expand Down Expand Up @@ -434,6 +538,8 @@ export async function applyPers(manifests) {
decoratePreviewCheck(config, []);
return;
}

getEntitlements();
const cleanedManifests = cleanManifestList(manifests);

let results = [];
Expand Down
10 changes: 6 additions & 4 deletions libs/martech/martech.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ const getDtmLib = (env) => ({
url:
env.name === 'prod'
? env.consumer?.marTechUrl || 'https://assets.adobedtm.com/d4d114c60e50/a0e989131fd5/launch-5dd5dd2177e6.min.js'
// TODO: This is a custom launch script for milo-target - update before merging to main
: env.consumer?.marTechUrl || 'https://assets.adobedtm.com/d4d114c60e50/a0e989131fd5/launch-2c94beadc94f-development.min.js',
: env.consumer?.marTechUrl || 'https://assets.adobedtm.com/d4d114c60e50/a0e989131fd5/launch-a27b33fc2dc0-development.min.js',
});

export default async function init({ persEnabled = false, persManifests }) {
Expand Down Expand Up @@ -135,9 +134,12 @@ export default async function init({ persEnabled = false, persManifests }) {
if (persEnabled) {
const targetManifests = await getTargetPersonalization();
if (targetManifests || persManifests?.length) {
const { preloadManifests } = await import('../features/personalization/manifest-utils.js');
const [{ preloadManifests }, { applyPers, getEntitlements }] = await Promise.all([
import('../features/personalization/manifest-utils.js'),
import('../features/personalization/personalization.js'),
]);
getEntitlements();
const manifests = preloadManifests({ targetManifests, persManifests });
const { applyPers } = await import('../features/personalization/personalization.js');
await applyPers(manifests);
}
}
Expand Down
12 changes: 6 additions & 6 deletions libs/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,11 +89,6 @@ const AUTO_BLOCKS = [
{ 'offer-preview': '/tools/commerce' },
];
const ENVS = {
local: {
name: 'local',
edgeConfigId: '8d2805dd-85bf-4748-82eb-f99fdad117a6',
pdfViewerClientId: '600a4521c23d4c7eb9c7b039bee534a0',
},
stage: {
name: 'stage',
ims: 'stg1',
Expand All @@ -113,6 +108,11 @@ const ENVS = {
pdfViewerClientId: '3c0a5ddf2cc04d3198d9e48efc390fa9',
},
};
ENVS.local = {
...ENVS.stage,
name: 'local',
};

const LANGSTORE = 'langstore';

const PAGE_URL = new URL(window.location.href);
Expand Down Expand Up @@ -752,7 +752,7 @@ async function checkForPageMods() {
if (targetEnabled) {
await loadMartech({ persEnabled: true, persManifests, targetMd });
} else if (persManifests.length) {
// load the personalization only
loadIms().catch(() => {});
const { preloadManifests } = await import('../features/personalization/manifest-utils.js');
const manifests = preloadManifests({ persManifests }, { getConfig, loadLink });

Expand Down
2 changes: 1 addition & 1 deletion test/blocks/global-navigation/mocks/subscriptionsAll.js
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const formatted = {
document_cloud: true,
experience_cloud: true,
},
arrangment_codes: {
arrangement_codes: {
dm_data_launch_ml: true,
dm_analytics_enterprise_ml: true,
dm_audience_manager_enterprise_ml: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { res, formatted } from '../mocks/subscriptionsAll.js';

const emptyEntitlements = () => ({
clouds: {},
arrangment_codes: {},
arrangement_codes: {},
fulfilled_codes: {},
offer_families: {},
offers: {},
Expand Down

0 comments on commit 9bd6dba

Please sign in to comment.