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-164660: [3-in-1][Milo] Implement a link converter for CTAs using an iFrame #3564

Open
wants to merge 8 commits into
base: stage
Choose a base branch
from

Conversation

mirafedas
Copy link
Contributor

@mirafedas mirafedas commented Jan 28, 2025

To display the content of the 3-in-1 modal, we replace the iFrame link from the checkout-link.xlsx file with a link to the appropriate commerce page that includes the segmentation step for the given offer. This replacement occurs only if the link contains the modal=twp, modal=crm, or modal=d2p parameters.

In the threeInOne.js module, we handle the communication protocol to respond to events emitted by the commerce page rendered inside the iFrame, and render our custom loader.

A delay in loading the commerce page content is expected, as we cannot preload the page displayed within the iFrame (similar to the behavior of the upgrade modal).

Authoring:
For commerce CTAs (created in https://milo.adobe.com/tools/ost):

  1. Checkout link must have the workflowStep=segmentation and modal=true parameters,
    e.g. https://milo.adobe.com/tools/ost?osi=L2C9cKHNNDaFtBVB6GVsyNI88RlyimSlzVfkMM2gH4A&type=checkoutUrl&text=free-trial&workflowStep=segmentation&modal=true
  2. In the checkout-link.xlsx file all URLs that should be replaced with the new commerce URL should have the modal parameter set. The value should be the modal type: modal=twp, modal=crm, or modal=d2p.
    I added these values in the file and previewed it, but I have not published it yet.

Resolves: MWPW-164660

Test URLs:

@mirafedas mirafedas requested a review from a team as a code owner January 28, 2025 11:34
Copy link
Contributor

aem-code-sync bot commented Jan 28, 2025

Copy link

codecov bot commented Jan 28, 2025

Codecov Report

Attention: Patch coverage is 94.13333% with 22 lines in your changes missing coverage. Please review.

Project coverage is 96.51%. Comparing base (cf4c08b) to head (9eff2aa).

Files with missing lines Patch % Lines
libs/features/mas/src/checkout-mixin.js 63.26% 18 Missing ⚠️
libs/features/mas/src/buildCheckoutUrl.js 97.67% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##            stage    #3564      +/-   ##
==========================================
- Coverage   96.53%   96.51%   -0.03%     
==========================================
  Files         274      277       +3     
  Lines       61849    62209     +360     
==========================================
+ Hits        59705    60039     +334     
- Misses       2144     2170      +26     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@mirafedas mirafedas force-pushed the MWPW-164660-3-in-1-modal branch from 40e1011 to b4bf8e9 Compare January 28, 2025 12:37
loading: 'lazy',
class: 'loading',
});
const pCircle = createTag('sp-progress-circle', { label: 'progress circle', indeterminate: true, size: 'l' });
Copy link
Contributor

Choose a reason for hiding this comment

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

I think if the loading doesn't finish in certain amount of time, we should kill the spinner and show some kind of error message.
to be checked with Nick.

Copy link
Contributor

This pull request is not passing all required checks. Please see this discussion for information on how to get all checks passing. Inconsistent checks can be manually retried. If a test absolutely can not pass for a good reason, please add a comment with an explanation to the PR.

Copy link
Contributor

@3ch023 3ch023 left a comment

Choose a reason for hiding this comment

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

small request to remove checkout builder package from dependencies.
rest looks good to me

