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 @@
+
+
+
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');
}