Skip to content

feat: add flex layout support to renderV2Message (DTCRCMERC-5374) - #1367

Merged
surrsurus merged 17 commits into
developfrom
feature/DTCRCMERC-5374
Jul 31, 2026
Merged

feat: add flex layout support to renderV2Message (DTCRCMERC-5374)#1367
surrsurus merged 17 commits into
developfrom
feature/DTCRCMERC-5374

Conversation

@surrsurus

@surrsurus surrsurus commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Description

This PR is an attempt to cherry pick the flex changes from Laya's PR onto the latest develop.

Original Description

Adds flex layout support to renderV2Message (DTCRCMERC-5374), following the same static CSS/class-based approach used by the text layout renderer. The flex renderer does not depend on getMutations, applyCascade, locale/product mutation files, or any v5 mutation cascade logic.

Changes:

  • src/server/v2/message.jsx — adds FlexMessage component that extracts logo from main_items and renders pp-flex__logo-container, pp-flex__messaging, pp-flex__main, pp-flex__action, and pp-flex__disclaimer sections directly from CPS v2 content blocks
  • src/server/v2/styles.js — adds flexStyles() covering all 7 color themes (blue, black, white, white-no-border, gray, monochrome, grayscale) and all 4 ratio layouts (1x1, 1x4, 8x1, 20x1) with responsive media queries; color and ratio are applied via static CSS class selectors, not dynamic injection
  • src/server/v2/getParentStyles.js — existing ratioMap already handles flex ratio outer iframe sizing; no changes needed
  • src/server/message/font.js — adds shared buildFontRules utility used by v2 stylesheets (text and flex)
  • src/server/v2/utils/buildLogoConfiguration.js — updates to accept logoType and correctly handle inline logo extraction
  • Tests and snapshots for all new flex behavior

Color/ratio values are normalized before reaching renderV2Message by the existing validateStyle logic (greygray, greyscalegrayscale).

Author's Note

This fork was fairly out of date so this PR captures the additional effort to address rendering issues that popped due to all of the differences. The existing font and logo logic was broken when I put it on the latest develop and I also tried cleaning up the flex styles CSS & ratio logic. The files changed as a result look completely different.

Screenshots / Videos

I'm using the v5 test harness to render these messages out

1x1 1x4 8x1 20x1
image image image image

Testing instructions

Stage Tag

Test with tag v5_rewrite_77046c1