@@ -496,7 +497,13 @@ export async function openModal(e, url, offerType, hash, extraOptions) {
const fragmentPath = url.split(/(hlx|aem).(page|live)/).pop();
modal = await openFragmentModal(fragmentPath, getModal);
} else {
modal = await openExternalModal(url, getModal, extraOptions);
const isThreeInOneModal = [MODAL_TYPE_3_IN_1.CRM, MODAL_TYPE_3_IN_1.D2P, MODAL_TYPE_3_IN_1.TWP].includes(el?.getAttribute('data-modal-type')) && el?.href;
Copy link
Contributor

Choose a reason for hiding this comment

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

If we add additional modal types we would need to maintain the code in two places. Why not use Object.values to create an array from the values?

document.querySelector('.three-in-one iframe')?.classList?.remove('loading');
document.querySelector('.three-in-one sp-theme')?.remove();
break;
case MSG_SUBTYPE.EXTERNAL:
Copy link
Contributor

Choose a reason for hiding this comment

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

I see the same action being executed for MSG_SUBTYPE.EXTERNAL, MSG_SUBTYPE.SWITCH, MSG_SUBTYPE.RETURN_BACK. Maybe the code can be refactored a bit here.

@@ -496,7 +497,13 @@ export async function openModal(e, url, offerType, hash, extraOptions) {
const fragmentPath = url.split(/(hlx|aem).(page|live)/).pop();
modal = await openFragmentModal(fragmentPath, getModal);
} else {
modal = await openExternalModal(url, getModal, extraOptions);
const isThreeInOneModal = [MODAL_TYPE_3_IN_1.CRM, MODAL_TYPE_3_IN_1.D2P, MODAL_TYPE_3_IN_1.TWP].includes(el?.getAttribute('data-modal-type')) && el?.href;
Copy link
Contributor

Choose a reason for hiding this comment

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

The constants are part of merch, but the code looks like it could be any type of modal, without a direct connection to merch. We should either a) make this more explicit, b) make the modal types more general so they can be used outside of merch scope.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The 'trial with purchase' (twp), 'direct to purchase' (d2p), and 'content rich modal' are the only three types of modals we have on catalog and plans pages, and all three are directly connected to merch context, because they are opened by clicking the checkout CTAs. I'm not sure we should make is more general, because this logic is relevant only for these three types of modals.

@@ -359,6 +386,13 @@
width: 80%;
}

