8000 [3in1] MWPW-173962 by 3ch023 · Pull Request #4258 · adobecom/milo · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

[3in1] MWPW-173962 #4258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jun 2, 2025
Merged
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
87 changes: 46 additions & 41 deletions libs/blocks/merch/merch.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,18 @@ const GeoMap = {
th_th: 'TH_th',
};

/**
* Used when 3in1 modals are configured with ms=e or cs=t extra paramter, but 3in1 is disabled.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: paramter > parameter

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will fix it in the follow up pr, thx!

* Dexter modals should deeplink to plan=edu or plan=team tabs.
* @type {Record<string, string>}
*/
const TAB_DEEPLINK_MAPPING = {
ms: 'plan',
cs: 'plan',
e: 'edu',
t: 'team',
};

const LANG_STORE_PREFIX = 'langstore/';

function getDefaultLangstoreCountry(language) {
Expand Down Expand Up @@ -259,18 +271,6 @@ export const CHECKOUT_ALLOWED_KEYS = [
'marketSegment',
];

/**
* Used when 3in1 modals are configured with ms=e or cs=t extra paramter, but 3in1 is disabled.
* Dexter modals should deeplink to plan=edu or plan=team tabs.
* @type {Record<string, string>}
*/
const TAB_DEEPLINK_MAPPING = {
ms: 'plan',
cs: 'plan',
e: 'edu',
t: 'team',
};

export const CC_SINGLE_APPS_ALL = CC_SINGLE_APPS.flatMap((item) => item);

export const CC_ALL_APPS = ['CC_ALL_APPS',
Expand Down Expand Up @@ -501,51 +501,56 @@ async function openFragmentModal(path, getModal) {
return modal;
}

export function appendTabName(url) {
function appendTabName(url, el) {
if (el?.is3in1Modal) {
if (el.marketSegment === 'EDU') {
url.searchParams.set('plan', 'edu');
} else if (el.customerSegment === 'TEAM') {
url.searchParams.set('plan', 'team');
}
}
const metaPreselectPlan = document.querySelector('meta[name="preselect-plan"]');
if (!metaPreselectPlan?.content) return url;
const isRelativePath = url.startsWith('/');
let urlWithPlan;
try {
urlWithPlan = isRelativePath ? new URL(`${window.location.origin}${url}`) : new URL(url);
< 8000 /td> } catch (err) {
window.lana?.log(`Invalid URL ${url} : ${err}`);
return url;
}
urlWithPlan.searchParams.set('plan', metaPreselectPlan.content);
return isRelativePath ? urlWithPlan.href.replace(window.location.origin, '') : urlWithPlan.href;
url.searchParams.set('plan', metaPreselectPlan.content);
return url;
}

export function appendExtraOptions(url, extraOptions) {
function appendExtraOptions(url, extraOptions) {
if (!extraOptions) return url;
const extraOptionsObj = JSON.parse(extraOptions);
let urlWithExtraOptions;
try {
const fullUrl = url.startsWith('/') ? `${window.location.origin}${url}` : url;
urlWithExtraOptions = new URL(fullUrl);
} catch (err) {
window.lana?.log(`Invalid URL ${url} : ${err}`);
return url;
}
Object.keys(extraOptionsObj).forEach((key) => {
if (CHECKOUT_ALLOWED_KEYS.includes(key)) {
const value = extraOptionsObj[key];
urlWithExtraOptions.searchParams.set(
url.searchParams.set(
TAB_DEEPLINK_MAPPING[key] ?? key,
TAB_DEEPLINK_MAPPING[value] ?? value,
);
}
});
return urlWithExtraOptions.href;
return url;
}

// TODO this should migrate to checkout.js buildCheckoutURL
export function appendDexterParameters(url, extraOptions, el) {
const isRelativePath = url.startsWith('/');
let absoluteUrl;
try {
absoluteUrl = new URL(isRelativePath ? `${window.location.origin}${url}` : url);
} catch (err) {
window.lana?.log(`Invalid URL ${url} : ${err}`);
return url;
}
absoluteUrl = appendExtraOptions(absoluteUrl, extraOptions);
absoluteUrl = appendTabName(absoluteUrl, el);
return isRelativePath ? absoluteUrl.href.replace(window.location.origin, '') : absoluteUrl.href;
}

async function openExternalModal(url, getModal, extraOptions) {
async function openExternalModal(url, getModal, extraOptions, el) {
loadStyle(`${getConfig().base}/blocks/iframe/iframe.css`);
const root = createTag('div', { class: 'milo-iframe' });
const urlWithExtraOptions = appendExtraOptions(url, extraOptions);
const urlWithTabName = appendTabName(urlWithExtraOptions);
const absoluteUrl = appendDexterParameters(url, extraOptions, el);
createTag('iframe', {
src: urlWithTabName,
src: absoluteUrl,
frameborder: '0',
marginwidth: '0',
marginheight: '0',
Expand Down Expand Up @@ -844,11 +849,11 @@ export async function buildCta(el, params) {
} else if (!cta.ariaLabel) {
cta.onceSettled().then(async () => {
const productFamily = cta.value[0]?.productArrangement?.productFamily;
const marketSegment = cta.value[0]?.marketSegments[0];
const customerSegment = marketSegment === 'EDU' ? marketSegment : cta.value[0]?.customerSegment;
const { marketSegment, customerSegment } = cta;
const segment = marketSegment === 'EDU' ? marketSegment : customerSegment;
let ariaLabel = cta.textContent;
ariaLabel = productFamily ? `${ariaLabel} - ${await replaceKey(productFamily, getConfig())}` : ariaLabel;
ariaLabel = customerSegment ? `${ariaLabel} - ${await replaceKey(customerSegment, getConfig())}` : ariaLabel;
ariaLabel = segment ? `${ariaLabel} - ${await replaceKey(segment, getConfig())}` : ariaLabel;
cta.setAttribute('aria-label', ariaLabel);
});
}
Expand Down
6 changes: 3 additions & 3 deletions libs/deps/mas/commerce.js

Large diffs are not rendered by default.

30 changes: 15 additions & 15 deletions libs/deps/mas/merch-card-collection.js

Large diffs are not rendered by default.

120 changes: 60 additions & 60 deletions libs/features/mas/dist/mas.js

Large diffs are not rendered by default.

59 changes: 26 additions & 33 deletions libs/features/ 9E88 mas/src/buildCheckoutUrl.js
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] reported by reviewdog 🐶
File ignored because of a matching ignore pattern. Use "--no-ignore" to override.

Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const PARAMETERS = new Map([
['clientId', 'cli'],
['context', 'ctx'],
['productArrangementCode', 'pa'],
['addonProductArrangementCode', 'ao'],
['offerType', 'ot'],
['marketSegment', 'ms'],
]);
Expand All @@ -27,6 +28,7 @@ const PARAMETERS = new Map([
const ALLOWED_KEYS = new Set([
'af',
'ai',
'ao',
'apc',
'appctxid',
'cli',
Expand Down Expand Up @@ -132,30 +134,21 @@ export function setItemsParameter(items, parameters) {
* @param {string} addonProductArrangementCode - Addon product arrangement code
* @returns URL object
*/
export function add3in1Parameters({ url, modal, customerSegment, cs, ms, marketSegment, quantity, productArrangementCode, addonProductArrangementCode }) {
const masFF3in1 = document.querySelector('meta[name=mas-ff-3in1]');
if (!Object.va F438 lues(MODAL_TYPE_3_IN_1).includes(modal) || !url?.searchParams || !customerSegment || !marketSegment || (masFF3in1 && masFF3in1.content === 'off')) return url;
export function add3in1Parameters({ url, modal, is3in1 }) {
if (!is3in1 || !url?.searchParams) return url;
url.searchParams.set('rtc', 't');
url.searchParams.set('lo', 'sl');
url.searchParams.set('af', 'uc_new_user_iframe,uc_new_system_close');
if (url.searchParams.get('cli') !== 'doc_cloud') {
url.searchParams.set('cli', modal === MODAL_TYPE_3_IN_1.CRM ? 'creative' : 'mini_plans');
}
if (modal === MODAL_TYPE_3_IN_1.TWP || modal === MODAL_TYPE_3_IN_1.D2P) {
if (customerSegment === 'INDIVIDUAL' && marketSegment === 'EDU') {
url.searchParams.set('ms', 'e');
}
if (customerSegment === 'TEAM' && marketSegment === 'COM') {
url.searchParams.set('cs', 't');
}
// used on catalog page by MEP to preselect plan
const metaPreselectPlan = document.querySelector('meta[name="preselect-plan"]');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildCheckoutUrl.js is should be agnostic of context. It should just generate an URL as string.
Introducing a document.querySelector here seems wrong to me.
I think this should be handled in checkout-mixin.js

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mmm i could move it to checkout.js but not sure how to move it to checkout-mixin

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at https://github.com/adobecom/milo/blob/stage/libs/features/mas/src/checkout-mixin.js#L196
ww are passing offers and options, I think it should be a new option called csOverride .
Ideally this setting is initialized once at libs/features/mas/src/settings.js.

if (metaPreselectPlan?.content?.toLowerCase() === 'edu') {
url.searchParams.set('ms', 'EDU');
} else if (metaPreselectPlan?.content?.toLowerCase() === 'team') {
url.searchParams.set('cs', 'TEAM');
}
if (quantity) url.searchParams.set('q', quantity);
if (addonProductArrangementCode) url.searchParams.set('ao', addonProductArrangementCode);
if (productArrangementCode) url.searchParams.set('pa', productArrangementCode);
// cs and ms are params manually set by authors, they should take precedence over marketSegment and customerSegment
if (cs) url.searchParams.set('cs', cs);
if (ms) url.searchParams.set('ms', ms);
if (url.searchParams.get('ot') === 'PROMOTION') url.searchParams.delete('ot');
return url;
}

Expand All @@ -164,36 +157,36 @@ export function add3in1Parameters({ url, modal, customerSegment, cs, ms, marketS
*/
export function buildCheckoutUrl(checkoutData) {
validateCheckoutData(checkoutData);
const { env, items, workflowStep, ms, cs, marketSegment, customerSegment, ot, offerType, pa, productArrangementCode, landscape, modal, ...rest } =
const { env, items, workflowStep, ms, cs, marketSegment, customerSegment, ot, offerType, pa, productArrangementCode, landscape, modal, is3in1, ...rest } =
checkoutData;
const segmentationParameters = {
marketSegment: marketSegment ?? ms,
offerType: offerType ?? ot,
productArrangementCode: productArrangementCode ?? pa,
};

let url = new URL(getHostName(env));
url.pathname = `${UCV3_PREFIX}${workflowStep}`;
if (workflowStep !== CheckoutWorkflowStep.SEGMENTATION && workflowStep !== CheckoutWorkflowStep.CHANGE_PLAN_TEAM_PLANS) {
setItemsParameter(items, url.searchParams);
}
addParameters({ cs, ...rest }, url.searchParams, ALLOWED_KEYS);
addParameters({ ...rest }, url.searchParams, ALLOWED_KEYS);
if (landscape === Landscape.DRAFT) {
addParameters({ af: AF_DRAFT_LANDSCAPE }, url.searchParams, ALLOWED_KEYS);
}
if (workflowStep === CheckoutWorkflowStep.SEGMENTATION) {
// ms, ot, cs, pa are params manually set by authors, they should take precedence over 'marketSegment', etc
const segmentationParameters = {
marketSegment: ms ?? marketSegment,
offerType: ot ?? offerType,
customerSegment: cs ?? customerSegment,
productArrangementCode: pa ?? productArrangementCode,
quantity: items?.[0]?.quantity > 1 ? items?.[0]?.quantity : undefined,
Copy link
Member
@Axelcureno Axelcureno May 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: store items?.[0] in a variable, it's used in many places.

addonProductArrangementCode: productArrangementCode
? items?.find((item) => item.productArrangementCode !== productArrangementCode)?.productArrangementCode
: items?.[1]?.productArrangementCode,
};
addParameters(segmentationParameters, url.searchParams, ALLOWED_KEYS);
if (url.searchParams.get('ot') === 'PROMOTION') url.searchParams.delete('ot');
url = add3in1Parameters({
url,
modal,
customerSegment,
marketSegment,
cs,
ms,
quantity: items?.[0]?.quantity > 1 && items?.[0]?.quantity,
productArrangementCode,
addonProductArrangementCode: productArrangementCode
? items?.find((item) => item.productArrangementCode !== productArrangementCode)?.productArrangementCode
: items?.[1]?.productArrangementCode,
is3in1,
});
}
return url.toString();
Expand Down
20 changes: 19 additions & 1 deletion libs/features/mas/src/checkout-mixin.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import { MODAL_TYPE_3_IN_1 } from '../src/constants.js';

export const CLASS_NAME_DOWNLOAD = 'download';
export const CLASS_NAME_UPGRADE = 'upgrade';
const CHECKOUT_PARAM_VALUE_MAPPING = {
e: 'EDU',
t: 'TEAM',
};

export function createCheckoutElement(Class, options = {}, innerHTML = '') {
// eslint-disable-next-line react-hooks/rules-of-hooks
Expand Down Expand Up @@ -80,9 +84,23 @@ export function CheckoutMixin(Base) {
return this.masElement.options;
}

get marketSegment() {
const value = this.options?.ms ?? this.value?.[0].marketSegments?.[0];
return CHECKOUT_PARAM_VALUE_MAPPING[value] ?? value;
}

get customerSegment() {
const value = this.options?.cs ?? this.value?.[0]?.customerSegment;
return CHECKOUT_PARAM_VALUE_MAPPING[value] ?? value;
}

get is3in1Modal() {
return Object.values(MODAL_TYPE_3_IN_1).includes(this.getAttribute('data-modal'));
}

get isOpen3in1Modal() {
const masFF3in1 = document.querySelector('meta[name=mas-ff-3in1]');
return Object.values(MODAL_TYPE_3_IN_1).includes(this.getAttribute('data-modal')) && (!masFF3in1 || masFF3in1.content !== 'off');
return this.is3in1Modal && (!masFF3in1 || masFF3in1.content !== 'off');
}

requestUpdate(force = false) {
Expand Down
1 change: 1 addition & 0 deletions libs/features/mas/src/checkout.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export function Checkout({ providers, settings }) {
const is3in1 = Object.values(MODAL_TYPE_3_IN_1).includes(options.modal) && (!masFF3in1 || masFF3in1.content !== 'off');
const context = window.frameElement || is3in1 ? 'if' : 'fp';
const data = {
is3in1,
checkoutPromoCode,
clientId,
context,
Expand Down
23 changes: 16 additions & 7 deletions libs/features/mas/test/build-checkout-url.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ describe('buildCheckoutUrl', () => {
customerSegment: 'INDIVIDUAL',
marketSegment: 'EDU',
modal: 'crm',
is3in1: true,
};
const url = buildCheckoutUrl(checkoutData);
const parsedUrl = new URL(url);
Expand All @@ -101,6 +102,7 @@ describe('buildCheckoutUrl', () => {
customerSegment: 'INDIVIDUAL',
marketSegment: 'EDU',
modal: 'twp',
is3in1: true,
};
const url = buildCheckoutUrl(checkoutData);
const parsedUrl = new URL(url);
Expand All @@ -118,6 +120,7 @@ describe('buildCheckoutUrl', () => {
customerSegment: 'INDIVIDUAL',
marketSegment: 'EDU',
modal: 'd2p',
is3in1: true,
};
const url = buildCheckoutUrl(checkoutData);
const parsedUrl = new URL(url);
Expand All @@ -135,10 +138,11 @@ describe('buildCheckoutUrl', () => {
customerSegment: 'INDIVIDUAL',
marketSegment: 'EDU',
modal: 'twp',
is3in1: true,
};
const url = buildCheckoutUrl(checkoutData);
const parsedUrl = new URL(url);
expect(parsedUrl.searchParams.get('ms')).to.equal('e');
expect(parsedUrl.searchParams.get('ms')).to.equal('EDU');
});

it('should set customer segment for COM team customer', () => {
Expand All @@ -151,10 +155,11 @@ describe('buildCheckoutUrl', () => {
customerSegment: 'TEAM',
marketSegment: 'COM',
modal: 'twp',
is3in1: true,
};
const url = buildCheckoutUrl(checkoutData);
const parsedUrl = new URL(url);
expect(parsedUrl.searchParams.get('cs')).to.equal('t');
expect(parsedUrl.searchParams.get('cs')).to.equal('TEAM');
});

it('should handle addon product arrangement code for 3-in-1 modal', () => {
Expand All @@ -169,14 +174,15 @@ describe('buildCheckoutUrl', () => {
],
modal: 'twp',
customerSegment: 'INDIVIDUAL',
marketSegment: 'EDU'
marketSegment: 'EDU',
is3in1: true,
};
const url = buildCheckoutUrl(checkoutData);
const parsedUrl = new URL(url);
expect(parsedUrl.searchParams.get('ao')).to.equal('ADDON123');
});

it('should respect mas-ff-3in1 meta tag when off', () => {
it('should not set 3in1 parameters when 3in1 is disabled', () => {
const meta = document.createElement('meta');
meta.name = 'mas-ff-3in1';
meta.content = 'off';
Expand All @@ -190,7 +196,8 @@ describe('buildCheckoutUrl', () => {
items: [{ quantity: 1 }],
modal: 'twp',
customerSegment: 'INDIVIDUAL',
marketSegment: 'EDU'
marketSegment: 'EDU',
is3in1: false,
};
const url = buildCheckoutUrl(checkoutData);
const parsedUrl = new URL(url);
Expand All @@ -210,7 +217,8 @@ describe('buildCheckoutUrl', () => {
items: [{ quantity: 1 }],
modal: 'twp',
customerSegment: 'INDIVIDUAL',
marketSegment: 'EDU'
marketSegment: 'EDU',
is3in1: true,
};
const url = buildCheckoutUrl(checkoutData);
const parsedUrl = new URL(url);
Expand Down Expand Up @@ -278,7 +286,8 @@ describe('buildCheckoutUrl', () => {
items: [{ quantity: 1 }],
modal: 'twp',
customerSegment: 'INDIVIDUAL',
marketSegment: 'EDU'
marketSegment: 'EDU',
is3in1: true,
};
const url = buildCheckoutUrl(checkoutData);
const parsedUrl = new URL(url);
Expand Down
Loading
Loading
0