v5 Harness

  • Start CPS locally
  • Point CPNW towards your local CPS:
        "@paypalcorp/creditpresentmentserv": {
            "hostname": "localhost.paypal.com",
            "port": 15606,
            "rejectUnauthorized": false
        }
  • Point CPNW at your local bundle (https://localhost.paypal.com:8080) in getMessagingBundle
  • Start CPNW, ClientSDKNodeWeb, and PMC
  • Using the v5 rewrite test, create a bunch of flex messages

Test page

node scripts/preview-v2-render.js apparently also can test this but I will be honest - I have no clue how to get this to work

Review

The expected SLA for reviews in this repo is days.

All pull requests require an initial review from your team followed by code owner approval.

While initial review is ongoing, please add the following label to your PR Needs initial review. This initial review process should ensure that:

  • Code follows team standards and best practices
  • Changes are thoroughly tested and verified
  • The PR template is filled out
  • Documentation is complete and accurate
  • The PR is ready for official code owner review

Once you have received initial approval, please do the following:

  • Remove the label Needs initial review
  • Add the label Needs codeowner review

Code owners will then review the PR in accordance with our SLA. This process helps maintain code quality and reduces review cycles.

@jeremy-herman jeremy-herman left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Some minor comments from the code review. Will do some testing next!

Comment thread src/server/v2/flexStyles.js Outdated
Comment thread src/server/v2/utils/renderBlock.js Outdated
Comment thread src/server/v2/utils/renderBlock.js Outdated

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These tests were pretty flaky so this is my attempt to fix that. Was noticing that the iframe existed before document.body was ready, this should help smooth that out in CI

Comment thread src/server/v2/flexStyles.js

@Braluna-pp Braluna-pp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved intent-aware PR review findings: F1, F2, F3, F4.

Comment thread src/server/v2/flex.jsx Outdated
if (assets) {
return assets.map(({ src, dimensions: [width, height] }, idx) => (
// eslint-disable-next-line react/no-array-index-key
<span key={idx} className="pp-flex__logo">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Finding Description

[P1] Size single-piece brand logos as full lockups

The renderer gives every resolved asset the same pp-flex__logo class, but the ratio CSS treats the first child as a small PayPal monogram and the second as the wider wordmark. PayPal Credit and Venmo each resolve to one combined lockup, so that full logo lands in the monogram slot. In 1x1 it is constrained to 29px wide, and in 1x4 to 27px. The PayPal Credit lockup is therefore only about 4px tall.

Problematic Code Snippet(s)

return assets.map(({ src, dimensions: [width, height] }, idx) => (
    <span key={idx} className="pp-flex__logo">
        <img src={src} alt="" role="presentation" width={width} height={height} />
    </span>
));

Suggested Change(s)

  • Implementation: Add brand or asset-shape classes and size single-piece PayPal Credit and Venmo lockups as whole logos instead of by PayPal asset position.
  • Test: Cover PayPal Credit, Venmo, and fallback images in portrait and landscape flex ratios, asserting the selected class and rendered dimensions.

Relevant Links

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Makes sense, addressed in c66135d

return linkClassName ? <span className={linkClassName}>{item.text}</span> : <span>{item.text}</span>;
case 'TEXT':
return item.brand ? <strong>{item.text}</strong> : item.text;
default:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Finding Description

[P1] Render supported TEXT_VARIABLE blocks

The renderer handles IMAGE, LINK, and TEXT, then returns item.text for every other type. The repository's supported TEXT_VARIABLE fixture contains name: 'installment_amount' and no text, so this branch returns undefined. Flex sends every non-image block through this helper, causing valid variable content to disappear from both the visible message and its accessible label.

Problematic Code Snippet(s)

case 'TEXT':
    return item.brand ? <strong>{item.text}</strong> : item.text;
default:
    return item.text;

Suggested Change(s)

  • Implementation: Implement the CPS TEXT_VARIABLE contract at this boundary, passing any required resolution context instead of silently returning an absent text field.
  • Test: Render a resolved variable between two text blocks and assert its visible position and inclusion in the accessible label. Add a defined unresolved-variable failure case.

Relevant Links

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Need to push back on this one a bit - CPS/UCG already merges TEXT_VARIABLETEXT unless the client opts into TEXT_VARIABLES_*. CPNW does not request those features, and adaptCpsPresentmentResponse rejects TEXT_VARIABLE entirely. The cited renderer switch is PMCs deferred-v6 soft-fallback, not a CPNW contract gap. Can go into more detail if you'd like more clarification on this.

Comment thread src/server/v2/flex.jsx
const actionItems = v2Content?.action_items ?? [];
const disclaimerItems = v2Content?.disclaimer_items ?? [];

const logoBlock = mainItems.find(item => item.type === 'IMAGE');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Finding Description

[P1] Preserve ordered image blocks

The flex renderer finds one image, filters every image out of main_items, and renders the selected image before the messaging container. A valid TEXT, IMAGE, TEXT sequence therefore becomes IMAGE, TEXT, TEXT, and any second image is removed completely. This breaks the ordered typed-block contract instead of rendering CPS content directly.

Problematic Code Snippet(s)

const logoBlock = mainItems.find(item => item.type === 'IMAGE');
const mainBlocks = mainItems.filter(item => item.type !== 'IMAGE');

Suggested Change(s)

  • Implementation: Preserve the ordered block stream. Extract only a contract-defined standalone logo, or render images in place and use layout classes for visual placement.
  • Test: Assert DOM order for TEXT, IMAGE, TEXT and define explicit behavior for multiple images so no valid block is silently discarded.

Relevant Links

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The find/filter behavior is intentional flex presentation, not a contract violation. Flex (like v5 and text left/top/right) extracts the brand logo into a dedicated logo container and renders the remaining blocks as messaging. Order-preserving in-stream IMAGE rendering is the inline path (buildLogoConfiguration + text layout), which has an explicit TEXT, IMAGE, TEXT test. Flex has no logo.position option, and CPNW does not request INLINE_LOGO for flex - so the mid-stream TEXT, IMAGE, TEXT shape the finding assumes is not the flex content contract.

CPS samples for this path use a single logo IMAGE. Dropping additional IMAGE blocks is theoretical vs real flex traffic, not evidence that flex must render an ordered multi-image stream.

Comment thread src/server/v2/flex.jsx
<div className="pp-flex__background" />
<div className="pp-flex__content">
{logoBlock ? (
<div className="pp-flex__logo-container" aria-hidden="true">

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Finding Description

[P1] Keep the logo alternative text accessible

mainLabel is built only from mainBlocks after the image has been removed, and the separate logo container is aria-hidden. For the normal leading PayPal image, its alternative_text is therefore absent from all flex accessible output even though the text renderer exposes the same brand label. A message such as PayPal Pay Later is announced only as Pay Later.

Problematic Code Snippet(s)

const mainLabel = buildContentLabel(mainBlocks);

<div className="pp-flex__logo-container" aria-hidden="true">
    {renderFlexLogo(logoBlock, color)}
</div>

Suggested Change(s)

  • Implementation: Expose the logo's alternative_text once, either through the main label in source order or through a labeled logo container with decorative child images.
  • Test: Assert the full accessible name for a standard leading logo and for an unknown image with custom alternative text.

Relevant Links

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

72e7634 Fixed here, nice catch

Comment thread src/server/v2/utils/renderBlock.js Outdated
className={`logo inline wordmark ${getLogoBrandClass({
logoName: item.name,
alternativeText: item.alternative_text
})}`.trim()}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nit: could we make the optional brand class explicit instead of relying on .trim() to remove the trailing space? Pulling it into a variable and joining the truthy classes makes this easier to scan:

const brandClass = getLogoBrandClass({
    logoName: item.name,
    alternativeText: item.alternative_text
});
const className = ['logo', 'inline', 'wordmark', brandClass].filter(Boolean).join(' ');

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed, this I believe might have been some copypasta - fixed it here 73cac51 but then noticed we use this pattern in multiple locations and addressed it 32d5840


const banner = iframeBody.querySelector(bannerSelectors.container);
return (
banner?.clientHeight > 0 && {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could we make this readiness check include the things that affect the screenshot instead of relying on the fixed 500 ms sleep below? A positive size proves the body exists, but fonts or images can still finish afterward and change the layout. Waiting inside the iframe for document.fonts.ready, incomplete images, and stable dimensions across two animation frames would remove the remaining timing guess.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

5dfcb44 - will probably need to iterate on this but got rid of the timeout

@surrsurus
surrsurus requested a review from Braluna-pp July 31, 2026 17:12

@Braluna-pp Braluna-pp left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks for addressing the feedback, after ci passes you should be good to go

@surrsurus
surrsurus merged commit cdaa69f into develop Jul 31, 2026
76 checks passed
@surrsurus
surrsurus deleted the feature/DTCRCMERC-5374 branch July 31, 2026 17:31
github-actions Bot pushed a commit that referenced this pull request Aug 3, 2026
## [1.93.0](v1.92.1...v1.93.0) (2026-08-03)

### Features

* add flex layout support to renderV2Message (DTCRCMERC-5374) ([#1367](#1367)) ([cdaa69f](cdaa69f))

### Bug Fixes

* Ios26 modal fix ([#1373](#1373)) ([263ab0f](263ab0f))
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown

🎉 This PR is included in version 1.93.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants