diff --git a/libs/features/mep/addons/event.js b/libs/features/mep/addons/event.js index 1e1bcd9e2db..59fda6c29e8 100644 --- a/libs/features/mep/addons/event.js +++ b/libs/features/mep/addons/event.js @@ -1,49 +1,110 @@ -import { isSignedOut, getConfig, getMepEnablement, loadIms } from '../../../utils/utils.js'; -import { getCookie } from '../../../martech/helpers.js'; +import { isSignedOut, getConfig, getMepEnablement, loadIms, getCookie } from '../../../utils/utils.js'; + +const defaultReturn = { isRegistered: false }; + +// Persist only what personalization reads; never the RF authToken/userKey. +const store = window.localStorage; +const TTL_REGISTERED = 24 * 60 * 60 * 1000; +const TTL_UNREGISTERED = 3 * 60 * 1000; +const cacheKey = (eventCode, userId) => `mep-event:${eventCode}:${userId}`; + +// TODO(MWPW-199051): enable once VEAL confirms the redirect-cookie name/domain. +// When enabling, clearRegisteredFlag must set domain= to match — a host-only +// delete won't clear a .adobe.com cookie, leaving the flag stuck. +const REDIRECT_INVALIDATION_ENABLED = false; +const justRegistered = (eventCode) => REDIRECT_INVALIDATION_ENABLED + && getCookie(`feds_${eventCode}_registeredByRedirect`) === 'true'; +/* c8 ignore next 3 */ +const clearRegisteredFlag = (eventCode) => { + document.cookie = `feds_${eventCode}_registeredByRedirect=; Max-Age=0; path=/`; +}; + +function readCache(eventCode, userId) { + try { + const raw = store.getItem(cacheKey(eventCode, userId)); + if (!raw) return null; + const { isRegistered, inPersonAttendee, exp } = JSON.parse(raw); + if (!exp || Date.now() > exp) return null; + return { isRegistered, inPersonAttendee }; + } catch { + return null; + } +} + +function writeCache(eventCode, userId, data) { + try { + const ttl = data.isRegistered ? TTL_REGISTERED : TTL_UNREGISTERED; + store.setItem(cacheKey(eventCode, userId), JSON.stringify({ + isRegistered: !!data.isRegistered, + inPersonAttendee: !!data.inPersonAttendee, + exp: Date.now() + ttl, + })); + } catch { + // storage unavailable — non-fatal + } +} async function getUserId() { - if (isSignedOut() && !getMepEnablement('signedIn')) return false; - if (getMepEnablement('userId') !== 'undefined') return getMepEnablement('userId'); - /* c8 ignore next 3 */ - await loadIms(); - const { userId } = await window.adobeIMS.getProfile(); - return userId; + const signedInEnable = getMepEnablement('signedIn') || getMepEnablement('signedin'); + if (isSignedOut() && !signedInEnable) return false; + const userIdEnable = getMepEnablement('userId') || getMepEnablement('userid'); + if (userIdEnable && userIdEnable !== 'undefined') return userIdEnable; + /* c8 ignore start */ + try { + await loadIms(); + const { userId } = await window.adobeIMS.getProfile(); + return userId; + } catch { + return false; + } + /* c8 ignore stop */ } -async function fetchFromRainfocus(eventId) { + +async function fetchFromRainfocus(eventCode) { const userId = await getUserId(); - if (!userId) return {}; + if (!userId) return defaultReturn; + + /* c8 ignore next 3 */ + if (justRegistered(eventCode)) { + store.removeItem(cacheKey(eventCode, userId)); + clearRegisteredFlag(eventCode); + } else { + const cached = readCache(eventCode, userId); + if (cached) return cached; + } const accessToken = window.adobeIMS.getAccessToken()?.token; - if (!accessToken) return {}; + if (!accessToken) return defaultReturn; const domainSuffix = getConfig()?.env?.name === 'prod' ? '' : '.stage'; - const url = `https://www${domainSuffix}.adobe.com/events/api/rf-auth-seq-generic/${eventId}?user_id=${encodeURIComponent(userId)}`; + const url = `https://www${domainSuffix}.adobe.com/events/api/rf-auth-seq-generic/${eventCode}?user_id=${encodeURIComponent(userId)}`; try { const response = await fetch(url, { method: 'GET', headers: { Authorization: `Bearer ${accessToken}` }, credentials: 'same-origin', }); - if (response.ok) return response.json(); + if (response.ok) { + // RF returns {} (not { isRegistered: false }) when not registered. + const data = { isRegistered: false, ...(await response.json()) }; + writeCache(eventCode, userId, data); + return data; + } window.lana?.log(`Unable to fetch from Rainfocus: ${response.statusText}`, { tags: 'mep-event', severity: 'error', }); - return {}; + return defaultReturn; } catch (e) { window.lana?.log(`Unable to fetch from Rainfocus: ${e.toString()}`, { tags: 'mep-event', severity: 'error', }); - return {}; + return defaultReturn; } } -export default async function init(eventId) { - if (eventId === true) return eventId; - - const consentCookieValue = getCookie('OptanonConsent'); - if (consentCookieValue?.includes('C0002:0')) return false; - const eventDetails = await fetchFromRainfocus(eventId); - return eventDetails?.isRegistered === true; +export default async function init(eventCode) { + if (eventCode === true) return { isRegistered: eventCode }; + return fetchFromRainfocus(eventCode); } diff --git a/libs/features/personalization/personalization.js b/libs/features/personalization/personalization.js index 94a2bd5d6ac..af471cdf2f1 100644 --- a/libs/features/personalization/personalization.js +++ b/libs/features/personalization/personalization.js @@ -1093,7 +1093,7 @@ async function getPersonalizationVariant( if (userEntitlements?.includes(name)) return true; const { lob, event } = config.mep.promises; if (lob && lob === name.split('lob-')[1]?.toLowerCase()) return true; - if (name === 'registered' && event) return true; + if (name === 'registered' && event?.isRegistered) return true; return PERSONALIZATION_KEYS.includes(name) && PERSONALIZATION_TAGS[name](); }; diff --git a/libs/utils/utils.js b/libs/utils/utils.js index fe7f90cb39c..94dc5d6bcbb 100644 --- a/libs/utils/utils.js +++ b/libs/utils/utils.js @@ -2119,7 +2119,7 @@ export function enablePersonalizationV2() { } export function loadMepAddons() { - const mepAddons = ['lob', 'event-id']; + const mepAddons = ['lob', 'event-code']; const promises = {}; mepAddons.forEach((addon) => { const enablement = getMepEnablement(addon); diff --git a/test/features/mep/addons/event.test.js b/test/features/mep/addons/event.test.js index 357d4ad05f4..062e5acedd3 100644 --- a/test/features/mep/addons/event.test.js +++ b/test/features/mep/addons/event.test.js @@ -27,34 +27,36 @@ const setMetadata = (name, content) => { describe('event', () => { beforeEach(() => { document.head.innerHTML = ''; + localStorage.clear(); setMetadata('signedIn', 'on'); setCookie('OptanonConsent', 'C0002:1'); window.adobeIMS = { getAccessToken: () => ({ token: '1234567890' }) }; }); + afterEach(() => { + setCookie('feds_adobe-max-2025_registeredByRedirect', '; Max-Age=0'); + }); it('should return true when sending true into init', async () => { const event = await init(true); - expect(event).to.equal(true); - }); - it('should return false when consent cookie is set to C0002:0', async () => { - setCookie('OptanonConsent', 'C0002:0'); - const event = await init('adobe-max-2025'); - expect(event).to.equal(false); + expect(event.isRegistered).to.equal(true); }); it('should return false when the user is signed out', async () => { document.head.innerHTML = ''; const event = await init('adobe-max-2025'); - expect(event).to.equal(false); + expect(event.isRegistered).to.equal(false); }); - it('should return false when userId is set to off', async () => { - setMetadata('userId', 'off'); + it('should read enablements from lowercase DA metadata', async () => { + document.head.innerHTML = ''; + setMetadata('signedin', 'on'); + setMetadata('userid', '1234567890'); + setFetchResponse({ ok: true, isRegistered: true }); const event = await init('adobe-max-2025'); - expect(event).to.equal(false); + expect(event.isRegistered).to.equal(true); }); it('should return false when getAccessToken returns an empty object', async () => { setMetadata('userId', '1234567890'); window.adobeIMS.getAccessToken = () => Promise.resolve({}); const event = await init('adobe-max-2025'); - expect(event).to.equal(false); + expect(event.isRegistered).to.equal(false); }); it('should return false when api returns false', async () => { setMetadata('userId', '1234567890'); @@ -63,7 +65,7 @@ describe('event', () => { isRegistered: false, }); const event = await init('adobe-max-2025'); - expect(event).to.equal(false); + expect(event.isRegistered).to.equal(false); }); it('should return true when api returns true', async () => { setMetadata('userId', '1234567890'); @@ -72,6 +74,32 @@ describe('event', () => { isRegistered: true, }); const event = await init('adobe-max-2025'); - expect(event).to.equal(true); + expect(event.isRegistered).to.equal(true); + }); + it('should serve a cached result without re-calling the API', async () => { + setMetadata('userId', '1234567890'); + setFetchResponse({ ok: true, isRegistered: true }); + const first = await init('adobe-max-2025'); + expect(first.isRegistered).to.equal(true); + // A cache hit must ignore a now-changed API response. + setFetchResponse({ ok: true, isRegistered: false }); + const second = await init('adobe-max-2025'); + expect(second.isRegistered).to.equal(true); + }); + it('should normalize an empty API response to isRegistered false', async () => { + setMetadata('userId', '1234567890'); + setFetchResponse({}); + const event = await init('adobe-max-2025'); + expect(event.isRegistered).to.equal(false); + }); + // Un-skip when REDIRECT_INVALIDATION_ENABLED flips on (MWPW-199051). + it.skip('should bypass the cache when the registration-redirect cookie is set', async () => { + setMetadata('userId', '1234567890'); + setFetchResponse({ ok: true, isRegistered: false }); + await init('adobe-max-2025'); + setCookie('feds_adobe-max-2025_registeredByRedirect', 'true'); + setFetchResponse({ ok: true, isRegistered: true }); + const event = await init('adobe-max-2025'); + expect(event.isRegistered).to.equal(true); }); }); diff --git a/test/utils/utils-mep.test.js b/test/utils/utils-mep.test.js index 935fa1c0b6d..7abde7ac304 100644 --- a/test/utils/utils-mep.test.js +++ b/test/utils/utils-mep.test.js @@ -120,6 +120,16 @@ describe('MEP Utils', () => { console.log(promises); expect(promises.lob).to.be.a('promise'); }); + it('loads event from event-code metadata', async () => { + document.head.innerHTML = ''; + const promises = loadMepAddons(); + expect(promises.event).to.be.a('promise'); + }); + it('does not load event from the legacy event-id metadata', async () => { + document.head.innerHTML = ''; + const promises = loadMepAddons(); + expect(promises.event).to.be.undefined; + }); afterEach(() => { document.head.innerHTML = ''; });