.dialog-modal.three-in-one {
Copy link
Contributor

Choose a reason for hiding this comment

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

I see .dialog-modal.upgrade-flow-modal has the exact properties, can we combine them somehow?

.three-in-one sp-theme {
width: max-content;
height: max-content;
left: 50%;
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this handle RTL accordingly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, corrected 👍

@narcis-radu narcis-radu dismissed their stale review February 3, 2025 17:37

changes have been implemented

@@ -489,14 +490,28 @@ export async function openModal(e, url, offerType, hash, extraOptions) {
const prevHash = window.location.hash.replace('#', '') === hash ? '' : window.location.hash;
window.location.hash = hash;
window.addEventListener('milo:modal:closed', () => {
window.history.pushState({}, document.title, `#${prevHash}`);
if (prevHash === '') {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not do something like

window.history.pushState({}, document.title, prevHash !== '' ? `#${prevHash}` : `${window.location.pathname}${window.location.search}`)

modal = await openExternalModal(url, getModal, extraOptions);
const isThreeInOneModal = Object.values(MODAL_TYPE_3_IN_1).includes(el?.getAttribute('data-modal-type')) && el?.href;
if (isThreeInOneModal) {
const { default: openThreeInOneModal } = await import('./threeInOne.js');
Copy link
Member

Choose a reason for hiding this comment

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

Camel case for file naming seems odd. Please instead use kebab case.

Copy link
Contributor

@yesil yesil left a comment

Choose a reason for hiding this comment

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

I wonder whether we should be using Milo modal to display the 3-in-1 or use SWC to render the modal from MAS directly.
Thus, the integration will be re-usable outside adobe.com.
@mirafedas I don't know how much work is this, especially given the timelines.
this is just to start the reflection on it.
cc: @npeltier @3ch023

@@ -0,0 +1,7 @@
export const MODAL_TYPE_3_IN_1 = {
Copy link
Contributor

Choose a reason for hiding this comment

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

This constant is duplicated at https://github.com/adobecom/milo/pull/3564/files#diff-e91eb0b5a655ea856ec40025c1071fb88ac9908264c11f760b6208130e1099f0R106.

Wherever it is used, it should import it from mas feature.

Copy link
Contributor

Choose a reason for hiding this comment

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

to avoid confusion, I recommend to remove this file.

}, { once: true });
}
if (isInternalModal(url)) {
const fragmentPath = url.split(/(hlx|aem).(page|live)/).pop();
modal = await openFragmentModal(fragmentPath, getModal);
} else {
modal = await openExternalModal(url, getModal, extraOptions);
const isThreeInOneModal = Object.values(MODAL_TYPE_3_IN_1).includes(el?.getAttribute('data-modal-type')) && el?.href;
Copy link
Contributor

Choose a reason for hiding this comment

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

this logic could be a helper function in MAS checkout-mixin some related files.


export async function createContent(iframeUrl, modalType) {
const { base } = getConfig();
await Promise.all([
Copy link
Contributor

Choose a reason for hiding this comment

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

these imports can go to top of the file.
this is blocking the loading for no reason IMO.

const pCircle = createTag('sp-progress-circle', { label: 'progress circle', indeterminate: true, size: 'l' });
const theme = createTag('sp-theme', { theme: 'spectrum', color: 'light', scale: 'medium', dir: 'ltr' });
theme.append(pCircle);
content.append(theme);
Copy link
Contributor

Choose a reason for hiding this comment

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

dark theme support?
also since the markup being generated static, you could also do:

content.innerHTML = '<sp-theme ....>...</sp-theme>';

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 rewrote it to use .innerHTML, and regarding the dark theme - if support is needed, I can work on it in the scope of the follow-up tickets, because this has to be aligned with the commerce team, in the current task it is not mentioned, and it would be good to avoid adding more changes to this PR to get it merged before the RCP

const modalType = el?.getAttribute('data-modal-type');
if (!modalType || !iframeUrl) return undefined;

window.addEventListener('message', handle3in1IFrameEvents);
Copy link
Contributor

Choose a reason for hiding this comment

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

might we be registering more events that intended if the user closes / re-opens the modal?

I would register the even handler in the module scope, and ignore the messages if the modal is not open.

padding-bottom: 0;
height: 100%;
}

.dialog-modal.commerce-frame.height-fit-content, .dialog-modal.dynamic-height.height-fit-content {
.dialog-modal.commerce-frame.height-fit-content,
.dialog-modal.dynamic-height.height-fit-content {
Copy link
Contributor

Choose a reason for hiding this comment

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

if this is only code formatting, I would omit these changes from your PR.

return modalParam;
}
} catch (error) {
// do nothing
Copy link
Contributor

Choose a reason for hiding this comment

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

log using this.masElement.log

@@ -1,9 +1,4 @@
// This file aliases and re-exports commonly used external dependencies
import {
Copy link
Contributor

Choose a reason for hiding this comment

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

❤️

@mirafedas
Copy link
Contributor Author

@yesil thank you for reviewing, I addressed all comments. Regarding the comment about rendering the modal as SWC directly from MAS - I would wait for the commerce team to develop their web component (as they mentioned at the meeting)

Comment on lines +63 to +66
content.innerHTML = `<sp-theme system="light" color="light" scale="medium" dir="ltr">
<sp-progress-circle label="progress circle" indeterminate="" size="l" dir="ltr" role="progressbar" aria-label="progress circle"></sp-progress-circle>
</sp-theme>
<iframe src="${iframeUrl}" title="${modalType === MODAL_TYPE_3_IN_1.CRM ? 'Single App' : modalType}" frameborder="0" marginwidth="0" marginheight="0" allowfullscreen="true" loading="lazy" class="loading" style="height: 100%;"></iframe>`;

Check warning

Code scanning / CodeQL

DOM text reinterpreted as HTML Medium

DOM text
is reinterpreted as HTML without escaping meta-characters.
@@ -38,6 +38,19 @@ await build({
outfile: `${outfolder}/mas.js`,
});

// constants.js
await build({
Copy link
Member

Choose a reason for hiding this comment

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

why not part of mas.js tho? @mirafedas @npeltier @3ch023 @yesil

Copy link
Member

@Axelcureno Axelcureno left a comment

Choose a reason for hiding this comment

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

LGTM, just one question and one recommendation, both trivial.

} catch (error) {
this.masElement.log?.error('Failed to add 3-in-1 modal parameters', error);
}
}
Copy link
Member

Choose a reason for hiding this comment

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

Should we add a catch() here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is already a catch statement here :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants