diff --git a/test/blocks/unity-verb-marquee/mocks/body-word-to-pdf.html b/test/blocks/unity-verb-marquee/mocks/body-word-to-pdf.html new file mode 100644 index 000000000..8bf3cdbe6 --- /dev/null +++ b/test/blocks/unity-verb-marquee/mocks/body-word-to-pdf.html @@ -0,0 +1,12 @@ +
+
+
+
+
+

Word to PDF

+
+
+
+
+
+
diff --git a/test/blocks/unity-verb-marquee/mocks/placeholders.json b/test/blocks/unity-verb-marquee/mocks/placeholders.json new file mode 100644 index 000000000..11539bb95 --- /dev/null +++ b/test/blocks/unity-verb-marquee/mocks/placeholders.json @@ -0,0 +1,17 @@ +{ + "total": 10, + "offset": 0, + "limit": 10, + "data": [ + { "key": "verb-widget-cta", "value": "Select a file" }, + { "key": "verb-widget-word-to-pdf-dragndrop-text", "value": "or drag and drop a file" }, + { "key": "verb-widget-word-to-pdf-file-limit", "value": "PDF or Word, up to 100 MB" }, + { "key": "verb-marquee-legal", "value": "By using this service, you agree to the Adobe Terms of Use and Privacy Policy." }, + { "key": "verb-marquee-legal-2", "value": "" }, + { "key": "verb-widget-terms-of-use", "value": "Terms of Use" }, + { "key": "verb-widget-privacy-policy", "value": "Privacy Policy" }, + { "key": "verb-widget-tool-tip", "value": "Your files are secure with us." }, + { "key": "verb-widget-privacy-policy-url", "value": "https://www.adobe.com/privacy/policy.html" }, + { "key": "verb-widget-terms-of-use-url", "value": "https://www.adobe.com/legal/terms.html" } + ] +} diff --git a/test/blocks/unity-verb-marquee/unity-verb-marquee.test.js b/test/blocks/unity-verb-marquee/unity-verb-marquee.test.js new file mode 100644 index 000000000..1f6a3f10c --- /dev/null +++ b/test/blocks/unity-verb-marquee/unity-verb-marquee.test.js @@ -0,0 +1,177 @@ +/* eslint-disable compat/compat */ +import { readFile } from '@web/test-runner-commands'; +import { expect } from '@esm-bundle/chai'; +import sinon from 'sinon'; +import { getConfig, setConfig } from 'https://main--milo--adobecom.aem.live/libs/utils/utils.js'; // eslint-disable-line import/no-unresolved, import/order +import { delay } from '../../helpers/waitfor.js'; + +const { default: init } = await import( + '../../../acrobat/blocks/unity-verb-marquee/unity-verb-marquee.js' +); + +describe('unity-verb-marquee block', () => { + let xhr; + let placeholders; + + beforeEach(async () => { + sinon.stub(window, 'fetch'); + window.fetch.callsFake((x) => { + if (x.endsWith('.svg')) { + return window.fetch.wrappedMethod.call(window, x); + } + return Promise.resolve(); + }); + const placeholdersText = await readFile({ path: './mocks/placeholders.json' }); + placeholders = JSON.parse(placeholdersText); + + window.mph = {}; + placeholders.data.forEach((item) => { + window.mph[item.key] = item.value; + }); + xhr = sinon.useFakeXMLHttpRequest(); + document.body.innerHTML = await readFile({ path: './mocks/body-word-to-pdf.html' }); + window.adobeIMS = { isSignedInUser: () => false }; + window.lana = { log: sinon.spy() }; + }); + + afterEach(() => { + xhr.restore(); + sinon.restore(); + }); + + it('init unity-verb-marquee', async () => { + const conf = getConfig(); + setConfig({ ...conf, locale: { prefix: '' } }); + const block = document.body.querySelector('.unity-verb-marquee'); + await init(block); + expect(document.querySelector('.unity-verb-marquee .acrobat-icon svg')).to.exist; + expect(document.querySelector('.unity-verb-marquee .unity-verb-marquee-cta')).to.exist; + expect(document.querySelector('.unity-verb-marquee .unity-verb-marquee-dropzone')).to.exist; + }); + + it('show error toast', async () => { + const conf = getConfig(); + setConfig({ ...conf, locale: { prefix: '' } }); + const block = document.body.querySelector('.unity-verb-marquee'); + await init(block); + await delay(100); + + window.analytics = { verbAnalytics: sinon.spy(), sendAnalyticsToSplunk: sinon.spy() }; + + block.dispatchEvent(new CustomEvent('unity:show-error-toast', { + detail: { + code: 'error_only_accept_one_file', + info: 'Test error info', + metaData: 'metadata', + errorData: 'errorData', + sendToSplunk: true, + message: 'Test error message', + }, + })); + + expect(window.analytics.verbAnalytics.called).to.be.true; + expect(window.analytics.sendAnalyticsToSplunk.called).to.be.true; + expect(window.lana.log.called).to.be.true; + }); + + it('error toast does not auto-close after 5 seconds', async () => { + const conf = getConfig(); + setConfig({ ...conf, locale: { prefix: '' } }); + const block = document.body.querySelector('.unity-verb-marquee'); + await init(block); + await delay(100); + + const clock = sinon.useFakeTimers(); + block.dispatchEvent(new CustomEvent('unity:show-error-toast', { detail: { code: 'error_generic', message: 'Test error', sendToSplunk: false } })); + + const errorState = block.querySelector('.error'); + expect(errorState.classList.contains('hide')).to.be.false; + + clock.tick(6000); + expect(errorState.classList.contains('hide')).to.be.false; + }); + + it('error toast closes when clicking outside the toast', async () => { + const conf = getConfig(); + setConfig({ ...conf, locale: { prefix: '' } }); + const block = document.body.querySelector('.unity-verb-marquee'); + await init(block); + await delay(100); + + block.dispatchEvent(new CustomEvent('unity:show-error-toast', { detail: { code: 'error_generic', message: 'Test error', sendToSplunk: false } })); + + const errorState = block.querySelector('.error'); + expect(errorState.classList.contains('hide')).to.be.false; + + await delay(50); + document.body.dispatchEvent(new MouseEvent('click', { bubbles: true })); + expect(errorState.classList.contains('hide')).to.be.true; + }); + + it('error toast does not close on click inside toast', async () => { + const conf = getConfig(); + setConfig({ ...conf, locale: { prefix: '' } }); + const block = document.body.querySelector('.unity-verb-marquee'); + await init(block); + await delay(100); + + block.dispatchEvent(new CustomEvent('unity:show-error-toast', { detail: { code: 'error_generic', message: 'Test error', sendToSplunk: false } })); + + const errorState = block.querySelector('.error'); + expect(errorState.classList.contains('hide')).to.be.false; + + errorState.dispatchEvent(new MouseEvent('click', { bubbles: true })); + expect(errorState.classList.contains('hide')).to.be.false; + }); + + it('error close button closes toast on Enter key', async () => { + const conf = getConfig(); + setConfig({ ...conf, locale: { prefix: '' } }); + const block = document.body.querySelector('.unity-verb-marquee'); + await init(block); + await delay(100); + + block.dispatchEvent(new CustomEvent('unity:show-error-toast', { detail: { code: 'error_generic', message: 'Test error', sendToSplunk: false } })); + + const errorState = block.querySelector('.error'); + const errorCloseBtn = block.querySelector('.unity-verb-marquee-errorBtn'); + expect(errorState.classList.contains('hide')).to.be.false; + + errorCloseBtn.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); + expect(errorState.classList.contains('hide')).to.be.true; + }); + + it('error close button closes toast on Space key', async () => { + const conf = getConfig(); + setConfig({ ...conf, locale: { prefix: '' } }); + const block = document.body.querySelector('.unity-verb-marquee'); + await init(block); + await delay(100); + + block.dispatchEvent(new CustomEvent('unity:show-error-toast', { detail: { code: 'error_generic', message: 'Test error', sendToSplunk: false } })); + + const errorState = block.querySelector('.error'); + const errorCloseBtn = block.querySelector('.unity-verb-marquee-errorBtn'); + expect(errorState.classList.contains('hide')).to.be.false; + + errorCloseBtn.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true })); + expect(errorState.classList.contains('hide')).to.be.true; + }); + + it('error close button hides error toast', async () => { + const conf = getConfig(); + setConfig({ ...conf, locale: { prefix: '' } }); + const block = document.body.querySelector('.unity-verb-marquee'); + await init(block); + await delay(100); + + block.dispatchEvent(new CustomEvent('unity:show-error-toast', { detail: { code: 'error_generic', message: 'Test error', sendToSplunk: false } })); + + const errorState = block.querySelector('.error'); + const errorCloseBtn = block.querySelector('.unity-verb-marquee-errorBtn'); + expect(errorState.classList.contains('hide')).to.be.false; + + errorCloseBtn.click(); + expect(errorState.classList.contains('hide')).to.be.true; + }); +}); diff --git a/test/helpers/waitfor.js b/test/helpers/waitfor.js new file mode 100644 index 000000000..d3580ec2b --- /dev/null +++ b/test/helpers/waitfor.js @@ -0,0 +1,130 @@ +export const waitForElement = ( + selector, + { + options = { + childList: true, + subtree: true, + }, + rootEl = document.body, + textContent = '', + } = {}, +) => new Promise((resolve) => { + const el = document.querySelector(selector); + + if (el) { + resolve(el); + return; + } + + const observer = new MutationObserver((mutations, obsv) => { + mutations.forEach((mutation) => { + const nodes = [...mutation.addedNodes]; + nodes.some((node) => { + if (node.matches && node.matches(selector)) { + if (textContent && node.textContent !== textContent) return false; + + obsv.disconnect(); + resolve(node); + return true; + } + + // check for child in added node + const treeWalker = document.createTreeWalker(node); + let { currentNode } = treeWalker; + while (currentNode) { + if (currentNode.matches && currentNode.matches(selector)) { + obsv.disconnect(); + resolve(currentNode); + return true; + } + currentNode = treeWalker.nextNode(); + } + return false; + }); + }); + }); + + observer.observe(rootEl, options); +}); + +export const waitForUpdate = ( + el, + options = { + childList: true, + subtree: true, + }, +) => new Promise((resolve) => { + const observer = new MutationObserver((mutations, obsv) => { + obsv.disconnect(); + resolve(); + }); + observer.observe(el, options); +}); + +export const waitForRemoval = ( + selector, + options = { + childList: true, + subtree: false, + }, +) => new Promise((resolve) => { + const el = document.querySelector(selector); + + if (!el) { + resolve(); + return; + } + + const observer = new MutationObserver((mutations, obsv) => { + mutations.forEach((mutation) => { + const nodes = [...mutation.removedNodes]; + nodes.some((node) => { + if (node.matches(selector)) { + obsv.disconnect(); + resolve(); + return true; + } + return false; + }); + }); + }); + + observer.observe(el.parentElement, options); +}); + +/** + * Promise based setTimeout that can be await'd + * @param {int} timeOut time out in milliseconds + * @param {*} cb Callback function to call when time elapses + * @returns + */ +export const delay = (timeOut, cb) => new Promise((resolve) => { + setTimeout(() => { + resolve((cb && cb()) || null); + }, timeOut); +}); + +/** + * Waits for predicate function to be true or times out. + * @param {function} predicate Callback that returns boolean + * @param {number} timeout Timeout in milliseconds + * @param {number} interval Interval delay in milliseconds + * @returns {Promise} + */ +export function waitFor(predicate, timeout = 1000, interval = 100) { + return new Promise((resolve, reject) => { + if (predicate()) resolve(); + + const intervalId = setInterval(() => { + if (predicate()) { + clearInterval(intervalId); + resolve(); + } + }, interval); + + setTimeout(() => { + clearInterval(intervalId); + reject(new Error('Timed out waiting for predicate to be true')); + }, timeout); + }); +} \ No newline at end of file diff --git a/test/utils/experiment-provider.test.js b/test/utils/experiment-provider.test.js index 3a397cacd..25f1378f2 100644 --- a/test/utils/experiment-provider.test.js +++ b/test/utils/experiment-provider.test.js @@ -1,6 +1,6 @@ /* eslint-disable no-underscore-dangle */ import { expect } from '@esm-bundle/chai'; -import { getExperimentData, getDecisionScopesForVerb } from '../../unitylibs/utils/experiment-provider.js'; +import getExperimentData, { getDecisionScopesForVerb } from '../../unitylibs/utils/experiment-provider.js'; describe('getExperimentData', () => { // Helper function to setup mock with result and error diff --git a/unitylibs/blocks/unity-verb-marquee/unity-verb-marquee.css b/unitylibs/blocks/unity-verb-marquee/unity-verb-marquee.css new file mode 100644 index 000000000..3d183e873 --- /dev/null +++ b/unitylibs/blocks/unity-verb-marquee/unity-verb-marquee.css @@ -0,0 +1,951 @@ +:root { + --unity-verb-marquee-padding: 40px 16px; + --unity-verb-marquee-gap: 32px; +} + +.unity-verb-marquee { + position: relative; + padding: var(--unity-verb-marquee-padding); + overflow: hidden; + color: var(--color-white); + box-sizing: border-box; + display: flex; + flex-direction: column; +} + +.unity-verb-marquee-heading { + align-self: stretch; + font-family: "Adobe Clean Black", var(--body-font-family); + font-size: 44px; + font-style: normal; + font-weight: 900; + line-height: 0.98; + letter-spacing: -1.98px; + color: inherit; + margin: 0; + margin-top: -22px; + word-break: break-word; + overflow-wrap: break-word; + hyphens: auto; +} + +.unity-verb-marquee-header + .unity-verb-marquee-heading { + margin-top: -8px; +} + +.unity-verb-marquee.light, +.unity-verb-marquee.mobile-light, +.unity-verb-marquee.light .unity-verb-marquee-heading, +.unity-verb-marquee.mobile-light .unity-verb-marquee-heading { + color: var(--color-black); +} + +.unity-verb-marquee.mobile-dark { + color: var(--color-white); +} + +.unity-verb-marquee .foreground { + max-width: var(--grid-container-width); + min-width: var(--grid-container-width); + margin: 0 auto; +} + +.unity-verb-marquee-container { + width: 100%; + overflow: hidden; +} + +.unity-verb-marquee-row { + display: flex; + flex-direction: column; + gap: var(--unity-verb-marquee-gap); + align-items: flex-start; +} + +.unity-verb-marquee-col-left { + display: flex; + flex-direction: column; + gap: 32px; + width: 100%; + min-width: 0; + overflow-wrap: break-word; +} + +.unity-verb-marquee-col-right { + flex-shrink: 0; + width: 100%; + display: flex; + justify-content: flex-start; + align-items: center; +} + +.unity-verb-marquee-header { + display: flex; + align-items: center; + gap: 8px; +} + +.unity-verb-marquee .acrobat-icon { + display: inline-flex; + width: 31.5px; + height: 30.414px; + flex-shrink: 0; +} + +.unity-verb-marquee .acrobat-icon svg { + width: 100%; + height: 100%; +} + +.unity-verb-marquee-title { + font-size: 20.25px; + line-height: 1.25; + font-weight: 700; + letter-spacing: -0.42px; + color: inherit; + margin: 0; +} + +.verb-marquee-title-svg { + height: auto; + width: auto; +} + +.unity-verb-marquee-copy, +.unity-verb-marquee-copy-sub { + font-style: normal; + font-weight: 400; + line-height: 1.5; + color: inherit; + margin: 0; + opacity: 0.9; +} + +.unity-verb-marquee-copy { + font-size: 24px; +} + +.unity-verb-marquee-copy-sub { + font-size: 20px; + display: grid; + grid-template-columns: auto 1fr; + column-gap: 0.42em; + align-items: start; + max-width: 480px; +} + +.unity-verb-marquee-heading + .unity-verb-marquee-copy { + margin-top: -16px; +} + +.unity-verb-marquee.light .unity-verb-marquee-copy, +.unity-verb-marquee.mobile-light .unity-verb-marquee-copy { + color: var(--color-black); + opacity: 1; +} + +.unity-verb-marquee-col-left > .unity-verb-marquee-copy:has(+ .unity-verb-marquee-copy-sub) { + margin-bottom: -16px; +} + +.unity-verb-marquee-col-left > .unity-verb-marquee-copy-sub + .unity-verb-marquee-copy-sub { + margin-top: -32px; +} + +.unity-verb-marquee-copy-sub-icon { + grid-column: 1; + grid-row: 1; + width: 1em; + height: 1em; + display: block; + line-height: inherit; + margin-top: calc((1lh - 1em) / 2); +} + +.unity-verb-marquee-copy-sub-icon path { + fill: currentcolor; +} + +.unity-verb-marquee-copy-sub-label { + grid-column: 2; + grid-row: 1; + min-width: 0; +} + +.unity-verb-marquee.light .unity-verb-marquee-copy-sub, +.unity-verb-marquee.mobile-light .unity-verb-marquee-copy-sub { + color: #484848; + opacity: 1; +} + +.unity-verb-marquee-dropzone { + border: 1.043px dashed #8F8F8F; + border-radius: 24px; + padding: 34px 10px; + background: #393939; + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + gap: 16px; + cursor: pointer; + transition: all 0.2s ease; + box-sizing: border-box; + width: 100%; + align-self: stretch; +} + +.unity-verb-marquee-mobile-app .unity-verb-marquee-dropzone, +.unity-verb-marquee-trial .unity-verb-marquee-dropzone { + align-items: stretch; + cursor: default; +} + +.unity-verb-marquee-dropzone:hover, +.unity-verb-marquee-dropzone.dragging, +.unity-verb-marquee.dragging-block .unity-verb-marquee-dropzone { + border-color: #B8B8B8; + background: #4D4D4D; + border-style: dashed; +} + +.unity-verb-marquee.light .unity-verb-marquee-dropzone, +.unity-verb-marquee.mobile-light .unity-verb-marquee-dropzone, +.unity-verb-marquee.tablet-light .unity-verb-marquee-dropzone, +.unity-verb-marquee.desktop-light .unity-verb-marquee-dropzone { + border: 1px dashed #8F8F8F; + border-radius: 24px; + background-color: #F7F7F7; + backdrop-filter: blur(10px); +} + +.unity-verb-marquee.unity-verb-marquee-mobile-app .unity-verb-marquee-dropzone { + border: none !important; + background: transparent !important; + background-color: transparent !important; + border-radius: 0; + padding: 8px 0 0; + backdrop-filter: none; + box-shadow: none; +} + +.unity-verb-marquee.light .unity-verb-marquee-dropzone:hover, +.unity-verb-marquee.mobile-light .unity-verb-marquee-dropzone:hover, +.unity-verb-marquee.tablet-light .unity-verb-marquee-dropzone:hover, +.unity-verb-marquee.desktop-light .unity-verb-marquee-dropzone:hover, +.unity-verb-marquee.light.dragging-block .unity-verb-marquee-dropzone, +.unity-verb-marquee.mobile-light.dragging-block .unity-verb-marquee-dropzone, +.unity-verb-marquee.tablet-light.dragging-block .unity-verb-marquee-dropzone, +.unity-verb-marquee.desktop-light.dragging-block .unity-verb-marquee-dropzone, +.unity-verb-marquee.light .unity-verb-marquee-dropzone.dragging, +.unity-verb-marquee.mobile-light .unity-verb-marquee-dropzone.dragging, +.unity-verb-marquee.tablet-light .unity-verb-marquee-dropzone.dragging, +.unity-verb-marquee.desktop-light .unity-verb-marquee-dropzone.dragging { + border-color: #1473E6; + background-color: #FFF; + border-style: dashed; +} + +.unity-verb-marquee.dragging-block { + outline: none; +} + +.unity-verb-marquee.dragging-block::before { + display: none; +} + +.unity-verb-marquee.unity-verb-marquee-mobile-app .unity-verb-marquee-dropzone:hover, +.unity-verb-marquee.unity-verb-marquee-mobile-app.dragging-block .unity-verb-marquee-dropzone, +.unity-verb-marquee.unity-verb-marquee-mobile-app .unity-verb-marquee-dropzone.dragging { + border: none !important; + background: transparent !important; + background-color: transparent !important; + border-radius: 0; + padding: 8px 0 0; + backdrop-filter: none; + box-shadow: none; +} + +.unity-verb-marquee-cta { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + margin: 0; + padding: 12px 24px 14px; + background-color: #1473E6; + color: #FFF; + border: none; + border-radius: 100px; + font-size: 19px; + line-height: 1.4; + font-weight: 700; + cursor: pointer; + transition: background-color 0.2s ease; + white-space: nowrap; +} + +.unity-verb-marquee-cta:hover { + background-color: #0D66D0; +} + +.unity-verb-marquee-cta-solo { + display: inline-flex; + padding: 9px 18px 10px; + min-width: auto; + font-size: 17px; + text-decoration: none; + cursor: pointer; +} + +.unity-verb-marquee-cta-solo:hover { + text-decoration: none; + color: #FFF; +} + +@media screen and (max-width: 600px) { + .unity-verb-marquee-cta-solo { + inline-size: auto; + } +} + +.unity-verb-marquee-mobile-cta { + display: inline-flex; + padding: 12px 24px; + border-radius: 100px; + background: #1473e6; + color: #fff; + font-weight: 700; + justify-content: center; + align-items: center; + font-size: 19px; + line-height: 29px; + text-decoration: none; + white-space: pre-wrap; + text-align: center; + margin-bottom: var(--consonant-spacing-s, 24px); + box-sizing: border-box; +} + +.unity-verb-marquee-mobile-app .unity-verb-marquee-mobile-cta { + width: 100%; + max-width: 100%; + min-height: 48px; + padding: 14px 24px; + line-height: 1.4; + margin-top: 8px; + margin-bottom: 32px; + white-space: normal; +} + +.unity-verb-marquee-mobile-cta:hover, +.unity-verb-marquee-mobile-cta:active { + background-color: #0054b6; + color: #fff; + text-decoration: none; +} + +.unity-verb-marquee-cta .upload-icon { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; +} + +.unity-verb-marquee-drag { + display: none; + color: #FFF; + margin: 0; + text-align: center; + font-family: 'Adobe Clean ExtraBold', 'Adobe Clean', sans-serif; + font-weight: 800; +} + +.unity-verb-marquee.light .unity-verb-marquee-drag, +.unity-verb-marquee.mobile-light .unity-verb-marquee-drag, +.unity-verb-marquee.tablet-light .unity-verb-marquee-drag, +.unity-verb-marquee.desktop-light .unity-verb-marquee-drag { + color: var(--color-black); +} + +.unity-verb-marquee-file-limit { + font-size: 12px; + line-height: 1.4; + color: #FFF; + margin: 0; + text-align: center; +} + +.unity-verb-marquee.light .unity-verb-marquee-file-limit, +.unity-verb-marquee.mobile-light .unity-verb-marquee-file-limit, +.unity-verb-marquee.tablet-light .unity-verb-marquee-file-limit, +.unity-verb-marquee.desktop-light .unity-verb-marquee-file-limit { + color: var(--color-black); +} + +.unity-verb-marquee-media { + width: 100%; + max-width: 100%; + min-width: 0; + aspect-ratio: 1; + overflow: hidden; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.unity-verb-marquee-media .asset { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + line-height: 0; +} + +.unity-verb-marquee-media picture, +.unity-verb-marquee-media .asset picture, +.unity-verb-marquee .background picture { + width: 100%; + height: 100%; + display: flex; + line-height: 0; +} + +.unity-verb-marquee-media img, +.unity-verb-marquee-media picture img, +.unity-verb-marquee-media video { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.unity-verb-marquee .background img, +.unity-verb-marquee .background video { + width: 100%; + height: 100%; + object-fit: cover; +} + +.unity-verb-marquee .background { + position: absolute; + inset: 0; +} + +.unity-verb-marquee-legal a { + color: var(--consonant-link-color); + text-decoration: underline; +} + +.unity-verb-marquee-legal a:hover { + color: var(--consonant-link-color-hover); +} + +.unity-verb-marquee-media .asset a { + display: block; + width: 100%; + height: 100%; + line-height: 0; +} + +.unity-verb-marquee-media .asset a picture, +.unity-verb-marquee-media .asset a img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.unity-verb-marquee-footer { + display: flex; + align-items: flex-start; + justify-content: flex-start; + gap: 8px; + margin-top: 0; + width: 100%; +} + +.unity-verb-marquee-col-left > .unity-verb-marquee-footer { + margin-top: -16px; +} + +.unity-verb-marquee-legal { + font-family: var(--body-font-family); + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 1.5; + letter-spacing: 0; + color: #FFF; + margin: 0; + text-align: left; + flex: 1; +} + +.unity-verb-marquee.light .unity-verb-marquee-legal, +.unity-verb-marquee.mobile-light .unity-verb-marquee-legal, +.unity-verb-marquee.tablet-light .unity-verb-marquee-legal, +.unity-verb-marquee.desktop-light .unity-verb-marquee-legal { + color: #464646; +} + +.unity-verb-marquee.light .unity-verb-marquee-legal a, +.unity-verb-marquee.mobile-light .unity-verb-marquee-legal a, +.unity-verb-marquee.tablet-light .unity-verb-marquee-legal a, +.unity-verb-marquee.desktop-light .unity-verb-marquee-legal a { + color: var(--link-color); +} + +.unity-verb-marquee.light .unity-verb-marquee-legal a:hover, +.unity-verb-marquee.mobile-light .unity-verb-marquee-legal a:hover, +.unity-verb-marquee.tablet-light .unity-verb-marquee-legal a:hover, +.unity-verb-marquee.desktop-light .unity-verb-marquee-legal a:hover { + color: var(--link-hover-color); + opacity: 1; +} + +.unity-verb-marquee .info-icon { + display: flex; + align-items: center; + justify-content: center; + min-width: 18px; + min-height: 18px; + width: 18px; + height: 18px; + cursor: pointer; + flex-shrink: 0; + margin-top: 2px; + padding: 0; + background: none; + border: none; + outline: none; +} + +.unity-verb-marquee .info-icon:hover { + opacity: 0.8; +} + +.unity-verb-marquee .info-icon svg { + width: 18px; + height: 18px; +} + +.unity-verb-marquee .info-icon svg, +.unity-verb-marquee.dark .info-icon svg, +.unity-verb-marquee.mobile-dark .info-icon svg, +.unity-verb-marquee.tablet-dark .info-icon svg, +.unity-verb-marquee.desktop-dark .info-icon svg { + filter: brightness(0) invert(1); +} + +.unity-verb-marquee.light .info-icon svg, +.unity-verb-marquee.mobile-light .info-icon svg, +.unity-verb-marquee.tablet-light .info-icon svg, +.unity-verb-marquee.desktop-light .info-icon svg { + filter: none; +} + +.unity-verb-marquee .milo-tooltip { + position: relative; + text-decoration: none; + border-bottom: none; + min-width: 24px; + display: inline-flex; + min-height: 24px; + align-items: center; + justify-content: center; +} + +.unity-verb-marquee .milo-tooltip::before { + content: attr(data-tooltip); + position: absolute; + top: 50%; + transform: translateY(-50%); + left: 100%; + margin-left: 7px; + max-width: 140px; + padding: 10px; + border-radius: 5px; + background: #0469E3; + color: #fff; + text-align: left; + display: none; + z-index: 8; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 16px; + width: max-content; +} + +.unity-verb-marquee .milo-tooltip::after { + content: ""; + position: absolute; + left: 100%; + margin-left: -8px; + top: 50%; + transform: translateY(-50%); + border: 8px solid #0469E3; + border-color: transparent #0469E3 transparent transparent; + display: none; + z-index: 8; +} + +.unity-verb-marquee .milo-tooltip.top::before { + left: 50%; + transform: translateX(-50%) translateY(-100%); + top: -6px; + margin-bottom: 15px; + margin-left: 0; +} + +.unity-verb-marquee .milo-tooltip.top::after { + left: 50%; + top: 2px; + transform: translateX(-50%) translateY(-50%); + border: 8px solid #0469E3; + border-color: #0469E3 transparent transparent; + margin-left: 0; +} + +.unity-verb-marquee .milo-tooltip:hover::before, +.unity-verb-marquee .milo-tooltip:focus::before, +.unity-verb-marquee .milo-tooltip:active::before, +.unity-verb-marquee .milo-tooltip:hover::after, +.unity-verb-marquee .milo-tooltip:focus::after, +.unity-verb-marquee .milo-tooltip:active::after { + display: block; +} + +@supports (-webkit-backdrop-filter: blur(1px)) and (not (backdrop-filter: blur(1px))) { + .unity-verb-marquee .milo-tooltip:focus::before { + outline: 4px solid -webkit-focus-ring-color; + outline-offset: 1px; + } +} + +.unity-verb-marquee .hide { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.unity-verb-marquee-error { + cursor: default; + background: #D31510; + max-width: 435px; + color: white; + position: absolute; + top: 60px; + transform: translate(-50%, -50%); + left: 50%; + font-size: 14px; + font-weight: 400; + display: flex; + justify-content: center; + align-items: center; + padding: 8px; + z-index: 5; +} + +.unity-verb-marquee .close-icon { + position: relative; +} + +.unity-verb-marquee-error-text { + border-right: 1px solid rgb(255 255 255 / 20%); + padding: 0; + margin: 0; + margin-right: 8px; + padding-right: 16px; + max-width: 324px; + margin-left: 8px; + min-width: 100px; +} + +.unity-verb-marquee-errorBtn { + align-self: self-start; + width: 32px; + height: 32px; + cursor: pointer; + display: block; + background: none; + border: none; + outline: none; +} + +.unity-verb-marquee-errorIcon { + align-self: self-start; + width: 18px; + height: 18px; + margin-left: 8px; + margin-right: 4px; + margin-top: 7px; +} + +.unity-verb-marquee-errorIcon::after { + content: ''; + background-image: url('/acrobat/img/icons/ui/alert.svg'); + background-repeat: no-repeat; + display: flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; +} + +@media screen and (min-width: 600px) { + :root { + --unity-verb-marquee-padding: 64px 16px; + } + + .unity-verb-marquee-col-left:has(.unity-verb-marquee-copy-sub) .unity-verb-marquee-dropzone { + margin-top: 16px; + } + + .unity-verb-marquee-footer { + justify-content: center; + align-items: center; + } + + .unity-verb-marquee-legal { + text-align: start; + flex: 0 1 auto; + max-width: 550px; + } + + .unity-verb-marquee.tablet-light, + .unity-verb-marquee.tablet-light .unity-verb-marquee-heading { + color: var(--color-black); + } + + .unity-verb-marquee.tablet-dark { + color: var(--color-white); + } + + .unity-verb-marquee.tablet-light .unity-verb-marquee-copy { + color: var(--color-black); + opacity: 1; + } + + .unity-verb-marquee.tablet-light .unity-verb-marquee-copy-sub { + color: #484848; + opacity: 1; + } + + .unity-verb-marquee.tablet-light .unity-verb-marquee-legal a { + color: var(--link-color); + } + + .unity-verb-marquee.tablet-dark .unity-verb-marquee-legal a { + color: var(--consonant-link-color); + } + + .unity-verb-marquee.tablet-dark .unity-verb-marquee-legal a:hover { + color: var(--consonant-link-color-hover); + } + + .unity-verb-marquee.tablet-light .unity-verb-marquee-legal a:hover { + color: var(--link-hover-color); + } +} + +@media screen and (min-width: 600px) and (max-width: 1199px) { + .unity-verb-marquee-heading { + font-size: 64px; + letter-spacing: -2.88px; + } + + .unity-verb-marquee-dropzone { + gap: 14px; + } + + .unity-verb-marquee.unity-verb-marquee-mobile-app .unity-verb-marquee-col-left:has(.unity-verb-marquee-copy-sub) .unity-verb-marquee-dropzone { + margin-top: 0; + } +} + +@media screen and (min-width: 900px) { + :root { + --unity-verb-marquee-padding: 80px 40px; + --unity-verb-marquee-gap: 80px; + } +} + +@media screen and (max-width: 1199px) { + .unity-verb-marquee-row { + gap: 48px; + } + + .unity-verb-marquee.unity-verb-marquee-mobile-app .unity-verb-marquee-row { + gap: 0; + } + + .unity-verb-marquee.unity-verb-marquee-mobile-app .unity-verb-marquee-dropzone, + .unity-verb-marquee.unity-verb-marquee-mobile-app .unity-verb-marquee-dropzone:hover, + .unity-verb-marquee.unity-verb-marquee-mobile-app.dragging-block .unity-verb-marquee-dropzone, + .unity-verb-marquee.unity-verb-marquee-mobile-app .unity-verb-marquee-dropzone.dragging { + padding: 0; + } + + .unity-verb-marquee.unity-verb-marquee-mobile-app .unity-verb-marquee-mobile-cta { + margin-top: 0; + margin-bottom: 48px; + } + + .unity-verb-marquee .milo-tooltip.top::before { + left: auto; + right: 0; + transform: translateX(0) translateY(-100%); + max-width: 200px; + } + + .unity-verb-marquee .milo-tooltip.top::after { + left: auto; + right: 4px; + transform: translateX(0) translateY(-50%); + } + + [dir="rtl"] .unity-verb-marquee .milo-tooltip.top::before { + left: 0; + right: auto; + transform: translateX(0) translateY(-100%); + text-align: right; + } + + [dir="rtl"] .unity-verb-marquee .milo-tooltip.top::after { + left: 4px; + right: auto; + transform: translateX(0) translateY(-50%); + } +} + +@media screen and (min-width: 1200px) { + :root { + --unity-verb-marquee-padding: 80px; + --unity-verb-marquee-gap: 75px; + } + + .unity-verb-marquee-row { + display: grid; + width: 100%; + grid-template-columns: minmax(0, 550px) minmax(0, 575px); + align-items: center; + column-gap: 75px; + row-gap: 0; + } + + .unity-verb-marquee-col-right { + justify-content: center; + min-width: 0; + } + + .unity-verb-marquee-title { + font-size: 21px; + } + + .unity-verb-marquee-heading { + font-size: 80px; + letter-spacing: -3.6px; + } + + .unity-verb-marquee-dropzone { + width: 100%; + max-width: 100%; + align-self: stretch; + padding: 24px 0; + gap: 18px; + } + + .unity-verb-marquee-drag { + display: block; + max-width: 261px; + font-size: 18px; + line-height: 0.98; + letter-spacing: -0.54px; + } + + .unity-verb-marquee.desktop-light, + .unity-verb-marquee.desktop-light .unity-verb-marquee-heading { + color: var(--color-black); + } + + .unity-verb-marquee.desktop-dark { + color: var(--color-white); + } + + .unity-verb-marquee.desktop-light .unity-verb-marquee-copy { + color: var(--color-black); + opacity: 1; + } + + .unity-verb-marquee.desktop-light .unity-verb-marquee-copy-sub { + color: #484848; + opacity: 1; + } + + .unity-verb-marquee.desktop-light .unity-verb-marquee-legal a { + color: var(--link-color); + } + + .unity-verb-marquee.desktop-dark .unity-verb-marquee-legal a { + color: var(--consonant-link-color); + } + + .unity-verb-marquee.desktop-dark .unity-verb-marquee-legal a:hover { + color: var(--consonant-link-color-hover); + } + + .unity-verb-marquee.desktop-light .unity-verb-marquee-legal a:hover { + color: var(--link-hover-color); + } +} + +@media screen and (max-width: 600px) { + .unity-verb-marquee-error { + max-width: calc(100% - 32px); + top: 50px; + padding: 6px; + } + + .unity-verb-marquee-error-text { + font-size: 12px; + max-width: none; + padding-right: 12px; + margin-right: 6px; + margin-left: 6px; + min-width: auto; + } + + .unity-verb-marquee-errorIcon { + width: 16px; + height: 16px; + margin-left: 6px; + margin-right: 2px; + margin-top: 5px; + } + + .unity-verb-marquee-errorIcon::after { + width: 16px; + height: 16px; + } + + .unity-verb-marquee-errorBtn { + width: 28px; + height: 28px; + } + + .unity-verb-marquee .milo-tooltip.top::before { + max-width: 180px; + } +} + + diff --git a/unitylibs/blocks/unity-verb-marquee/unity-verb-marquee.js b/unitylibs/blocks/unity-verb-marquee/unity-verb-marquee.js new file mode 100644 index 000000000..3967906df --- /dev/null +++ b/unitylibs/blocks/unity-verb-marquee/unity-verb-marquee.js @@ -0,0 +1,988 @@ +import { setLibs } from '../../scripts/utils.js'; + +function getAppEnv() { + const { hostname } = window.location; + if (['www.adobe.com', 'sign.ing', 'edit.ing'].includes(hostname)) return 'prod'; + if ( + /--[^.]+--adobecom\.(hlx|aem)\.(page|live)$/.test(hostname) + || hostname === 'www.stage.adobe.com' + ) return 'stage'; + return 'dev'; +} + +function isOldBrowser() { + const { name, version } = window?.browser || {}; + return ( + name === 'Internet Explorer' + || (name === 'Microsoft Edge' && (!version || version.split('.')[0] < 86)) + || (name === 'Safari' && version.split('.')[0] < 14) + ); +} + +async function loadPlaceholders(prefix) { + const miloLibs = setLibs('/libs'); + const { getConfig } = await import(`${miloLibs}/utils/utils.js`); + const config = getConfig(); + + let prefixes; + if (prefix == null) prefixes = []; + else if (Array.isArray(prefix)) prefixes = prefix; + else prefixes = [prefix]; + const keyMatches = (key) => prefixes.length === 0 || prefixes.some((p) => key.startsWith(p)); + + window.mph = window.mph || {}; + + const mphKeyList = Object.keys(window.mph); + const allCovered = (prefixes.length === 0 && mphKeyList.length > 0) + || (prefixes.length > 0 && prefixes.every((p) => mphKeyList.some((k) => k.startsWith(p)))); + + if (!allCovered) { + const placeholdersPath = `${config.locale.contentRoot}/placeholders.json`; + try { + const response = await fetch(placeholdersPath); + if (response.ok) { + const placeholderData = await response.json(); + placeholderData.data.forEach(({ key, value }) => { + if (prefixes.length && !keyMatches(key)) return; + window.mph[key] = value.replace(/ /g, ' '); + }); + } + } catch (error) { + window.lana?.log(`Failed to load placeholders: ${error?.message}`, { severity: 'error' }); + } + } +} + +const MB100 = 104857600; +const MB20 = 20971520; +const PDF_ONLY = ['.pdf']; +const DOC_ONLY = ['.pdf', '.doc', '.docx']; +const ALL_FILES = ['.pdf', '.doc', '.docx', '.xml', '.ppt', '.pptx', '.xls', '.xlsx', '.rtf', '.txt', '.text', '.ai', '.form', '.bmp', '.gif', '.indd', '.jpeg', '.jpg', '.png', '.psd', '.tif', '.tiff']; +const SINGLE_PDF = { maxFileSize: MB100, acceptedFiles: PDF_ONLY, maxNumFiles: 1 }; +const MULTI_ALL = { maxFileSize: MB100, acceptedFiles: ALL_FILES, multipleFiles: true }; +const group = (verbs, config) => verbs.reduce((acc, v) => { acc[v] = config; return acc; }, {}); + +export const LIMITS = { + fillsign: { ...SINGLE_PDF, mobileApp: true }, + 'summarize-pdf': { maxFileSize: MB100, acceptedFiles: ALL_FILES, maxNumFiles: 1, genAI: true }, + 'resume-builder': { maxFileSize: MB20, acceptedFiles: DOC_ONLY, maxNumFiles: 1, genAI: true }, + ...group(['word-to-pdf', 'jpg-to-pdf'], MULTI_ALL), +}; + +const miloLibs = setLibs('/libs'); +let createTag; +let getConfig; +let loadStyle; +let decorateBlockBg; + +const EOLBrowserPage = 'https://acrobat.adobe.com/home/index-browser-eol.html'; + +const lanaOptions = { + sampleRate: 1, + tags: 'Express_Milo,Project Unity (Express)', + severity: 'error', +}; + +const ICONS = { + WIDGET_ICON: '', + UPLOAD_ICON: '', + INFO_ICON: '', + CLOSE_ICON: '', + SUBCOPY_CHECK: '', +}; + +function createSvgElement(iconName) { + const svgString = ICONS[iconName]; + if (!svgString) { + window.lana?.log( + `Error Code: Unknown, Status: 'Unknown', Message: Icon not found: ${iconName}`, + lanaOptions, + ); + return null; + } + const parser = new DOMParser(); + const svgDoc = parser.parseFromString(svgString, 'image/svg+xml'); + return svgDoc.documentElement; +} + +const getCTA = (verb) => { + const verbConfig = LIMITS[verb]; + return window.mph?.[`verb-marquee-${verb}-upload-cta`] + || window.mph?.[`verb-widget-cta-${verbConfig?.uploadType}`] + || window.mph?.['verb-widget-cta'] || ''; +}; + +function isMobileDevice() { + const ua = navigator.userAgent.toLowerCase(); + return /android|iphone|ipod|blackberry|windows phone/i.test(ua); +} + +function isTabletDevice() { + const ua = navigator.userAgent.toLowerCase(); + const isIPadOS = navigator.userAgent.includes('Mac') + && 'ontouchend' in document + && !/iphone|ipod/i.test(ua); + const isTabletUA = /ipad|android(?!.*mobile)/i.test(ua); + return isIPadOS || isTabletUA; +} + +function getStoreType() { + const { ua } = window.browser; + if (/android/i.test(ua)) return 'google'; + if (/iphone|ipod/i.test(ua)) return 'apple'; + if (navigator.userAgent.includes('Mac') && 'ontouchend' in document && !/iphone|ipod/i.test(navigator.userAgent)) { + return 'apple'; + } + if (/ipad/i.test(ua)) return 'apple'; + return 'desktop'; +} + +function getEnv() { + const { hostname } = window.location; + if (['localhost', '.hlx.', '.aem.', 'stage.adobe.com'].some((p) => hostname.includes(p))) return 'stage'; + return 'prod'; +} + +function redDirLink(verb) { + const hostname = window?.location?.hostname; + const env = getEnv(); + const verbSlug = verb.split('-').join(''); + return hostname !== 'www.adobe.com' + ? `https://www.adobe.com/go/acrobat-${verbSlug}-${env}` + : `https://www.adobe.com/go/acrobat-${verbSlug}`; +} + +function redDir(verb) { + window.location.href = redDirLink(verb); +} + +function getSplunkEndpoint() { + return (getEnv() === 'prod') ? 'https://unity.adobe.io/api/v1/log' : 'https://unity-stage.adobe.io/api/v1/log'; +} + +function getCookie(name) { + const match = document.cookie.match(new RegExp(`(?:^|; )${name}=([^;]*)`)); + return match ? decodeURIComponent(match[1]) : null; +} + +function setCookie(name, value, expires) { + document.cookie = `${name}=${value};domain=.adobe.com;path=/;expires=${expires}`; +} + +function uploadedTime() { + const uploadingUTS = parseInt(getCookie('UTS_Uploading'), 10); + const uploadedUTS = parseInt(getCookie('UTS_Uploaded'), 10); + if (Number.isNaN(uploadingUTS) || Number.isNaN(uploadedUTS)) return 'N/A'; + return ((uploadedUTS - uploadingUTS) / 1000).toFixed(1); +} + +function incrementVerbKey(verbKey) { + let count = parseInt(localStorage.getItem(verbKey), 10) || 0; + count += 1; + localStorage.setItem(verbKey, count); + return count; +} + +function getVerbKey(verbKey) { + const count = parseInt(localStorage.getItem(verbKey), 10) || 0; + const trialMapping = { + 0: '1st', + 1: '2nd', + }; + return trialMapping[count] || '2+'; +} + +const setUser = () => { + localStorage.setItem('unity.user', 'true'); +}; + +const redirectReady = new CustomEvent('DCUnity:RedirectReady'); + +let exitFlag = true; +let tabClosureSent = false; +let isUploading = false; + +function prefetchTarget() { + const iframe = document.createElement('iframe'); + iframe.src = window.prefetchTargetUrl; + iframe.style.display = 'none'; + document.body.appendChild(iframe); +} + +function prefetchNextPage(url) { + const link = document.createElement('link'); + link.rel = 'prefetch'; + link.href = url; + link.crossOrigin = 'anonymous'; + link.as = 'document'; + document.head.appendChild(link); +} + +function initiatePrefetch(url) { + if (!window.prefetchTargetUrl) { + prefetchNextPage(url); + window.prefetchTargetUrl = url; + } +} + +function handleExit(event, verb, userObj, unloadFlag, workflowStep) { + if (exitFlag || tabClosureSent || (isUploading && workflowStep === 'preuploading')) { return; } + tabClosureSent = true; + const uploadingStartTime = parseInt(getCookie('UTS_Uploading'), 10); + const tabClosureTime = Date.now(); + const duration = uploadingStartTime ? ((tabClosureTime - uploadingStartTime) / 1000).toFixed(1) : 'N/A'; + window.analytics.verbAnalytics('job:browser-tab-closure', verb, userObj, unloadFlag); + window.analytics.sendAnalyticsToSplunk('job:browser-tab-closure', verb, { ...userObj, workflowStep, uploadTime: duration }, getSplunkEndpoint(), true); + if (!isUploading) return; + event.preventDefault(); + event.returnValue = true; +} + +window.analytics = window.analytics || { + verbAnalytics: () => {}, + sendAnalyticsToSplunk: () => {}, +}; + +async function loadAnalyticsAfterLCP(analyticsData) { + const { verb, userAttempts } = analyticsData; + try { + const analyticsModule = await import('../../scripts/alloy/verb-widget.js'); + const { default: verbAnalytics, sendAnalyticsToSplunk } = analyticsModule; + window.analytics.verbAnalytics = verbAnalytics; + window.analytics.sendAnalyticsToSplunk = sendAnalyticsToSplunk; + window.analytics.verbAnalytics('landing:shown', verb, { userAttempts }); + } catch (error) { + window.lana?.log( + `Error Code: Unknown, Status: 'Unknown', Message: Analytics import failed: ${error.message} on ${verb}`, + lanaOptions, + ); + } + return window.analytics; +} + +window.addEventListener('analyticsLoad', async ({ detail }) => { + /* eslint-disable-next-line compat/compat -- Opera Mini not a target */ + const delay = (ms) => new Promise((resolve) => { + setTimeout(resolve, ms); + }); + const { + verbAnalytics: stubVerb, + sendAnalyticsToSplunk: stubSend, + } = window.analytics; + if (window.PerformanceObserver) { + await Promise.race([ + new Promise((res) => { + try { + const obs = new PerformanceObserver((list) => { + const entries = list.getEntries(); + if (entries.length > 0) res(); + }); + obs.observe({ type: 'largest-contentful-paint', buffered: true }); + } catch (error) { + res(); + } + }), + delay(3000), + ]); + } else { + await delay(3000); + } + await loadAnalyticsAfterLCP(detail); + + const { + verbAnalytics, + reviewAnalytics, + sendAnalyticsToSplunk, + } = window.analytics; + if ( + verbAnalytics === stubVerb + || sendAnalyticsToSplunk === stubSend + ) { + window.lana?.log( + 'Analytics failed to initialize correctly: some methods remain no-ops on unity-verb-marquee block', + lanaOptions, + ); + } +}); + +function decorateImage(media) { + media.classList.add('image'); + const imageLink = media.querySelector('a'); + const picture = media.querySelector('picture'); + if (imageLink && picture && !imageLink.parentElement.classList.contains('modal-img-link')) { + imageLink.textContent = ''; + imageLink.append(picture); + } +} + +function processMedia(mediaDiv) { + if (!mediaDiv) return null; + mediaDiv.classList.add('asset'); + const hasVideo = mediaDiv.querySelector('video, a[href*=".mp4"], a[href*=".webm"], a[href*=".ogg"]'); + if (!hasVideo) { + decorateImage(mediaDiv); + } + return mediaDiv; +} + +function getAuthoredSvgInfo(foregroundEl, headlineEl) { + if (!foregroundEl) return null; + const headingCell = headlineEl + && [...foregroundEl.children].find((div) => div.contains(headlineEl)); + const searchRoot = headingCell || foregroundEl; + const svgImg = searchRoot.querySelector('img[src$=".svg"]'); + if (!svgImg) return null; + return { url: svgImg.getAttribute('src').trim(), altText: svgImg.getAttribute('alt') || '' }; +} + +export default async function init(element) { + ({ createTag, getConfig, loadStyle } = (await import(`${miloLibs}/utils/utils.js`))); + ({ decorateBlockBg } = (await import(`${miloLibs}/utils/decorate.js`))); + + element.classList.add('con-block'); + if (isOldBrowser()) { + window.location.href = EOLBrowserPage; + return; + } + window.mph = window.mph || {}; + await loadPlaceholders(['verb-marquee', 'verb-widget']); + const rawVerb = element.classList[1]; + const VERB = rawVerb === 'ai-summary-generator' ? 'summarize-pdf' : rawVerb; + const limits = LIMITS[VERB]; + const isMobile = isMobileDevice(); + const isTablet = isTabletDevice(); + const mobileOrTabletTouch = isMobile || isTablet; + + function getPricingLink() { + const { locale } = getConfig(); + const ENV = getAppEnv(); + const links = { + dev: `https://www.stage.adobe.com${locale.prefix}/acrobat/pricing/pricing.html`, + stage: `https://www.stage.adobe.com${locale.prefix}/acrobat/pricing/pricing.html`, + prod: `https://www.adobe.com${locale.prefix}/acrobat/pricing/pricing.html`, + }; + return links[ENV] || links.prod; + } + + let useFileUpload = true; + if (mobileOrTabletTouch) { + if (limits?.level === 0) useFileUpload = false; + else if (limits?.mobileApp) useFileUpload = false; + } + + // Initialize analytics - track attempts for analytics data (no UI changes based on attempts) + const userAttempts = getVerbKey(`${VERB}_attempts`); + let noOfFiles = null; + + function mergeData(eventData = {}) { + return { ...eventData, noOfFiles }; + } + function getLocale() { + const currLocale = getConfig().locale?.prefix.replace('/', ''); + return currLocale || 'en-us'; + } + function runWhenDocumentIsReady(callback) { + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', callback); + } else { + callback(); + } + } + const initializePingService = async () => { + try { + const { PingService, USER_TYPE } = await import('../../scripts/ping.js'); + const isSignedIn = window.adobeIMS?.isSignedInUser() || false; + const userType = isSignedIn ? USER_TYPE.SIGNEDIN : USER_TYPE.ANON; + const userId = isSignedIn ? ((await window.adobeIMS?.getProfile())?.userId || '') : ''; + const pingService = new PingService({ + locale: getLocale(), + config: { + serverEnv: getEnv(), + appName: 'adobe_com', + appVersion: '1.0', + appReferrer: '', + }, + userId, + isSignedIn, + userType, + subscriptionType: 'unspecified', + }); + const pingConfig = { + appPath: 'unity-dc-frictionless', + schema: {}, + }; + await pingService.sendPingEvent(pingConfig); + } catch (error) { + window.lana?.log( + `Error Code: Unknown, Status: 'Unknown', Message: Failed to send ping: ${error.message}`, + lanaOptions, + ); + } + }; + runWhenDocumentIsReady(() => { + initializePingService(); + window.dispatchEvent(new CustomEvent('analyticsLoad', { detail: { verb: VERB, userAttempts } })); + }); + const children = element.querySelectorAll(':scope > div'); + const foreground = children[children.length - 1]; + foreground.classList.add('foreground', 'container'); + if (children.length > 1 && children[0].textContent !== '') { + children[0].classList.add('background'); + decorateBlockBg(element, children[0], { useHandleFocalpoint: true }); + } + const headline = foreground.querySelector('h1, h2, h3, h4, h5, h6'); + const heading = headline?.textContent?.trim() || ''; + const text = headline?.closest('div'); + if (text) { + text.classList.add('text'); + } + const authoredSvg = getAuthoredSvgInfo(foreground, headline); + const media = foreground.querySelector(':scope > div:not([class])'); + if (media) { + processMedia(media); + } + const container = createTag('div', { class: 'unity-verb-marquee-container' }); + const row = createTag('div', { class: 'unity-verb-marquee-row' }); + const leftCol = createTag('div', { class: 'unity-verb-marquee-col unity-verb-marquee-col-left' }); + const rightCol = createTag('div', { class: 'unity-verb-marquee-col unity-verb-marquee-col-right' }); + const header = createTag('div', { class: 'unity-verb-marquee-header' }); + if (authoredSvg) { + const svgImg = createTag('img', { + src: authoredSvg.url, + alt: authoredSvg.altText, + class: 'verb-marquee-title-svg', + }); + header.append(svgImg); + } else { + const iconWrapper = createTag('div', { class: 'acrobat-icon' }); + const widgetIconSvg = createSvgElement('WIDGET_ICON'); + if (widgetIconSvg) { + widgetIconSvg.classList.add('icon-acrobat'); + widgetIconSvg.setAttribute('aria-hidden', 'true'); + iconWrapper.appendChild(widgetIconSvg); + } + const title = createTag('div', { class: 'unity-verb-marquee-title' }); + const adobeText = createTag('span', {}, 'Adobe'); + const studySpaceText = createTag('span', {}, ' Acrobat'); + title.append(adobeText, studySpaceText); + header.append(iconWrapper, title); + } + const headingEl = createTag('h1', { class: 'unity-verb-marquee-heading' }, heading); + const isMobileOrTabletViewport = window.innerWidth < 1200; + const copy1Text = isMobileOrTabletViewport + ? (window.mph?.[`verb-marquee-${VERB}-mobile-copy`] || window.mph?.[`verb-marquee-${VERB}-copy`] || '') + : (window.mph?.[`verb-marquee-${VERB}-copy`] || ''); + const subCopies = ['', '-2'] + .map((suffix) => { + const subCopyText = isMobileOrTabletViewport + ? (window.mph?.[`verb-marquee-${VERB}-mobile-sub-copy${suffix}`] + || window.mph?.[`verb-marquee-${VERB}-sub-copy${suffix}`] || '') + : (window.mph?.[`verb-marquee-${VERB}-sub-copy${suffix}`] || ''); + if (!subCopyText) return null; + const el = createTag('p', { class: 'unity-verb-marquee-copy-sub' }); + const icon = createSvgElement('SUBCOPY_CHECK'); + if (icon) { + icon.classList.add('unity-verb-marquee-copy-sub-icon'); + icon.setAttribute('aria-hidden', 'true'); + el.appendChild(icon); + } + el.appendChild(createTag('span', { class: 'unity-verb-marquee-copy-sub-label' }, subCopyText)); + return el; + }) + .filter(Boolean); + const copy1 = createTag('p', { class: 'unity-verb-marquee-copy' }, copy1Text); + const dropzone = createTag('div', { + class: 'unity-verb-marquee-dropzone', + id: 'drop-zone', + }); + const ctaButtonLabel = getCTA(VERB); + const ctaButton = createTag('button', { + class: 'unity-verb-marquee-cta', + type: 'button', + ...(ctaButtonLabel && { 'aria-label': ctaButtonLabel }), + }); + const uploadIconSvg = createSvgElement('UPLOAD_ICON'); + if (uploadIconSvg) { + uploadIconSvg.classList.add('upload-icon'); + uploadIconSvg.setAttribute('aria-hidden', 'true'); + ctaButton.appendChild(uploadIconSvg); + } + const ctaLabel = createTag('span', { class: 'unity-verb-marquee-cta-label' }, ctaButtonLabel); + ctaButton.appendChild(ctaLabel); + const dragText = createTag('p', { class: 'unity-verb-marquee-drag' }, window.mph?.[`verb-widget-${VERB}-dragndrop-text`] || ''); + const fileLimitText = createTag('p', { + class: 'unity-verb-marquee-file-limit', + id: 'file-upload-description', + }, window.mph?.[`verb-widget-${VERB}-file-limit`] || ''); + + if (useFileUpload) { + dropzone.append(ctaButton, dragText, fileLimitText); + } else if (mobileOrTabletTouch) { + if (limits?.level === 0) { + element.classList.add('unity-verb-marquee-trial'); + const trialCta = createTag( + 'a', + { class: 'unity-verb-marquee-mobile-cta', href: getPricingLink() }, + window.mph?.['verb-widget-cta-mobile-start-trial'] || '', + ); + dropzone.append(trialCta); + } else if (limits?.mobileApp) { + element.classList.add('unity-verb-marquee-mobile-app'); + const storeType = getStoreType(); + const mobileLink = window.mph?.[`verb-widget-${VERB}-${storeType}`] + || window.mph?.[`verb-widget-${VERB}-apple`]; + const storeCta = createTag( + 'a', + { class: 'unity-verb-marquee-mobile-cta', href: mobileLink || '#' }, + window.mph?.['verb-widget-cta-mobile'] || '', + ); + storeCta.addEventListener('click', () => { + window.analytics.verbAnalytics('goto-app:clicked', VERB, { userAttempts }); + }); + dropzone.append(storeCta); + } + } + + let soloClicked = false; + let fileInput = null; + if (useFileUpload) { + fileInput = createTag('input', { + type: 'file', + accept: limits?.acceptedFiles, + id: 'file-upload', + class: 'hide', + 'aria-hidden': 'true', + 'aria-describedby': 'file-upload-description', + ...(limits?.multipleFiles && { multiple: '' }), + }); + } + const errorState = createTag('div', { + class: 'error hide', + role: 'alert', + 'aria-live': 'assertive', + 'aria-atomic': 'true', + }); + const errorStateText = createTag('p', { + class: 'unity-verb-marquee-error-text', + id: 'error-message', + }); + const errorIcon = createTag('div', { + class: 'unity-verb-marquee-errorIcon', + 'aria-hidden': 'true', + }); + const errorCloseBtn = createTag('div', { class: 'unity-verb-marquee-errorBtn', role: 'button', tabindex: '0', 'aria-label': 'Close error' }); + const srAlert = { announceTimer: null, cleanupTimer: null }; + const clearSrAlert = () => { + clearTimeout(srAlert.announceTimer); + clearTimeout(srAlert.cleanupTimer); + document.querySelector('.unity-verb-marquee-sr-alert')?.remove(); + }; + const announceToScreenReader = (msg) => { + clearSrAlert(); + srAlert.announceTimer = setTimeout(() => { + const alertEl = createTag('div', { + class: 'unity-verb-marquee-sr-alert', + role: 'alert', + style: 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0', + }); + alertEl.textContent = msg; + document.body.appendChild(alertEl); + srAlert.cleanupTimer = setTimeout(() => alertEl.remove(), 10000); + }, 5000); + }; + const closeIconSvg = createSvgElement('CLOSE_ICON'); + if (closeIconSvg) { + closeIconSvg.classList.add('close-icon', 'error'); + closeIconSvg.setAttribute('aria-hidden', 'true'); + errorCloseBtn.prepend(closeIconSvg); + } + errorState.append(errorIcon, errorStateText, errorCloseBtn); + const footer = createTag('div', { class: 'unity-verb-marquee-footer' }); + const { locale } = getConfig(); + const ppURL = window.mph?.['verb-widget-privacy-policy-url'] || `https://www.adobe.com${locale.prefix}/privacy/policy.html`; + const touURL = window.mph?.['verb-widget-terms-of-use-url'] || `https://www.adobe.com${locale.prefix}/legal/terms.html`; + const genAIurl = window.mph?.['verb-widget-genai-terms-url'] || `https://www.adobe.com${locale.prefix}/legal/licenses-terms/adobe-gen-ai-user-guidelines.html`; + const mph = window.mph || {}; + const legalPart1 = mph['verb-marquee-legal'] || mph['verb-widget-legal'] || ''; + const legalPart2 = limits?.genAI + ? (mph['verb-marquee-legal-2-ai'] || mph['verb-widget-legal-2-ai'] || '') + : (mph['verb-marquee-legal-2'] || mph['verb-widget-legal-2'] || ''); + const legalCombined = [legalPart1, legalPart2].filter(Boolean).join(' ').trim(); + const legalInitial = legalCombined || (mph['verb-marquee-legal-text'] || ''); + const legalText = createTag('p', { class: 'unity-verb-marquee-legal' }, legalInitial); + const omitFooterForMobileStore = limits?.mobileApp && mobileOrTabletTouch; + if (!omitFooterForMobileStore && !(limits?.mobileApp && isMobile)) { + if (legalText.textContent) { + const createLegalLink = (label, url) => `${label}`; + const legalLinks = [ + ['verb-widget-terms-of-use', touURL], + ['verb-widget-privacy-policy', ppURL], + ...(limits?.genAI ? [['verb-widget-genai-guidelines', genAIurl]] : []), + ]; + legalText.innerHTML = legalLinks.reduce( + (html, [key, url]) => { + const linkText = window.mph?.[key]; + return linkText ? html.replace(linkText, createLegalLink(linkText, url)) : html; + }, + legalText.textContent, + ); + } + } + const tooltipContent = window.mph?.['verb-widget-tool-tip'] || ''; + const infoIcon = createTag('button', { + class: 'info-icon milo-tooltip top', + type: 'button', + ...(tooltipContent && { 'aria-label': tooltipContent }), + 'aria-describedby': 'info-tooltip-text', + ...(tooltipContent && { 'data-tooltip': tooltipContent }), + }); + const infoIconSvg = createSvgElement('INFO_ICON'); + if (infoIconSvg) { + infoIconSvg.setAttribute('aria-hidden', 'true'); + infoIcon.appendChild(infoIconSvg); + } + const tooltipText = createTag('span', { + id: 'info-tooltip-text', + class: 'hide', + }, tooltipContent); + infoIcon.appendChild(tooltipText); + if (!omitFooterForMobileStore) { + footer.append(legalText, infoIcon); + } + const leftColChildren = [ + header, + headingEl, + copy1, + ...subCopies, + dropzone, + ...(fileInput ? [fileInput] : []), + ...(omitFooterForMobileStore ? [] : [footer]), + ]; + leftCol.append(...leftColChildren); + if (media) { + const mediaWrapper = createTag('div', { class: 'unity-verb-marquee-media' }); + while (media.firstChild) { + mediaWrapper.appendChild(media.firstChild); + } + rightCol.appendChild(mediaWrapper); + } + row.append(leftCol, rightCol); + container.appendChild(row); + foreground.innerHTML = ''; + foreground.append(container); + element.append(errorState); + + function handleAnalyticsEvent( + eventName, + metadata = {}, + documentUnloading = true, + canSendDataToSplunk = true, + ) { + window.analytics.verbAnalytics(eventName, VERB, metadata, documentUnloading); + if (!canSendDataToSplunk) return; + window.analytics.sendAnalyticsToSplunk(eventName, VERB, metadata, getSplunkEndpoint()); + } + + function registerTabCloseEvent(eventData, workflowStep) { + window.addEventListener('beforeunload', (windowEvent) => { + handleExit(windowEvent, VERB, eventData, false, workflowStep); + }); + } + + function handleUploadingEvent(data, attempts, cookieExp, canSendDataToSplunk) { + isUploading = true; + exitFlag = false; + prefetchTarget(); + const metadata = mergeData({ ...data, userAttempts: attempts }); + handleAnalyticsEvent('job:uploading', metadata, false, canSendDataToSplunk); + if (LIMITS[VERB]?.multipleFiles) { + handleAnalyticsEvent('job:multi-file-uploading', metadata, false, canSendDataToSplunk); + } + setCookie('UTS_Uploading', Date.now(), cookieExp); + registerTabCloseEvent(metadata, 'uploading'); + } + + function handleUploadedEvent(data, attempts, cookieExp, canSendDataToSplunk) { + exitFlag = true; + setTimeout(() => { + window.dispatchEvent(redirectReady); + window.lana?.log( + 'Adobe Analytics done callback failed to trigger, 3 second timeout dispatched event.', + { sampleRate: 1, tags: 'DC_Milo,Project Unity (DC)', severity: 'warning' }, + ); + }, 3000); + setCookie('UTS_Uploaded', Date.now(), cookieExp); + const calcUploadedTime = uploadedTime(); + const metadata = { ...data, uploadTime: calcUploadedTime, userAttempts: attempts }; + handleAnalyticsEvent('job:uploaded', metadata, false, canSendDataToSplunk); + if (LIMITS[VERB]?.multipleFiles) { + handleAnalyticsEvent('job:multi-file-uploaded', metadata, false, canSendDataToSplunk); + } + setUser(); + incrementVerbKey(`${VERB}_attempts`); + } + + const setDraggingClass = (shouldToggle) => { + dropzone.classList.toggle('dragging', !!shouldToggle); + }; + let outsideClickHandler = null; + const closeError = () => { + errorState.classList.remove('unity-verb-marquee-error'); + errorState.classList.add('hide'); + errorStateText.textContent = ''; + clearSrAlert(); + if (outsideClickHandler) { + document.removeEventListener('click', outsideClickHandler); + outsideClickHandler = null; + } + }; + const handleError = (detail, logToLana = false, logOptions = {}) => { + const { code, message, status, info = 'No additional info provided', accountType = 'Unknown account type' } = detail; + if (message) { + setDraggingClass(false); + errorState.classList.add('unity-verb-marquee-error'); + errorState.classList.remove('hide'); + errorStateText.textContent = message; + announceToScreenReader(message); + errorCloseBtn.focus(); + setTimeout(() => { + if (outsideClickHandler) return; + outsideClickHandler = (e) => { + if (!errorState.contains(e.target)) closeError(); + }; + document.addEventListener('click', outsideClickHandler); + }, 0); + } + if (logToLana) { + window.lana?.log( + `Error Code: ${code}, Status: ${status}, Message: ${message}, Info: ${info}, Account Type: ${accountType}`, + logOptions, + ); + } + }; + if (useFileUpload && fileInput) { + ctaButton.addEventListener('click', () => { + fileInput.click(); + }); + dropzone.addEventListener('click', (e) => { + if (e.target.tagName === 'BUTTON' || e.target.closest('button')) { return; } + if (e.target.classList.value.includes('error') || e.target.closest('.error')) { return; } + fileInput.click(); + }); + element.addEventListener('dragover', (e) => { + e.preventDefault(); + e.stopPropagation(); + setDraggingClass(true); + element.classList.add('dragging-block'); + }); + element.addEventListener('dragleave', (e) => { + e.preventDefault(); + e.stopPropagation(); + if (!element.contains(e.relatedTarget)) { + setDraggingClass(false); + element.classList.remove('dragging-block'); + } + }); + element.addEventListener('drop', (e) => { + e.preventDefault(); + setDraggingClass(false); + element.classList.remove('dragging-block'); + const { dataTransfer: { files } } = e; + if (files.length > 0) { + noOfFiles = files.length; + } + }); + fileInput.addEventListener('click', () => { + if (soloClicked) { + soloClicked = false; + return; + } + [ + 'filepicker:shown', + 'dropzone:choose-file-clicked', + 'files-selected', + 'entry:clicked', + 'discover:clicked', + ].forEach((analyticsEvent) => { + window.analytics.verbAnalytics(analyticsEvent, VERB, { userAttempts }); + }); + }); + fileInput.addEventListener('change', (data) => { + const { target: { files } } = data; + if (files.length > 0) { + noOfFiles = files.length; + } + }); + fileInput.addEventListener('cancel', () => { + window.analytics.verbAnalytics('choose-file:close', VERB, { userAttempts }); + }); + } + errorCloseBtn.addEventListener('click', (e) => { + e.stopPropagation(); + closeError(); + }); + errorCloseBtn.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + closeError(); + } + }); + function soloUpload() { + if (!useFileUpload || !fileInput || !ctaButton) return; + const uploadLinks = document.querySelectorAll('a[href*="#upload"]'); + uploadLinks.forEach((link) => { + const labelElement = createTag('label', { + for: 'file-upload', + class: 'unity-verb-marquee-cta unity-verb-marquee-cta-solo', + tabindex: 0, + 'daa-ll': ctaButton.textContent, + 'aria-label': ctaButton.textContent, + }); + labelElement.innerHTML = ctaButton.innerHTML; + const wrapper = link.closest('div'); + if (!wrapper) return; + wrapper.append(labelElement); + link.remove(); + labelElement.addEventListener('click', (data) => { + soloClicked = true; + [ + 'filepicker:shown', + 'cta:choose-file-clicked', + 'files-selected', + 'entry:clicked', + 'discover:clicked', + ].forEach((analyticsEvent) => { + window.analytics.verbAnalytics(analyticsEvent, VERB, { ...data, userAttempts }); + }); + }); + }); + } + runWhenDocumentIsReady(soloUpload); + element.addEventListener('unity:track-analytics', (e) => { + const cookieExp = new Date(Date.now() + 30 * 60 * 1000).toUTCString(); + const { event, data } = e.detail || {}; + const canSendDataToSplunk = e.detail?.sendToSplunk ?? true; + if (!event) return; + const metadata = mergeData({ ...data, userAttempts }); + const analyticsMap = { + change: () => { + exitFlag = false; + handleAnalyticsEvent('choose-file:open', metadata, true, canSendDataToSplunk); + registerTabCloseEvent(metadata, 'preuploading'); + }, + drop: () => { + exitFlag = false; + ['files-dropped', 'entry:clicked', 'discover:clicked'].forEach((analyticsEvent) => { + handleAnalyticsEvent(analyticsEvent, metadata, true, canSendDataToSplunk); + }); + setDraggingClass(false); + registerTabCloseEvent(metadata, 'preuploading'); + }, + cancel: () => { + if (exitFlag) return; + handleAnalyticsEvent('job:cancel', metadata, true, canSendDataToSplunk); + exitFlag = true; + }, + uploading: () => handleUploadingEvent(data, userAttempts, cookieExp, canSendDataToSplunk), + uploaded: () => handleUploadedEvent(data, userAttempts, cookieExp, canSendDataToSplunk), + chunk_uploaded: () => { + if (canSendDataToSplunk) window.analytics.sendAnalyticsToSplunk('job:chunk-uploaded', VERB, metadata, getSplunkEndpoint()); + }, + redirectUrl: () => { + if (data) initiatePrefetch(data.redirectUrl); + handleAnalyticsEvent('job:redirect-success', metadata, false, canSendDataToSplunk); + }, + }; + if (analyticsMap[event]) { + analyticsMap[event](); + } + }); + element.addEventListener('unity:show-error-toast', (e) => { + const { + code: errorCode, + info: errorInfo, + metaData: metadata, + errorData, + sendToSplunk: canSendDataToSplunk = true, + } = e.detail || {}; + if (!errorCode) return; + handleError(e.detail, true, lanaOptions); + if (errorCode.includes('cookie_not_set')) return; + const errorAnalyticsMap = { + error_only_accept_one_file: 'error_only_accept_one_file', + error_unsupported_type: 'error:UnsupportedFile', + error_empty_file: 'error:EmptyFile', + error_file_too_large: 'error:TooLargeFile', + error_max_page_count: 'error:max_page_count', + error_min_page_count: 'error:min_page_count', + error_max_num_files: 'error:max_num_files', + error_generic: 'error', + error_max_quota_exceeded: 'error:max_quota_exceeded', + error_no_storage_provision: 'error:no_storage_provision', + error_duplicate_asset: 'error:duplicate_asset', + warn_chunk_upload: 'warn:verb_upload_warn_chunk_upload', + error_file_same_type: 'error:file_same_type', + error_fetch_redirect_url: 'error:fetch_redirect_url', + error_finalize_asset: 'error:finalize_asset', + error_verify_page_count: 'error:verify_page_count', + error_chunk_upload: 'error:chunk_upload', + error_create_asset: 'error:create_asset', + error_fetching_access_token: 'error:fetching_access_token', + }; + const key = Object.keys(errorAnalyticsMap).find((k) => errorCode?.includes(k)); + if (key) { + const event = errorAnalyticsMap[key]; + window.analytics.verbAnalytics(event, VERB, event === 'error' ? { errorInfo } : {}); + } + if (canSendDataToSplunk) { + window.analytics.sendAnalyticsToSplunk( + key, + VERB, + { ...metadata, errorData }, + getSplunkEndpoint(), + ); + } + exitFlag = true; + }); + window.addEventListener('beforeunload', (event) => { + if (exitFlag || tabClosureSent || !isUploading) return; + tabClosureSent = true; + const uploadingUTS = parseInt(getCookie('UTS_Uploading'), 10); + const tabClosureTime = Date.now(); + const duration = uploadingUTS ? ((tabClosureTime - uploadingUTS) / 1000).toFixed(1) : 'N/A'; + window.analytics.verbAnalytics('job:browser-tab-closure', VERB, { userAttempts }, exitFlag); + window.analytics.sendAnalyticsToSplunk('job:browser-tab-closure', VERB, { userAttempts, uploadTime: duration }, getSplunkEndpoint(), true); + if (!isUploading) return; + event.preventDefault(); + event.returnValue = true; + }); + window.addEventListener('beforeunload', () => { + const cookieExp = new Date(Date.now() + 90 * 1000).toUTCString(); + if (exitFlag) { + document.cookie = `UTS_Redirect=${Date.now()};domain=.adobe.com;path=/;expires=${cookieExp}`; + } + }); + + async function checkSignedInUser() { + if (!window.adobeIMS?.isSignedInUser?.()) return; + let accountType; + try { + accountType = window.adobeIMS.getAccountType(); + } catch { + accountType = (await window.adobeIMS.getProfile()).account_type; + } + if (accountType) redDir(VERB); + } + await checkSignedInUser(); + window.addEventListener('IMS:Ready', checkSignedInUser); + window.prefetchTargetUrl = null; + element.parentNode.style.display = 'block'; + window.addEventListener('pageshow', (event) => { + const historyTraversal = event.persisted + || (typeof window.performance !== 'undefined' + && window.performance.getEntriesByType('navigation')[0].type === 'back_forward'); + if (historyTraversal) { + window.location.reload(); + } + }); +} diff --git a/unitylibs/blocks/unity/unity.js b/unitylibs/blocks/unity/unity.js index 4c9a896bf..a052c488b 100644 --- a/unitylibs/blocks/unity/unity.js +++ b/unitylibs/blocks/unity/unity.js @@ -1,5 +1,6 @@ import { loadStyle } from '../../scripts/utils.js'; + function getUnityLibs(prodLibs, project = 'unity') { const { hostname, origin } = window.location; if (project === 'unity') { return `${origin}/unitylibs`; } diff --git a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css index c48db813a..59a6db0a1 100644 --- a/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css +++ b/unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css @@ -943,8 +943,7 @@ margin: 0; .pbu-preview { position: absolute; -top: 0; -left: 0; +inset: 0; width: 100%; height: 100%; box-sizing: border-box; @@ -958,11 +957,14 @@ display: none; .pbu-preview-img { display: block; -width: stretch; -height: stretch; +width: 100%; +height: 100%; +max-width: none; object-fit: cover; +object-position: center; border-radius: 13.667px; border: 2px solid #4069FD; +box-sizing: border-box; } .unity-prompt-bar-upload.unity-enabled .pbu-delete-btn { diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index e679a60a4..4a5b11b24 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -79,6 +79,7 @@ export default class ActionBinder { 'quiz-maker': ['hybrid', 'allowed-filetypes-study-spaces', 'page-limit-600', 'max-numfiles-100', 'max-filesize-100-mb'], 'flashcard-maker': ['hybrid', 'allowed-filetypes-study-spaces', 'page-limit-600', 'max-numfiles-100', 'max-filesize-100-mb'], 'mindmap-maker': ['hybrid', 'allowed-filetypes-study-spaces', 'page-limit-600', 'max-numfiles-100', 'max-filesize-100-mb'], + 'resume-builder': ['single', 'allowed-filetypes-resume', 'page-limit-10', 'max-filesize-20-mb'], }; static ERROR_MAP = { @@ -91,6 +92,7 @@ export default class ActionBinder { pre_upload_error_create_asset: -55, pre_upload_error_missing_verb_config: -56, pre_upload_error_transition_screen: -57, + pre_upload_error_direct_upload: -58, validation_error_validate_files: -100, validation_error_unsupported_type: -101, validation_error_empty_file: -102, @@ -162,7 +164,7 @@ export default class ActionBinder { this.actionMap = actionMap; this.limits = {}; this.operations = []; - this.acrobatApiConfig = this.getAcrobatApiConfig(); + this.acrobatApiConfig = null; this.networkUtils = new NetworkUtils(); this.uploadHandler = null; this.splashScreenEl = null; @@ -183,6 +185,10 @@ export default class ActionBinder { this.multiFileValidationFailure = false; this.initialize(); this.experimentData = null; + this.experimentViaPageConfig = false; + this.pageConfigLocation = null; + this.pageConfigFetched = false; + this.pageConfigPromise = null; } async initialize() { @@ -225,11 +231,14 @@ export default class ActionBinder { } getAcrobatApiConfig() { + const base = this.pageConfigLocation ? `${this.pageConfigLocation}/api/v1` : unityConfig.apiEndPoint; unityConfig.acrobatEndpoint = { - createAsset: `${unityConfig.apiEndPoint}/asset`, - finalizeAsset: `${unityConfig.apiEndPoint}/asset/finalize`, - getMetadata: `${unityConfig.apiEndPoint}/asset/metadata`, + createAsset: `${base}/asset`, + finalizeAsset: `${base}/asset/finalize`, + getMetadata: `${base}/asset/metadata`, + directUpload: `${base}/asset/upload`, }; + unityConfig.connectorApiEndPoint = `${base}/asset/connector`; return unityConfig; } @@ -243,18 +252,6 @@ export default class ActionBinder { } async handlePreloads() { - if (!this.experimentData && this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0])) { - const { getExperimentData, getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); - try { - const decisionScopes = await getDecisionScopesForVerb(this.workflowCfg.enabledFeatures[0]); - this.experimentData = await getExperimentData(decisionScopes); - } catch (error) { - await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { - code: 'warn_fetch_experiment', - desc: error.message, - }); - } - } const parr = []; if (this.workflowCfg.targetCfg.showSplashScreen) { parr.push( @@ -264,6 +261,32 @@ export default class ActionBinder { await priorityLoad(parr); } + async ensurePageConfig() { + if (this.pageConfigFetched) return; + this.pageConfigFetched = true; + const verb = this.workflowCfg.enabledFeatures[0]; + try { + const { fetchPageConfig } = await import('../../../scripts/utils.js'); + const { default: getExperimentData } = await import('../../../utils/experiment-provider.js'); + const pageConfig = await fetchPageConfig({ product: 'acrobat', verb }); + this.pageConfigLocation = pageConfig.location; + if (pageConfig.config?.target?.enabled) { + this.experimentData = await getExperimentData(pageConfig.config.target.decisionScopes); + this.experimentViaPageConfig = true; + } else if (!this.experimentData && this.workflowCfg.targetCfg?.experimentationOn?.includes(verb)) { + const { getDecisionScopesForVerb } = await import('../../../utils/experiment-provider.js'); + const decisionScopes = await getDecisionScopesForVerb(verb); + this.experimentData = await getExperimentData(decisionScopes); + } + } catch (error) { + await this.dispatchErrorToast('warn_fetch_experiment', null, error.message, true, true, { + code: 'warn_fetch_experiment', + desc: error.message, + }); + } + this.acrobatApiConfig = this.getAcrobatApiConfig(); + } + async dispatchErrorToast(errorType, status, info = null, lanaOnly = false, showError = true, errorMetaData = {}) { if (!showError) return; const errorMessage = errorType in this.workflowCfg.errors @@ -458,7 +481,7 @@ export default class ActionBinder { redirectUrl = url.href; } } - this.redirectUrl = redirectUrl; + this.redirectUrl = redirectUrl; }) .catch(async (e) => { await this.showTransitionScreen(); @@ -487,7 +510,7 @@ export default class ActionBinder { if (this.multiFileValidationFailure) cOpts.payload.feedback = 'uploaderror'; if (this.showInfoToast) cOpts.payload.feedback = 'nonpdf'; } - if (this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0]) && this.experimentData) { + if (this.experimentData && (this.experimentViaPageConfig || this.workflowCfg.targetCfg?.experimentationOn?.includes(this.workflowCfg.enabledFeatures[0]))) { cOpts.payload.variationId = this.experimentData.variationId; } await this.getRedirectUrl(cOpts); @@ -557,6 +580,7 @@ export default class ActionBinder { if (prevalidatedFiles.length === 0) return; const { isValid, validFiles } = await this.validateFiles(prevalidatedFiles); if (!isValid) return; + await (this.pageConfigPromise || this.ensurePageConfig()); await this.initUploadHandler(); if (files.length === 1 || (validFiles.length === 1 && !verbsWithoutFallback.includes(this.workflowCfg.enabledFeatures[0]))) { await this.handleSingleFileUpload(validFiles); @@ -768,6 +792,7 @@ export default class ActionBinder { } if (b === this.block) { this.loadTransitionScreen(); + this.pageConfigPromise = this.ensurePageConfig(); } } } diff --git a/unitylibs/core/workflow/workflow-acrobat/limits.json b/unitylibs/core/workflow/workflow-acrobat/limits.json index 4ac878d50..18cb2692e 100644 --- a/unitylibs/core/workflow/workflow-acrobat/limits.json +++ b/unitylibs/core/workflow/workflow-acrobat/limits.json @@ -62,6 +62,11 @@ "pageLimit": { "maxNumPages": 600 } + }, + "page-limit-20": { + "pageLimit": { + "maxNumPages": 20 + } }, "max-filesize-5-mb": { "maxFileSize": 5242880 @@ -84,6 +89,9 @@ "max-numfiles-100": { "maxNumFiles": 100 }, + "max-filesize-20-mb": { + "maxFileSize": 20971520 + }, "allowed-filetypes-pdf-only": { "allowedFileTypes": ["application/pdf"] }, @@ -176,5 +184,12 @@ "text/plain", "text/vtt" ] + }, + "allowed-filetypes-resume": { + "allowedFileTypes": [ + "application/pdf", + "application/msword", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + ] } -} +} \ No newline at end of file diff --git a/unitylibs/core/workflow/workflow-acrobat/target-config.json b/unitylibs/core/workflow/workflow-acrobat/target-config.json index ab7f56ad4..b3d5fba91 100644 --- a/unitylibs/core/workflow/workflow-acrobat/target-config.json +++ b/unitylibs/core/workflow/workflow-acrobat/target-config.json @@ -14,6 +14,9 @@ }, "sendSplunkAnalytics": true, "verbsWithoutMfuToSfuFallback": ["compress-pdf"], + "awaitFinalizeVerbs": ["word-to-pdf"], + "directUploadVerbs": ["word-to-pdf"], + "directUploadMaxSize": 1048576, "nonpdfMfuFeedbackScreenTypeNonpdf": ["combine-pdf"], "nonpdfSfuProductScreen": ["word-to-pdf", "jpg-to-pdf", "ppt-to-pdf", "excel-to-pdf", "png-to-pdf", "createpdf", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "heic-to-pdf", "quiz-maker", "flashcard-maker", "mindmap-maker"], "mfuUploadAllowed": ["combine-pdf", "rotate-pages", "chat-pdf", "chat-pdf-student", "summarize-pdf", "pdf-ai", "quiz-maker", "flashcard-maker", "mindmap-maker"], @@ -85,5 +88,19 @@ ".foreground": "upload", "#file-upload": "upload" } + }, + "unity-verb-marquee": { + "selector": ".foreground", + "source": ".foreground .unity-verb-marquee-container", + "target": ".foreground .unity-verb-marquee-container", + "showSplashScreen": true, + "splashScreenConfig": { + "fragmentLink": "/drafts/ruchika/splashscreen", + "splashScreenParent": "body" + }, + "actionMap": { + ".foreground": "upload", + "#file-upload": "upload" + } } } diff --git a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js index ba04bef4e..3f2fbf2ce 100644 --- a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js +++ b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js @@ -19,6 +19,77 @@ export default class UploadHandler { return feature === 'pdf-ai' ? 'chat-pdf-pdf-ai' : feature; } + isDirectUpload(file) { + const verb = this.actionBinder.workflowCfg.enabledFeatures[0]; + const directUploadVerbs = this.actionBinder.workflowCfg.targetCfg.directUploadVerbs || []; + const directUploadMaxSize = this.actionBinder.workflowCfg.targetCfg.directUploadMaxSize || 0; + return directUploadVerbs.includes(verb) && file.size <= directUploadMaxSize; + } + + async directUploadAsset(file, signal, workflowId = null) { + const formData = new FormData(); + formData.append('surfaceId', unityConfig.surfaceId); + formData.append('targetProduct', this.actionBinder.workflowCfg.productName); + formData.append('name', file.name); + formData.append('size', file.size); + formData.append('format', 'application/pdf'); + formData.append('file', file); + if (workflowId) formData.append('workflowId', workflowId); + const opts = await getApiCallOptions( + 'POST', + unityConfig.apiKey, + this.actionBinder.getAdditionalHeaders() || {}, + { body: formData, signal }, + ); + delete opts.headers['Content-Type']; + const { response, attempt } = await this.networkUtils.fetchFromServiceWithRetry( + this.actionBinder.acrobatApiConfig.acrobatEndpoint.directUpload, + opts, + this.actionBinder.workflowCfg.targetCfg.fetchApiConfig.default, + ); + return { ...response, attempt }; + } + + async directUploadSingleFile(file, fileData, isPdf = true) { + const abortSignal = this.actionBinder.getAbortSignal(); + this.actionBinder.dispatchAnalyticsEvent('uploading', fileData); + this.actionBinder.setIsUploading(true); + let assetData; + try { + assetData = await this.directUploadAsset(file, abortSignal); + } catch (error) { + this.initSplashScreen(); + await this.transitionScreen.showSplashScreen(); + this.handleUploadError(error, 'pre_upload_error_direct_upload'); + return false; + } + fileData.assetId = assetData.id; + this.actionBinder.setAssetId(assetData.id); + const effectiveFileType = await this.getEffectiveFileType(file); + const cOpts = { + assetId: assetData.id, + targetProduct: this.actionBinder.workflowCfg.productName, + payload: { + languageRegion: this.actionBinder.workflowCfg.langRegion, + languageCode: this.actionBinder.workflowCfg.langCode, + verb: this.getVerbForFeature(), + assetMetadata: { [assetData.id]: { name: file.name, size: file.size, type: effectiveFileType } }, + ...(!isPdf ? { feedback: 'nonpdf' } : {}), + }, + }; + const redirectSuccess = await this.actionBinder.handleRedirect(cOpts, fileData); + if (!redirectSuccess) return false; + + this.actionBinder.operations.push(assetData.id); + this.actionBinder.uploadTimestamp = Date.now(); + this.actionBinder.dispatchAnalyticsEvent('uploaded', { + ...fileData, + assetId: assetData.id, + maxRetryCount: assetData.attempt || 0, + }); + return true; + } + async getEffectiveFileType(file) { const { getExtension } = await import('../../../utils/FileUtils.js'); const isHeicWithoutMimeType = this.actionBinder.workflowCfg.enabledFeatures[0] === 'heic-to-pdf' @@ -198,7 +269,22 @@ export default class UploadHandler { return { failedFiles, attemptMap }; } + isAwaitFinalizeVerb(verb) { + const awaitFinalizeVerbs = this.actionBinder.workflowCfg.targetCfg.awaitFinalizeVerbs || []; + return awaitFinalizeVerbs.includes(verb); + } + + getFinalizeRetryConfig(verb) { + const baseConfig = this.actionBinder.workflowCfg.targetCfg.fetchApiConfig.finalizeAsset; + if (this.isAwaitFinalizeVerb(verb)) { + return { ...baseConfig, retryOn202: false }; + } + return baseConfig; + } + async verifyContent(assetData, signal) { + const verb = this.actionBinder.workflowCfg.enabledFeatures[0]; + const finalizeRetryConfig = this.getFinalizeRetryConfig(verb); try { const finalAssetData = { surfaceId: unityConfig.surfaceId, @@ -214,7 +300,8 @@ export default class UploadHandler { const finalizeJson = await this.networkUtils.fetchFromServiceWithRetry( this.actionBinder.acrobatApiConfig.acrobatEndpoint.finalizeAsset, finalizeOpts, - this.actionBinder.workflowCfg.targetCfg.fetchApiConfig.finalizeAsset, + finalizeRetryConfig, + (responseJson) => responseJson, ); if (!finalizeJson || Object.keys(finalizeJson).length !== 0) { if (this.actionBinder.MULTI_FILE) { @@ -338,6 +425,11 @@ export default class UploadHandler { } async uploadSingleFile(file, fileData, isPdf = true) { + if (this.isDirectUpload(file)) { + const success = await this.directUploadSingleFile(file, fileData, isPdf); + if (success) return; + } + const { maxConcurrentChunks } = this.getConcurrentLimits(); const abortSignal = this.actionBinder.getAbortSignal(); let cOpts = {}; diff --git a/unitylibs/core/workflow/workflow.js b/unitylibs/core/workflow/workflow.js index 93af64e0c..2289ba684 100644 --- a/unitylibs/core/workflow/workflow.js +++ b/unitylibs/core/workflow/workflow.js @@ -288,7 +288,7 @@ class WfInitiator { getEnabledFeatures() { const { supportedFeatures, supportedTexts } = this.workflowCfg; - const verbWidget = this.el.closest('.section')?.querySelector('.verb-widget, .study-marquee, .verb-marquee'); + const verbWidget = this.el.closest('.section')?.querySelector('.verb-widget, .study-marquee, .verb-marquee, .unity-verb-marquee'); if (verbWidget) { const verb = [...verbWidget.classList].find((cn) => supportedFeatures.has(cn)); if (verb) this.workflowCfg.enabledFeatures.push(verb); diff --git a/unitylibs/scripts/alloy/verb-widget.js b/unitylibs/scripts/alloy/verb-widget.js new file mode 100644 index 000000000..e4d476f94 --- /dev/null +++ b/unitylibs/scripts/alloy/verb-widget.js @@ -0,0 +1,217 @@ +const params = new Proxy( + // eslint-disable-next-line compat/compat + new URLSearchParams(window.location.search), + { get: (searchParams, prop) => searchParams.get(prop) }, +); + +let appReferrer = params.x_api_client_id || params['x-product'] || ''; +if (params.x_api_client_location || params['x-product-location']) { + appReferrer = `${appReferrer}:${params.x_api_client_location || params['x-product-location']}`; +} +let trackingId = params.trackingid || ''; +if (params.mv) { + trackingId = `${trackingId}:${params.mv}`; +} +if (params.mv2) { + trackingId = `${trackingId}:${params.mv2}`; +} +const appTags = []; +if (params.workflow) { + appTags.push(params.workflow); +} +if (params.dropzone2) { + appTags.push('dropzone2'); +} + +function ensureSatelliteReady(callback) { + // eslint-disable-next-line no-underscore-dangle + if (window._satellite?.track instanceof Function) { + callback(); + } else { + setTimeout(() => ensureSatelliteReady(callback), 50); + } +} + +function getSessionID() { + const aToken = window.adobeIMS.getAccessToken(); + const arrayToken = aToken?.token.split('.'); + if (!arrayToken) return; + const tokenPayload = JSON.parse(atob(arrayToken[1])); + // eslint-disable-next-line consistent-return + return tokenPayload.sub || tokenPayload.user_id; +} + +function eventData(metaData, { appReferrer: referrer, trackingId: tracking }) { + const { + verb, eventName, errorInfo = '', noOfFiles, uploadTime, type, size, count, userAttempts, + } = metaData; + + return { + event: { + pagename: `acrobat:verb-${verb}:${eventName}${errorInfo ? ` ${errorInfo}` : ''}`, + ...(noOfFiles ? { no_of_files: noOfFiles } : {}), + ...(uploadTime ? { uploadTime } : {}), + }, + content: { type, size, count, fileType: type, totalSize: size }, + source: { + user_agent: navigator.userAgent, + lang: document.documentElement.lang, + app_name: `unity:${verb}`, + url: window.location.href, + referrer, + tracking, + }, + user: { + locale: document.documentElement.lang.toLocaleLowerCase(), + id: getSessionID(), + is_authenticated: `${window.adobeIMS?.isSignedInUser() ? 'true' : 'false'}`, + user_tags: [`${localStorage['unity.user'] ? 'frictionless_return_user' : 'frictionless_new_user'}`], + ...(userAttempts && { return_user_type: userAttempts }), + }, + }; +} + +function createPayloadForSplunk(metaData) { + const { + verb, eventName, noOfFiles, uploadTime, type, size, count, workflowStep, + uploadType, userAttempts, errorData, chunkUploadAttempt, chunkNumber, assetId, maxRetryCount, + } = metaData; + + return { + event: { + name: eventName, + category: 'acrobat', + subcategory: verb, + ...(uploadTime && { uploadTime }), + ...(uploadType && { uploadType }), + }, + content: { + type, + size, + count, + fileType: type, + totalSize: size, + ...(workflowStep && { workflowStep }), + ...(noOfFiles && { no_of_files: noOfFiles }), + ...(chunkUploadAttempt && { chunkUploadAttempt }), + ...(chunkNumber && { chunkNumber }), + ...(assetId && { assetId }), + ...(maxRetryCount && { maxRetryCount }), + }, + source: { + user_agent: navigator.userAgent, + lang: document.documentElement.lang, + app_name: 'unity', + url: window.location.href, + }, + user: { + locale: document.documentElement.lang.toLocaleLowerCase(), + id: getSessionID(), + isAuthenticated: `${window.adobeIMS?.isSignedInUser() ? 'true' : 'false'}`, + type: [`${localStorage['unity.user'] ? 'frictionless_return_user' : 'frictionless_new_user'}`], + ...(userAttempts && { return_user_type: userAttempts }), + }, + error: errorData ? { + type: errorData.code, + ...(errorData.subCode && { subCode: errorData.subCode }), + ...(errorData.desc && { desc: errorData.desc }), + } : undefined, + }; +} + +// eslint-disable-next-line max-len, compat/compat +export function sendAnalyticsToSplunk(eventName, verb, metaData, splunkEndpoint, sendBeacon = false) { + try { + const eventDataPayload = createPayloadForSplunk({ ...metaData, eventName, verb }); + const payloadString = JSON.stringify(eventDataPayload); + if (sendBeacon && navigator.sendBeacon + && navigator.sendBeacon(splunkEndpoint, payloadString)) return; + // eslint-disable-next-line compat/compat + fetch(splunkEndpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: payloadString, + }); + } catch (error) { + window.lana?.log( + `An error occurred while sending ${eventName} to splunk, verb: ${verb}, metadata: ${metaData}, error: ${error}`, + { sampleRate: 1, tags: 'DC_Milo,Project Unity (DC)', severity: 'error' }, + ); + } +} + +export function createEventObject(eventName, verb, metaData, trackingParams, documentUnloading) { + const verbEvent = `acrobat:verb-${verb}:${eventName}`; + const eventDataPayload = eventData({ ...metaData, eventName, verb }, trackingParams); + const redirectReady = new CustomEvent('DCUnity:RedirectReady'); + + return { + documentUnloading, + // eslint-disable-next-line + done: function (AJOPropositionResult, error) { + if (!documentUnloading) { + if (eventName === 'job:uploaded') { + window.dispatchEvent(redirectReady); + } + const accountType = window?.adobeIMS?.getAccountType(); + if (error) { + window.lana?.log( + `Error Code: ${error}, Status: 'Unknown', Message: An error occurred while sending ${verbEvent}, Account Type: ${accountType}`, + { sampleRate: 1, tags: 'DC_Milo,Project Unity (DC)', severity: 'error' }, + ); + } + } + }, + data: { + eventType: 'web.webinteraction.linkClicks', + web: { + webInteraction: { + linkClicks: { value: 1 }, + type: 'other', + name: verbEvent, + }, + }, + _adobe_corpnew: { + digitalData: { + primaryEvent: { + eventInfo: { + eventName: `${verbEvent}${metaData.errorInfo ? ` ${metaData.errorInfo}` : ''}`, + value: `${verb} - Frictionless to Acrobat Web`, + }, + }, + dcweb: eventDataPayload, + dcweb2: eventDataPayload, + }, + app: { + appName: 'ACROBAT_WEB_FRICTIONLESS', + appVersion: '1.0', + }, + }, + }, + }; +} + +export default function init(eventName, verb, metaData, documentUnloading = true) { + const trackingParams = { appReferrer, trackingId }; + + const trackEvent = () => { + const event = createEventObject(eventName, verb, metaData, trackingParams, documentUnloading); + // eslint-disable-next-line no-underscore-dangle + window._satellite.track('event', event); + window.alloy_getIdentity + .then((value) => { + window.ecid = value.identity.ECID; + }); + }; + + // eslint-disable-next-line no-underscore-dangle + if (window._satellite?.track instanceof Function) { + // If satellite is already ready, just track immediately + trackEvent(); + } else { + // Otherwise, keep waiting until _satellite is ready + // This should be just a 50 milliseconds delay + ensureSatelliteReady(trackEvent); + } +} + diff --git a/unitylibs/scripts/ping.js b/unitylibs/scripts/ping.js new file mode 100644 index 000000000..fab9bebbe --- /dev/null +++ b/unitylibs/scripts/ping.js @@ -0,0 +1,381 @@ +/* eslint-disable camelcase */ +/* eslint-disable compat/compat */ +/** *********************************************************************** +* ADOBE CONFIDENTIAL +* ___________________ +* +* Copyright 2022 Adobe +* All Rights Reserved. +* +* NOTICE: All information contained herein is, and remains +* the property of Adobe and its suppliers, if any. The intellectual +* and technical concepts contained herein are proprietary to Adobe +* and its suppliers and are protected by all applicable intellectual +* property laws, including trade secret and copyright laws. +* Dissemination of this information or reproduction of this material +* is strictly forbidden unless prior written permission is obtained +* from Adobe. +************************************************************************* */ + +/* eslint no-underscore-dangle: 0, class-methods-use-this: 0, max-len: 0 */ + +// Utility functions +export const getCookie = (cookieName) => { + const cookies = window.document.cookie ? window.document.cookie.split('; ') : []; + const item = cookies.find((cookie) => cookie.trim().startsWith(`${cookieName}=`)); + if (!item) { + return null; + } + const [, ...others] = item.split('='); + return others.join('='); +}; + +export const setCookie = (key, value, attrs = {}) => { + let cookieString = `${key}=${value}`; + if (attrs.domain) { + cookieString += `; domain=${attrs.domain}`; + } + if (attrs.path) { + cookieString += `; path=${attrs.path}`; + } + if (attrs.expires) { + cookieString += `; expires=${attrs.expires}`; + } + if (attrs.maxAge) { + cookieString += `; max-age=${attrs.maxAge}`; + } + if (attrs.secure) { + cookieString += '; secure'; + } + if (attrs.samesite) { + cookieString += `; samesite=${attrs.samesite}`; + } + window.document.cookie = cookieString; +}; + +export const deleteCookie = (cookieName) => { + setCookie(cookieName, '', { + domain: window.location.host.endsWith('.adobe.com') ? '.adobe.com' : '', + path: '/', + maxAge: -86400, + }); +}; + +const getTrackingURL = (env) => { + if (env && env !== 'prod') { + return 'https://acroipm2.stage.adobe.com/acrobat-web'; + } + return 'https://acroipm2.adobe.com/acrobat-web'; +}; + +export const polynomialHash = (str, base = 31, mod = 2 ** 32) => { + if (!str) { + return str; + } + let hashValue = 0; + for (let i = 0; i < str.length; i += 1) { + hashValue = (hashValue * base + str.charCodeAt(i)) % mod; + } + return hashValue; +}; + +// Constants +const PING_TYPE = { + MACHINE: 'machine', + SIGNEDIN: 'signedin', +}; + +export const USER_TYPE = { + PAID: 'paid', + ANON: 'anon', + FREE: 'free', + SIGNEDIN: 'signedin', +}; + +// Configuration objects (interfaces removed - using plain objects) + +// Default ping configuration +const DEFAULT_PING_SCHEMA = { + appIdentifier: '', + appVersion: '', + appReferrer: '', + userType: '', + subscriptionType: '', + locale: '', +}; + +export class PingService { + constructor(options = {}) { + this.locale = options.locale; + this.config = options.config; + this.userId = options.userId; + this.isSignedIn = options.isSignedIn || false; + this.userType = options.userType; + this.subscriptionType = options.subscriptionType; + } + + async getCountryFromGeoService() { + try { + const geoResponse = await fetch('https://geo2.adobe.com/json/'); + if (!geoResponse.ok) { + return null; + } + const geoData = await geoResponse.json(); + return geoData?.country?.toLowerCase() || null; + } catch (error) { + return null; + } + } + + /** + * Deletes all mmac cookies + */ + deleteAllMmacCookies() { + const cookies = window.document.cookie ? window.document.cookie.split('; ') : []; + const mmacCookies = cookies.filter((cookie) => cookie.trim().startsWith('mmac')); + + mmacCookies.forEach((cookie) => { + const cookieName = cookie.split('=')[0]; + deleteCookie(cookieName); + }); + } + + /** + * Determines whether a month has passed since the last fetch date for MAU tracking. + * + * @param {string | Date} lastFetchDate - The date of the last fetch, either as a string or Date object. + * @param {number} currentMonth - The current month as a number (0-11, where 0 is January and 11 is December). + * @param {number} currentYear - The current year as a 4-digit number (e.g., 2024). + * @returns {boolean} - Returns true if more than one month has passed since the last fetch date; otherwise, false. + */ + isMonthPassedForMAUTracking(lastFetchDate, currentMonth, currentYear) { + const lastFetch = new Date(lastFetchDate); + const lastFetchMonth = lastFetch.getMonth(); + const lastFetchYear = lastFetch.getFullYear(); + + // Check if current date is after the last fetch and a month has passed + return currentYear > lastFetchYear || (currentYear === lastFetchYear && currentMonth > lastFetchMonth); + } + + /** + * Validates the app ping configuration object. + * + * @param {Object} pingConfig - The configuration object to be validated. + * @param {string} pingConfig.appPath - The app path, expected to be a non-empty string. + * @returns {boolean} - Returns true if the configuration is valid; otherwise, false. + */ + isValidAppPingConfig(pingConfig) { + // Check if pingConfig is provided + if (!pingConfig) { + return false; + } + + // Check if 'appPath' is present and is a non-empty string + if (!pingConfig.appPath || typeof pingConfig.appPath !== 'string' || pingConfig.appPath.trim() === '') { + return false; + } + + // If all checks pass, return true + return true; + } + + async getCookieKey(pingType, appPath) { + let key = 'mmac'; + + if (pingType === PING_TYPE.MACHINE) { + key += '_machine'; + } else if (pingType === PING_TYPE.SIGNEDIN) { + if (this.userId) { + key += `_${await polynomialHash(this.userId)}`; + } + } + + if (appPath) { + key += `_${appPath}`; + } + return key; + } + + /** + * Checks if the ping has been made for the current month based on ping Type & App. + * @param {string} [pingType] - An string representing the ping Type for which the ping is being checked. + * @param {string} [appPath] - An optional string representing the appPath for which the ping is being checked. + * @returns {Promise} - Returns a promise which will resolve true if no ping has been made for the current month, or if more than a month has passed since the last ping; otherwise, resolves false. + */ + async isPingCurrentMonth(pingType, appPath) { + const currentDate = new Date(); + const currentMonth = currentDate.getMonth(); + const currentYear = currentDate.getFullYear(); + + const key = await this.getCookieKey(pingType, appPath); + + // Get the stored value from cookies + const storedValue = getCookie(key); + + const isMonthPassedForMAUTracking = this.isMonthPassedForMAUTracking(storedValue || '', currentMonth, currentYear); + + const notPingCurrentMonth = !storedValue || isMonthPassedForMAUTracking; + + // Note: Ping tracking for dc_web - logging removed + return !notPingCurrentMonth; + } + + /** + * Constructs a ping URL based on the provided configuration and a default schema. + * + * @param {Object} pingConfig - The configuration object for the ping. + * @param {string} [pingConfig.appPath] - The app path for the ping. Defaults to 'overall' if not provided. + * @param {Object} pingConfig.schema - An object containing additional properties to be appended to the URL path. + * @returns {string} - The constructed URL as a string, with the ping configuration included as path segments. + */ + createPingURL(pingConfig) { + const baseURL = getTrackingURL(this.config?.serverEnv); + + const url = new URL(baseURL); + + if (pingConfig.pingType === PING_TYPE.MACHINE) { + url.pathname += '/machine'; + } else if (pingConfig.pingType === PING_TYPE.SIGNEDIN) { + url.pathname += '/signedin'; + } + + if (pingConfig.appPath) { + url.pathname += `/${encodeURIComponent(pingConfig.appPath)}`; + } else { + url.pathname += '/overall'; + } + + const pingSchema = DEFAULT_PING_SCHEMA; + + // Iterate over the schema object + const pathSegments = Object.keys(pingSchema).map((key) => { + let value = pingConfig.schema[key] || pingSchema[key] || this.config?.[key]; + + if (!value) { + switch (key) { + case 'appIdentifier': + value = this.config?.appName || 'unspecified'; + break; + case 'locale': + value = this.locale || 'unspecified'; + break; + case 'userType': + value = this.userType || (!this.isSignedIn ? USER_TYPE.ANON : 'unspecified'); + break; + case 'subscriptionType': + value = this.subscriptionType || (!this.isSignedIn ? 'Free' : 'unspecified'); + break; + default: + value = 'unspecified'; + } + } + return `/${encodeURIComponent(value)}`; + }); + + url.pathname += pathSegments.join(''); + url.pathname += '/mmac.html'; + + return url.toString(); + } + + /** + * Calculates the expiration date in UTC based on the number of days provided. + * + * @param {number} days - The number of days from the current date to calculate the expiration date. Must be a positive number. + * @returns {string|undefined} - Returns the expiration date as a UTC string if the input is valid; otherwise, returns undefined. + */ + getExpirationInUTC = (days) => { + if (typeof days !== 'number' || days < 0) return undefined; + const expDateUTC = new Date(Date.now() + (days * 24 * 60 * 60 * 1000)); + return expDateUTC.toUTCString(); + }; + + /** + * Sends a ping request to the specified API and handles the response. + * + * @param {string} url - The URL to which the ping request is sent. + * @param {string} pingType - The ping Type to which the ping request is sent. + * @param {string} [appPath] - An optional app path used for tracking pings specific to a app. + * + * @returns {Promise} - An asynchronous function that handles the API response and manages cookie settings for MAU tracking. + */ + async pingAPICall(url, pingType, appPath) { + const key = await this.getCookieKey(pingType, appPath); + const currentDate = new Date(); + const dateString = currentDate.toISOString(); + + try { + setCookie( + key, + dateString, + { + domain: window.location.host.endsWith('.adobe.com') ? '.adobe.com' : '', + path: '/', + expires: this.getExpirationInUTC(31), + samesite: 'None', + secure: true, + }, + ); + + const res = await fetch(url, { + method: 'GET', + credentials: 'omit', + }); + + if (res?.status !== 200) { + deleteCookie(key); + } + } catch (err) { + deleteCookie(key); + } + } + + /** + * Sends a ping event for the overall acrobat web if the ping for the current month has not yet been sent for different ping type. + * + * @param {Object} pingConfig - The configuration object used to generate the ping URL. + */ + async sendOverallPingEvent(pingConfig) { + if (!pingConfig) { + return; + } + + if (!await this.isPingCurrentMonth(PING_TYPE.MACHINE)) { + const url = this.createPingURL({ ...pingConfig, appPath: undefined, pingType: PING_TYPE.MACHINE }); + await this.pingAPICall(url, PING_TYPE.MACHINE); + } + if (this.isSignedIn && this.userId && !await this.isPingCurrentMonth(PING_TYPE.SIGNEDIN)) { + const url = this.createPingURL({ ...pingConfig, appPath: undefined, pingType: PING_TYPE.SIGNEDIN }); + await this.pingAPICall(url, PING_TYPE.SIGNEDIN); + } + } + + /** + * Sends a ping event for both the overall application and a specific app, if applicable, to track Monthly Active Users (MAU). + * + * @param {Object} pingConfig - The configuration object used to generate the ping URL. + * @param {string} [pingConfig.appPath] - The app path for the ping. If provided, a app-specific ping is sent in addition to the overall ping. + */ + async sendPingEvent(pingConfig) { + const country = await this.getCountryFromGeoService(); + + if (['gb', 'uk', null].includes(country)) { + this.deleteAllMmacCookies(); + return; + } + await this.sendOverallPingEvent(pingConfig); + if (!this.isValidAppPingConfig(pingConfig)) { + return; + } + if (!await this.isPingCurrentMonth(PING_TYPE.MACHINE, pingConfig.appPath)) { + const url = this.createPingURL({ ...pingConfig, pingType: PING_TYPE.MACHINE }); + await this.pingAPICall(url, PING_TYPE.MACHINE, pingConfig.appPath); + } + if (this.isSignedIn && this.userId && !await this.isPingCurrentMonth(PING_TYPE.SIGNEDIN, pingConfig.appPath)) { + const url = this.createPingURL({ ...pingConfig, pingType: PING_TYPE.SIGNEDIN }); + await this.pingAPICall(url, PING_TYPE.SIGNEDIN, pingConfig.appPath); + } + } +} + +export default PingService; diff --git a/unitylibs/scripts/utils.js b/unitylibs/scripts/utils.js index 6e023f25c..059892037 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -1,3 +1,7 @@ +export const UNITY_BLOCKS = [ + 'unity-verb-marquee', +]; + export const [setLibs, getLibs] = (() => { let libs; return [ @@ -307,12 +311,14 @@ export const unityConfig = (() => { prod: { apiEndPoint: 'https://unity.adobe.io/api/v1', connectorApiEndPoint: 'https://unity.adobe.io/api/v1/asset/connector', + pageConfigEndPoint: 'https://cdn-unity.adobe.com/api/v1/pageConfig', env: 'prod', ...commoncfg, }, stage: { apiEndPoint: 'https://unity-stage.adobe.io/api/v1', connectorApiEndPoint: 'https://unity-stage.adobe.io/api/v1/asset/connector', + pageConfigEndPoint: 'https://cdn-unity.stage.adobe.com/api/v1/pageConfig', env: 'stage', ...commoncfg, }, @@ -345,3 +351,14 @@ export function sendAnalyticsEvent(event) { export function getMatchedDomain(domainMap = {}, hostname = window.location.hostname) { return Object.keys(domainMap).find((domain) => domainMap[domain].some((pattern) => new RegExp(pattern).test(hostname))); } + +export async function fetchPageConfig({ product, verb }) { + try { + const url = `${unityConfig.pageConfigEndPoint}?product=${product}&verb=${verb}`; + const resp = await fetch(url, { headers: { 'x-api-key': unityConfig.apiKey } }); + if (!resp.ok) throw new Error(`PageConfig fetch failed: ${resp.statusText}`); + return resp.json(); + } catch (e) { + return {}; + } +} diff --git a/unitylibs/utils/NetworkUtils.js b/unitylibs/utils/NetworkUtils.js index 555628b9e..20f7dff1b 100644 --- a/unitylibs/utils/NetworkUtils.js +++ b/unitylibs/utils/NetworkUtils.js @@ -122,6 +122,14 @@ export default class NetworkUtils { } } + shouldRetryPollingRequest(status, retryConfig, customRetryCheckResult) { + if (customRetryCheckResult) return true; + if (status >= 500 && status < 600) return true; + if (status === 429) return true; + if (status === 202 && retryConfig.retryOn202 !== false) return true; + return false; + } + async fetchFromServiceWithServerPollingRetry(url, options, retryConfig, onSuccess, onError) { const maxRetryDelay = retryConfig.retryParams?.maxRetryDelay || 300000; let timeLapsed = 0; @@ -133,7 +141,7 @@ export default class NetworkUtils { const { status, headers } = response; const responseJson = await this.getResponseJson(response); const customRetryCheckResult = retryConfig.extraRetryCheck && await retryConfig.extraRetryCheck(status, responseJson); - if (customRetryCheckResult || status === 202 || (status >= 500 && status < 600) || status === 429) { + if (this.shouldRetryPollingRequest(status, retryConfig, customRetryCheckResult)) { const retryDelay = (parseInt(headers.get('retry-after'), 10) * 1000) || retryConfig.retryParams?.defaultRetryDelay || 5000; await new Promise((resolve) => { setTimeout(resolve, retryDelay); }); timeLapsed += retryDelay; diff --git a/unitylibs/utils/experiment-provider.js b/unitylibs/utils/experiment-provider.js index 013755c2c..f102ba724 100644 --- a/unitylibs/utils/experiment-provider.js +++ b/unitylibs/utils/experiment-provider.js @@ -1,11 +1,5 @@ /* eslint-disable no-underscore-dangle */ -export async function getDecisionScopesForVerb(verb) { - const region = await getRegion().catch(() => undefined); - const verbScope = `acom_unity_acrobat_${verb}`; - return region ? [`${verbScope}_${region}`, verbScope] : [verbScope]; -} - export async function getRegion() { const resp = await fetch('https://geo2.adobe.com/json/', { cache: 'no-cache' }); if (!resp.ok) throw new Error(`Failed to resolve region: ${resp.statusText}`); @@ -14,7 +8,13 @@ export async function getRegion() { return country.toLowerCase(); } -export async function getExperimentData(decisionScopes) { +export async function getDecisionScopesForVerb(verb) { + const region = await getRegion().catch(() => undefined); + const verbScope = `acom_unity_acrobat_${verb}`; + return region ? [`${verbScope}_${region}`, verbScope] : [verbScope]; +} + +export default async function getExperimentData(decisionScopes) { if (!decisionScopes || decisionScopes.length === 0) { throw new Error('No decision scopes provided for experiment data fetch'); }