From 1ef0cf119d7ba84d78836fa58cf521e9d3375c3f Mon Sep 17 00:00:00 2001 From: Sanjay Saravanan <75960494+sanjayms01@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:35:07 -0700 Subject: [PATCH 1/8] MWPW-191927 Implementing Config-Based A/B Testing (#756) --- test/utils/experiment-provider.test.js | 2 +- .../workflow-acrobat/action-binder.js | 56 +++++++++++++------ unitylibs/scripts/utils.js | 13 +++++ unitylibs/utils/experiment-provider.js | 14 ++--- 4 files changed, 59 insertions(+), 26 deletions(-) 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/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index e679a60a4..e4343970b 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -162,7 +162,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 +183,9 @@ export default class ActionBinder { this.multiFileValidationFailure = false; this.initialize(); this.experimentData = null; + this.experimentViaPageConfig = false; + this.pageConfigLocation = null; + this.pageConfigFetched = false; } async initialize() { @@ -225,11 +228,13 @@ 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`, }; + unityConfig.connectorApiEndPoint = `${base}/asset/connector`; return unityConfig; } @@ -243,18 +248,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 +257,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 +477,7 @@ export default class ActionBinder { redirectUrl = url.href; } } - this.redirectUrl = redirectUrl; + this.redirectUrl = redirectUrl; }) .catch(async (e) => { await this.showTransitionScreen(); @@ -487,7 +506,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 +576,7 @@ export default class ActionBinder { if (prevalidatedFiles.length === 0) return; const { isValid, validFiles } = await this.validateFiles(prevalidatedFiles); if (!isValid) return; + await this.ensurePageConfig(); await this.initUploadHandler(); if (files.length === 1 || (validFiles.length === 1 && !verbsWithoutFallback.includes(this.workflowCfg.enabledFeatures[0]))) { await this.handleSingleFileUpload(validFiles); diff --git a/unitylibs/scripts/utils.js b/unitylibs/scripts/utils.js index 6e023f25c..8e573d33c 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -307,12 +307,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 +347,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/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'); } From bf76ad3669d9e47960416ae24b5d673b4b00b95d Mon Sep 17 00:00:00 2001 From: Arushi Gupta <65466846+arugupta1992@users.noreply.github.com> Date: Thu, 14 May 2026 14:46:49 +0530 Subject: [PATCH 2/8] MWPW-194869 : iPad/iPhone: Landscape image preview does not fully occupy upload container (#775) Resolves: [MWPW-194869](https://jira.corp.adobe.com/browse/MWPW-194869) **Test URLs:** - Before: https://main--da-cc--adobecom.aem.live/drafts/arugupta/image-to-video/doodlebug/image-to-video-actual?unitylibs=stage - After: https://main--da-cc--adobecom.aem.live/drafts/arugupta/image-to-video/doodlebug/image-to-video-actual?unitylibs=MWPW-194869 --------- Co-authored-by: Arushi Gupta Co-authored-by: Sanjay Saravanan <75960494+sanjayms01@users.noreply.github.com> Co-authored-by: Vipul Gupta Co-authored-by: vipulg Co-authored-by: Nishant Thakur Co-authored-by: Ratko Zagorac <90400759+zagi25@users.noreply.github.com> Co-authored-by: Ruchika Sinha <69535463+Ruchika4@users.noreply.github.com> Co-authored-by: Manasvi Agrawal --- .../widgets/prompt-bar-upload/prompt-bar-upload.css | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) 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 { From 70e85d2bcc0e7af50d929beadc5f24023eed3a6b Mon Sep 17 00:00:00 2001 From: Ruchika Sinha <69535463+Ruchika4@users.noreply.github.com> Date: Wed, 13 May 2026 22:24:44 -0700 Subject: [PATCH 3/8] [MWPW-194995]Fix delay with config-based A/B testing api initiation (#778) * Fix delay with config-based A/B testing api initiation Resolves: [MWPW-194995(https://jira.corp.adobe.com/browse/MWPW-194995) **Test URLs:** - Before: https://stage--da-dc--adobecom.aem.page/acrobat/online/compress-pdf?martech=off&unitylibs=stage - After: https://stage--da-dc--adobecom.aem.page/acrobat/online/compress-pdf?martech=off&unitylibs=target-test **Implementation details:** `this.pageConfigPromise = null (line 189)` initializes the field so it's always defined on the instance. `this.pageConfigPromise = this.ensurePageConfig() `(line 792, inside initActionListeners when b === this.block) fires the fetch the moment the verb-widget finishes rendering (for performance reasons), no await so it doesn't block listener setup. The promise is stored so it can be joined later. `await (this.pageConfigPromise || this.ensurePageConfig()) `(line 580, in handleFileUpload) if the widget has rendered (normal path), we await the already in flight or resolved promise; if somehow pageConfigPromise is null (edge case: upload triggered before initActionListeners ran with the block), we fall back to calling it directly. The existing pageConfigFetched guard inside ensurePageConfig still prevents double execution. --------- Co-authored-by: Arushi Gupta <65466846+arugupta1992@users.noreply.github.com> Co-authored-by: Arushi Gupta Co-authored-by: Sanjay Saravanan <75960494+sanjayms01@users.noreply.github.com> Co-authored-by: Vipul Gupta Co-authored-by: vipulg Co-authored-by: Nishant Thakur Co-authored-by: Ratko Zagorac <90400759+zagi25@users.noreply.github.com> Co-authored-by: Manasvi Agrawal --- unitylibs/core/workflow/workflow-acrobat/action-binder.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index e4343970b..2c50b7db8 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -186,6 +186,7 @@ export default class ActionBinder { this.experimentViaPageConfig = false; this.pageConfigLocation = null; this.pageConfigFetched = false; + this.pageConfigPromise = null; } async initialize() { @@ -576,7 +577,7 @@ export default class ActionBinder { if (prevalidatedFiles.length === 0) return; const { isValid, validFiles } = await this.validateFiles(prevalidatedFiles); if (!isValid) return; - await this.ensurePageConfig(); + 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); @@ -788,6 +789,7 @@ export default class ActionBinder { } if (b === this.block) { this.loadTransitionScreen(); + this.pageConfigPromise = this.ensurePageConfig(); } } } From 3ae352feb082348b49df0a814863105e12b56aaa Mon Sep 17 00:00:00 2001 From: Ruchika Sinha <69535463+Ruchika4@users.noreply.github.com> Date: Thu, 21 May 2026 10:51:37 -0700 Subject: [PATCH 4/8] Don't wait for finalize (#787) * Add your * Specific * Features or fixes Resolves: [MWPW-NUMBER](https://jira.corp.adobe.com/browse/MWPW-NUMBER) **Test URLs:** - Before: https://main--unity--adobecom.aem.page/?martech=off - After: https://--unity--adobecom.aem.page/?martech=off --------- Co-authored-by: Ruchika Sinha --- .../workflow-acrobat/upload-handler.js | 41 ++----------------- 1 file changed, 4 insertions(+), 37 deletions(-) diff --git a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js index ba04bef4e..638faaa9d 100644 --- a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js +++ b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js @@ -211,46 +211,13 @@ export default class UploadHandler { this.actionBinder.getAdditionalHeaders() || {}, { body: JSON.stringify(finalAssetData), signal }, ); - const finalizeJson = await this.networkUtils.fetchFromServiceWithRetry( + finalizeOpts.keepalive = true; + this.networkUtils.fetchFromServiceWithRetry( this.actionBinder.acrobatApiConfig.acrobatEndpoint.finalizeAsset, finalizeOpts, this.actionBinder.workflowCfg.targetCfg.fetchApiConfig.finalizeAsset, - ); - if (!finalizeJson || Object.keys(finalizeJson).length !== 0) { - if (this.actionBinder.MULTI_FILE) { - await this.actionBinder.dispatchErrorToast('upload_error_finalize_asset', 500, `Unexpected response from finalize call: ${assetData.id}, ${JSON.stringify(finalizeJson || {})}`, false, true, { - code: 'upload_error_finalize_asset', - desc: `Unexpected response from finalize call: ${assetData.id}, ${JSON.stringify(finalizeJson || {})}`, - }); - return false; - } - await this.showSplashScreen(); - await this.actionBinder.dispatchErrorToast('upload_error_finalize_asset', 500, `Unexpected response from finalize call: ${assetData.id}, ${JSON.stringify(finalizeJson)}`, false, true, { - code: 'upload_error_finalize_asset', - desc: `Unexpected response from finalize call: ${assetData.id}, ${JSON.stringify(finalizeJson)}`, - }); - this.actionBinder.operations = []; - return false; - } - } catch (e) { - if (e.name === 'AbortError') return false; - if (this.actionBinder.MULTI_FILE) { - await this.actionBinder.dispatchErrorToast('upload_error_finalize_asset', e.status || 500, `Exception thrown when verifying content: ${e.message}, ${assetData.id}`, false, e.showError, { - code: 'upload_error_finalize_asset', - subCode: e.status, - desc: `Exception thrown when verifying content: ${e.message}, ${assetData.id}`, - }); - return false; - } - await this.showSplashScreen(); - await this.actionBinder.dispatchErrorToast('upload_error_finalize_asset', e.status || 500, `Exception thrown when verifying content: ${e.message}, ${assetData.id}`, false, e.showError, { - code: 'upload_error_finalize_asset', - subCode: e.status, - desc: `Exception thrown when verifying content: ${e.message}, ${assetData.id}`, - }); - this.actionBinder.operations = []; - return false; - } + ).catch(() => {}); + } catch (e) { /* fire-and-forget */ } return true; } From 579511051a930e7b998b3f6d5e65948f892fee14 Mon Sep 17 00:00:00 2001 From: Ruchika Sinha <69535463+Ruchika4@users.noreply.github.com> Date: Tue, 19 May 2026 15:06:30 -0700 Subject: [PATCH 5/8] Revert "Don't await finalize" (#783) Reverts adobecom/unity#782 --- .../workflow-acrobat/upload-handler.js | 41 +++++++++++++++++-- 1 file changed, 37 insertions(+), 4 deletions(-) diff --git a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js index 638faaa9d..ba04bef4e 100644 --- a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js +++ b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js @@ -211,13 +211,46 @@ export default class UploadHandler { this.actionBinder.getAdditionalHeaders() || {}, { body: JSON.stringify(finalAssetData), signal }, ); - finalizeOpts.keepalive = true; - this.networkUtils.fetchFromServiceWithRetry( + const finalizeJson = await this.networkUtils.fetchFromServiceWithRetry( this.actionBinder.acrobatApiConfig.acrobatEndpoint.finalizeAsset, finalizeOpts, this.actionBinder.workflowCfg.targetCfg.fetchApiConfig.finalizeAsset, - ).catch(() => {}); - } catch (e) { /* fire-and-forget */ } + ); + if (!finalizeJson || Object.keys(finalizeJson).length !== 0) { + if (this.actionBinder.MULTI_FILE) { + await this.actionBinder.dispatchErrorToast('upload_error_finalize_asset', 500, `Unexpected response from finalize call: ${assetData.id}, ${JSON.stringify(finalizeJson || {})}`, false, true, { + code: 'upload_error_finalize_asset', + desc: `Unexpected response from finalize call: ${assetData.id}, ${JSON.stringify(finalizeJson || {})}`, + }); + return false; + } + await this.showSplashScreen(); + await this.actionBinder.dispatchErrorToast('upload_error_finalize_asset', 500, `Unexpected response from finalize call: ${assetData.id}, ${JSON.stringify(finalizeJson)}`, false, true, { + code: 'upload_error_finalize_asset', + desc: `Unexpected response from finalize call: ${assetData.id}, ${JSON.stringify(finalizeJson)}`, + }); + this.actionBinder.operations = []; + return false; + } + } catch (e) { + if (e.name === 'AbortError') return false; + if (this.actionBinder.MULTI_FILE) { + await this.actionBinder.dispatchErrorToast('upload_error_finalize_asset', e.status || 500, `Exception thrown when verifying content: ${e.message}, ${assetData.id}`, false, e.showError, { + code: 'upload_error_finalize_asset', + subCode: e.status, + desc: `Exception thrown when verifying content: ${e.message}, ${assetData.id}`, + }); + return false; + } + await this.showSplashScreen(); + await this.actionBinder.dispatchErrorToast('upload_error_finalize_asset', e.status || 500, `Exception thrown when verifying content: ${e.message}, ${assetData.id}`, false, e.showError, { + code: 'upload_error_finalize_asset', + subCode: e.status, + desc: `Exception thrown when verifying content: ${e.message}, ${assetData.id}`, + }); + this.actionBinder.operations = []; + return false; + } return true; } From fa96cabae3a699a804ddf51e2c24ce3e82e8835b Mon Sep 17 00:00:00 2001 From: Ruchika Sinha Date: Thu, 21 May 2026 21:29:56 -0700 Subject: [PATCH 6/8] conditional finalize await --- .../workflow/workflow-acrobat/target-config.json | 1 + .../workflow/workflow-acrobat/upload-handler.js | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/unitylibs/core/workflow/workflow-acrobat/target-config.json b/unitylibs/core/workflow/workflow-acrobat/target-config.json index ab7f56ad4..dae8c8e31 100644 --- a/unitylibs/core/workflow/workflow-acrobat/target-config.json +++ b/unitylibs/core/workflow/workflow-acrobat/target-config.json @@ -14,6 +14,7 @@ }, "sendSplunkAnalytics": true, "verbsWithoutMfuToSfuFallback": ["compress-pdf"], + "fireAndForgetFinalize": ["word-to-pdf"], "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"], diff --git a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js index ba04bef4e..e41ca4160 100644 --- a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js +++ b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js @@ -199,6 +199,9 @@ export default class UploadHandler { } async verifyContent(assetData, signal) { + const verb = this.actionBinder.workflowCfg.enabledFeatures[0]; + const fireAndForgetVerbs = this.actionBinder.workflowCfg.targetCfg.fireAndForgetFinalize || []; + const isFireAndForget = fireAndForgetVerbs.includes(verb); try { const finalAssetData = { surfaceId: unityConfig.surfaceId, @@ -211,6 +214,15 @@ export default class UploadHandler { this.actionBinder.getAdditionalHeaders() || {}, { body: JSON.stringify(finalAssetData), signal }, ); + if (isFireAndForget) { + finalizeOpts.keepalive = true; + this.networkUtils.fetchFromServiceWithRetry( + this.actionBinder.acrobatApiConfig.acrobatEndpoint.finalizeAsset, + finalizeOpts, + this.actionBinder.workflowCfg.targetCfg.fetchApiConfig.finalizeAsset, + ).catch(() => {}); + return true; + } const finalizeJson = await this.networkUtils.fetchFromServiceWithRetry( this.actionBinder.acrobatApiConfig.acrobatEndpoint.finalizeAsset, finalizeOpts, @@ -233,6 +245,7 @@ export default class UploadHandler { return false; } } catch (e) { + if (isFireAndForget) return true; if (e.name === 'AbortError') return false; if (this.actionBinder.MULTI_FILE) { await this.actionBinder.dispatchErrorToast('upload_error_finalize_asset', e.status || 500, `Exception thrown when verifying content: ${e.message}, ${assetData.id}`, false, e.showError, { From 87a9a9eb6ed4f5582cf75ee5239ab036c17e8076 Mon Sep 17 00:00:00 2001 From: Sanjay Saravanan Date: Fri, 22 May 2026 16:42:29 -0700 Subject: [PATCH 7/8] =?UTF-8?q?Direct=20upload=20for=20small=20files=20(wo?= =?UTF-8?q?rd-to-pdf,=20=E2=89=A41MB)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit For files at or under 1MB on the word-to-pdf verb, skip the createAsset → chunkPdf → finalize chain and instead POST the file directly as multipart to /asset/upload, then call /asset/connector. Reduces upload round-trips from 4 to 2 for small files. Also incorporates verb-finalize optimization: verifyContent is conditional fire-and-forget for verbs in fireAndForgetFinalize. Points stage environment at unity-dev for testing the new API. Co-Authored-By: Claude Sonnet 4.6 --- .../workflow-acrobat/action-binder.js | 2 + .../workflow-acrobat/target-config.json | 2 + .../workflow-acrobat/upload-handler.js | 73 +++++++++++++++++++ unitylibs/scripts/utils.js | 6 +- 4 files changed, 80 insertions(+), 3 deletions(-) diff --git a/unitylibs/core/workflow/workflow-acrobat/action-binder.js b/unitylibs/core/workflow/workflow-acrobat/action-binder.js index 2c50b7db8..649a8d753 100644 --- a/unitylibs/core/workflow/workflow-acrobat/action-binder.js +++ b/unitylibs/core/workflow/workflow-acrobat/action-binder.js @@ -91,6 +91,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, @@ -234,6 +235,7 @@ export default class ActionBinder { createAsset: `${base}/asset`, finalizeAsset: `${base}/asset/finalize`, getMetadata: `${base}/asset/metadata`, + directUpload: `${base}/asset/upload`, }; unityConfig.connectorApiEndPoint = `${base}/asset/connector`; return unityConfig; diff --git a/unitylibs/core/workflow/workflow-acrobat/target-config.json b/unitylibs/core/workflow/workflow-acrobat/target-config.json index dae8c8e31..c68037877 100644 --- a/unitylibs/core/workflow/workflow-acrobat/target-config.json +++ b/unitylibs/core/workflow/workflow-acrobat/target-config.json @@ -15,6 +15,8 @@ "sendSplunkAnalytics": true, "verbsWithoutMfuToSfuFallback": ["compress-pdf"], "fireAndForgetFinalize": ["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"], diff --git a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js index e41ca4160..b7fad8cb8 100644 --- a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js +++ b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js @@ -19,6 +19,74 @@ 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(); + 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.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' @@ -351,6 +419,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/scripts/utils.js b/unitylibs/scripts/utils.js index 8e573d33c..551a21fd0 100644 --- a/unitylibs/scripts/utils.js +++ b/unitylibs/scripts/utils.js @@ -312,9 +312,9 @@ export const unityConfig = (() => { ...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', + apiEndPoint: 'https://unity-dev.adobe.io/api/v1', + connectorApiEndPoint: 'https://unity-dev.adobe.io/api/v1/asset/connector', + pageConfigEndPoint: 'https://cdn-unity.dev.adobe.com/api/v1/pageConfig', env: 'stage', ...commoncfg, }, From f82cd58cc20117b430c32b847b58032b48d71c32 Mon Sep 17 00:00:00 2001 From: Sanjay Saravanan Date: Fri, 22 May 2026 17:02:02 -0700 Subject: [PATCH 8/8] Fix: directUploadSingleFile stuck on transition screen at 95% continueInApp() guards on operations.length before navigating; directUploadSingleFile never pushed to operations so the guard always failed and window.location.href was never set. Also adds 'uploading' analytics event and setIsUploading(true) to match the standard upload flow sequence. Co-Authored-By: Claude Sonnet 4.6 --- unitylibs/core/workflow/workflow-acrobat/upload-handler.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js index b7fad8cb8..2a1462c99 100644 --- a/unitylibs/core/workflow/workflow-acrobat/upload-handler.js +++ b/unitylibs/core/workflow/workflow-acrobat/upload-handler.js @@ -52,6 +52,8 @@ export default class UploadHandler { 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); @@ -78,6 +80,7 @@ export default class UploadHandler { 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,