Skip to content
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

MWPW-154098: Retain Modal param while traversing back from Commerce to Page #2781

Merged
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
41 changes: 35 additions & 6 deletions libs/blocks/merch/merch.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ const NAME_LOCALE = 'LOCALE';
const NAME_PRODUCT_FAMILY = 'PRODUCT_FAMILY';
const FREE_TRIAL_PATH = 'FREE_TRIAL_PATH';
const BUY_NOW_PATH = 'BUY_NOW_PATH';
const FREE_TRIAL_HASH = 'FREE_TRIAL_HASH';
const BUY_NOW_HASH = 'BUY_NOW_HASH';
const OFFER_TYPE_TRIAL = 'TRIAL';
const LOADING_ENTITLEMENTS = 'loading-entitlements';

Expand Down Expand Up @@ -357,13 +359,20 @@ async function openExternalModal(url, getModal) {

const isInternalModal = (url) => /\/fragments\//.test(url);

export async function openModal(e, url, offerType) {
export async function openModal(e, url, offerType, hash) {
e.preventDefault();
e.stopImmediatePropagation();
const { getModal } = await import('../modal/modal.js');
await import('../modal/modal.merch.js');
const offerTypeClass = offerType === OFFER_TYPE_TRIAL ? 'twp' : 'crm';
let modal;
if (hash) {
mirafedas marked this conversation as resolved.
Show resolved Hide resolved
const prevHash = window.location.hash.replace('#', '') === hash ? '' : window.location.hash;
window.location.hash = hash;
window.addEventListener('milo:modal:closed', () => {
window.location.hash = prevHash;
}, { once: true });
}
if (isInternalModal(url)) {
const fragmentPath = url.split(/hlx.(page|live)/).pop();
modal = await openFragmentModal(fragmentPath, getModal);
Expand All @@ -375,7 +384,16 @@ export async function openModal(e, url, offerType) {
}
}

export async function getModalAction(offers, options) {
export function setCtaHash(el, checkoutLinkConfig, offerType) {
if (!(el && checkoutLinkConfig && offerType)) return undefined;
const hash = checkoutLinkConfig[`${(offerType === OFFER_TYPE_TRIAL) ? FREE_TRIAL_HASH : BUY_NOW_HASH}`];
if (hash) {
el.setAttribute('data-modal-id', hash);
}
return hash;
}

export async function getModalAction(offers, options, el) {
const [{
offerType,
productArrangementCode,
Expand All @@ -389,20 +407,21 @@ export async function getModalAction(offers, options) {
);
if (!checkoutLinkConfig) return undefined;
const columnName = (offerType === OFFER_TYPE_TRIAL) ? FREE_TRIAL_PATH : BUY_NOW_PATH;
const hash = setCtaHash(el, checkoutLinkConfig, offerType);
let url = checkoutLinkConfig[columnName];
if (!url) return undefined;
url = isInternalModal(url)
? localizeLink(checkoutLinkConfig[columnName]) : checkoutLinkConfig[columnName];
return { url, handler: (e) => openModal(e, url, offerType) };
return { url, handler: (e) => openModal(e, url, offerType, hash) };
}

export async function getCheckoutAction(offers, options, imsSignedInPromise) {
export async function getCheckoutAction(offers, options, imsSignedInPromise, el) {
try {
await imsSignedInPromise;
const [downloadAction, upgradeAction, modalAction] = await Promise.all([
getDownloadAction(options, imsSignedInPromise, offers),
getUpgradeAction(options, imsSignedInPromise, offers),
getModalAction(offers, options),
getModalAction(offers, options, el),
]);
return downloadAction || upgradeAction || modalAction;
} catch (e) {
Expand Down Expand Up @@ -521,6 +540,12 @@ export async function getPriceContext(el, params) {
};
}

export function reopenModal(cta) {
if (cta && cta.getAttribute('data-modal-id') === window.location.hash.replace('#', '')) {
cta.click();
}
}

export async function buildCta(el, params) {
const large = !!el.closest('.marquee');
const strong = el.firstElementChild?.tagName === 'STRONG' || el.parentElement?.tagName === 'STRONG';
Expand All @@ -541,7 +566,11 @@ export async function buildCta(el, params) {
}
if (context.entitlement !== 'false') {
cta.classList.add(LOADING_ENTITLEMENTS);
cta.onceSettled().finally(() => cta.classList.remove(LOADING_ENTITLEMENTS));
cta.onceSettled().finally(() => {
cta.classList.remove(LOADING_ENTITLEMENTS);
// after opening a modal, navigating to another page and back we need to reopen the modal
reopenModal(cta);
});
}
return cta;
}
Expand Down
9 changes: 8 additions & 1 deletion libs/blocks/merch/upgrade.js
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export default async function handleUpgradeOffer(
if (upgradeUrl) {
window.addEventListener('message', handleIFrameEvents);
const { getModal } = await import('../modal/modal.js');
const showModal = async (e) => {
const showModal = async (e, hash) => {
e.preventDefault();
await Promise.all([
import(`${base}/features/spectrum-web-components/dist/theme.js`),
Expand All @@ -140,6 +140,13 @@ export default async function handleUpgradeOffer(
theme.append(pCircle);
content.append(theme);
content.append(iframe);
if (hash) {
const prevHash = window.location.hash.replace('#', '') === hash ? '' : window.location.hash;
window.location.hash = hash;
window.addEventListener('milo:modal:closed', () => {
window.location.hash = prevHash;
}, { once: true });
}
return getModal(null, { id: 'switch-modal', content, closeEvent: 'closeModal', class: ['upgrade-flow-modal'] });
};
const text = await replaceKey('upgrade-now', getConfig());
Expand Down
2 changes: 1 addition & 1 deletion libs/deps/mas/commerce.js

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions libs/features/mas/commerce/src/checkout-link.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export class HTMLCheckoutAnchorElement extends HTMLAnchorElement {
* @param {*} event
*/
clickHandler(event) {
this.#checkoutActionHandler?.(event);
this.#checkoutActionHandler?.(event);
}

async render(overrides = {}) {
Expand Down Expand Up @@ -140,6 +140,7 @@ export class HTMLCheckoutAnchorElement extends HTMLAnchorElement {
const checkoutAction = await service.buildCheckoutAction(
offers.flat(),
{ ...extraOptions, ...options },
this
);
return this.renderOffers(
offers.flat(),
Expand Down Expand Up @@ -185,8 +186,8 @@ export class HTMLCheckoutAnchorElement extends HTMLAnchorElement {
if (text) this.firstElementChild.innerHTML = text;
if (className) this.classList.add(...className.split(' '));
if (handler) {
this.setAttribute('href', '#');
this.#checkoutActionHandler = handler.bind(this);
this.setAttribute('href', '#');
this.#checkoutActionHandler = handler.bind(this);
}
return true;
} else if (offers.length) {
Expand Down
3 changes: 2 additions & 1 deletion libs/features/mas/commerce/src/checkout.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,14 @@ export function Checkout({ providers, settings }, dataProviders) {
}

/** @type {Commerce.Checkout.buildCheckoutAction} */
async function buildCheckoutAction(offers, options) {
async function buildCheckoutAction(offers, options, el) {
// eslint-disable-next-line react-hooks/rules-of-hooks
const instance = useService();
const checkoutAction = await dataProviders.getCheckoutAction?.(
offers,
options,
instance.imsSignedInPromise,
el
);
if (checkoutAction) {
return checkoutAction;
Expand Down
49 changes: 49 additions & 0 deletions test/blocks/merch/merch.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { expect } from '@esm-bundle/chai';
import sinon from 'sinon';
import { delay } from '../../helpers/waitfor.js';

import { CheckoutWorkflow, CheckoutWorkflowStep, Defaults, Log } from '../../../libs/deps/mas/commerce.js';
Expand All @@ -21,6 +22,9 @@ import merch, {
PRICE_TEMPLATE_REGULAR,
getMasBase,
appendTabName,
reopenModal,
setCtaHash,
openModal,
} from '../../../libs/blocks/merch/merch.js';

import { mockFetch, unmockFetch, readMockText } from './mocks/fetch.js';
Expand Down Expand Up @@ -133,6 +137,14 @@ const PROD_DOMAINS = [
'helpx.adobe.com',
];

const createCtaInMerchCard = () => {
const merchCard = document.createElement('merch-card');
merchCard.setAttribute('name', 'photoshop');
const el = document.createElement('a');
merchCard.appendChild(el);
return el;
};

describe('Merch Block', () => {
let setCheckoutLinkConfigs;
let setSubscriptionsData;
Expand Down Expand Up @@ -449,6 +461,33 @@ describe('Merch Block', () => {
const params = new URLSearchParams();
expect(await buildCta(el, params)).to.be.null;
});

describe('reopenModal', () => {
it('clicks the CTA if hashes match', async () => {
const prevHash = window.location.hash;
window.location.hash = '#try-photoshop';
const cta = document.createElement('a');
cta.setAttribute('data-modal-id', 'try-photoshop');
const clickSpy = sinon.spy(cta, 'click');
reopenModal(cta);
expect(clickSpy.called).to.be.true;
window.location.hash = prevHash;
});
});

describe('openModal', () => {
it('sets the new hash and event listener to restore the hash on close', async () => {
const prevHash = window.location.hash;
const event = new CustomEvent('dummy');
await openModal(event, 'https://www.adobe.com/mini-plans/creativecloud.html?mid=ft&web=1', 'TRIAL', 'try-photoshop');
expect(window.location.hash).to.equal('#try-photoshop');
const modalCloseEvent = new CustomEvent('milo:modal:closed');
window.dispatchEvent(modalCloseEvent);
expect(window.location.hash).to.equal(prevHash);
document.body.querySelector('.dialog-modal').remove();
window.location.hash = prevHash;
});
});
});

describe('Download flow', () => {
Expand Down Expand Up @@ -668,6 +707,16 @@ describe('Merch Block', () => {
expect(action).to.be.undefined;
});

it('setCtaHash: sets authored hash', async () => {
const el = createCtaInMerchCard();
const hash = setCtaHash(el, { FREE_TRIAL_HASH: 'try-photoshop-authored' }, 'TRIAL');
expect(hash).to.equal('try-photoshop-authored');
});

it('setCtaHash: does nothing with invalid params', async () => {
expect(setCtaHash()).to.be.undefined;
});

const MODAL_URLS = [
{
url: 'https://www.adobe.com/mini-plans/illustrator1.html?mid=ft&web=1',
Expand Down
19 changes: 18 additions & 1 deletion test/blocks/merch/upgrade.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,23 @@ describe('Switch Modal (Upgrade Flow)', () => {
modal?.dispatchEvent(new Event('closeModal'));
});

it('updates the hash when modal opens, and restores it when modal closes', async () => {
const initialHash = window.location.hash;
const { handler } = await handleUpgradeOffer(
CTA_PRODUCT_FAMILY,
UPGRADE_OFFER,
ENTITLEMENTS,
CC_SINGLE_APPS_ALL,
CC_ALL_APPS,
);
window.location.hash = '#prev-hash';
await handler(new Event('click'), 'new-hash');
expect(window.location.hash).to.equal('#new-hash');
document.querySelector('.dialog-modal.upgrade-flow-modal').dispatchEvent(new Event('closeModal'));
expect(window.location.hash).to.equal('#prev-hash');
window.location.hash = initialHash;
});

it('should return an upgrade action for PROD', async () => {
const result = await handleUpgradeOffer(
CTA_PRODUCT_FAMILY,
Expand Down Expand Up @@ -98,7 +115,7 @@ describe('Switch Modal (Upgrade Flow)', () => {
expect(result).to.equal(undefined);
});

it('should return undefined if user is has one of upgrade targets already', async () => {
it('should return undefined if user has one of upgrade targets already', async () => {
const ENTITLEMENTS_WITH_UPGRADE_TARGET = [
{
offer: {
Expand Down
Loading