Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion test/utils/experiment-provider.test.js
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 6 additions & 4 deletions unitylibs/core/widgets/prompt-bar-upload/prompt-bar-upload.css
Original file line number Diff line number Diff line change
Expand Up @@ -943,8 +943,7 @@ margin: 0;

.pbu-preview {
position: absolute;
top: 0;
left: 0;
inset: 0;
width: 100%;
height: 100%;
box-sizing: border-box;
Expand All @@ -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 {
Expand Down
60 changes: 42 additions & 18 deletions unitylibs/core/workflow/workflow-acrobat/action-binder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -162,7 +163,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;
Expand All @@ -183,6 +184,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() {
Expand Down Expand Up @@ -225,11 +230,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;
}

Expand All @@ -243,18 +251,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(
Expand All @@ -264,6 +260,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
Expand Down Expand Up @@ -458,7 +480,7 @@ export default class ActionBinder {
redirectUrl = url.href;
}
}
this.redirectUrl = redirectUrl;
this.redirectUrl = redirectUrl;
})
.catch(async (e) => {
await this.showTransitionScreen();
Expand Down Expand Up @@ -487,7 +509,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);
Expand Down Expand Up @@ -557,6 +579,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);
Expand Down Expand Up @@ -768,6 +791,7 @@ export default class ActionBinder {
}
if (b === this.block) {
this.loadTransitionScreen();
this.pageConfigPromise = this.ensurePageConfig();
}
}
}
3 changes: 3 additions & 0 deletions unitylibs/core/workflow/workflow-acrobat/target-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
},
"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"],
Expand Down
89 changes: 89 additions & 0 deletions unitylibs/core/workflow/workflow-acrobat/upload-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -199,6 +270,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,
Expand All @@ -211,6 +285,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,
Expand All @@ -233,6 +316,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, {
Expand Down Expand Up @@ -338,6 +422,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 = {};
Expand Down
17 changes: 15 additions & 2 deletions unitylibs/scripts/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
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,
},
Expand Down Expand Up @@ -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 {};
}
}
14 changes: 7 additions & 7 deletions unitylibs/utils/experiment-provider.js
Original file line number Diff line number Diff line change
@@ -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}`);
Expand All @@ -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');
}
Expand Down
Loading