Skip to content
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
18 changes: 18 additions & 0 deletions src/server/v2/constants.js
Original file line number Diff line number Diff line change
@@ -1,2 +1,20 @@
export const VARIANT = 'B';
export const PORT = process.env.PORT || 8080;

export const FLEX_DEFAULTS = {
color: 'blue',
ratio: '1x1'
};

// Maps flex style.color to logo asset variant keys (see flexLogoMutations + v5 flex defaults).
export const FLEX_COLOR_TO_LOGO_TEXT_COLOR = {
blue: 'white',
black: 'white',
white: 'black',
'white-no-border': 'black',
gray: 'black',
grey: 'black',
monochrome: 'monochrome',
grayscale: 'grayscale',
greyscale: 'grayscale'
};
107 changes: 107 additions & 0 deletions src/server/v2/flex.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from 'preact';

import { buildContentLabel } from './utils/buildContentLabel';
import { renderBlock } from './utils/renderBlock';
import { getLogoBrandClass, resolveLogoAssets } from './logos';
import flexStyles from './flexStyles';
import { FLEX_COLOR_TO_LOGO_TEXT_COLOR, FLEX_DEFAULTS } from './constants';

function renderFlexLogo(logoBlock, flexColor) {
const textColor = FLEX_COLOR_TO_LOGO_TEXT_COLOR[flexColor] ?? 'white';
const brandClass = getLogoBrandClass({
logoName: logoBlock.name,
alternativeText: logoBlock.alternative_text
});
const logoClassName = ['pp-flex__logo', brandClass].filter(Boolean).join(' ');
const assets = resolveLogoAssets({
logoName: logoBlock.name,
effectiveLogoType: 'wordmark',
effectiveLogoPosition: 'left',
textColor
});

if (assets) {
return assets.map(({ src, dimensions: [width, height] }, idx) => (
// eslint-disable-next-line react/no-array-index-key
<span key={idx} className={logoClassName}>
<img src={src} alt="" role="presentation" width={width} height={height} />
</span>
));
}

return (
<span className={['pp-flex__logo', 'pp-flex__logo--fallback', brandClass].filter(Boolean).join(' ')}>
{renderBlock(logoBlock)}
</span>
);
}

export default function FlexMessage({ style, v2Content }) {
const color = style.color ?? FLEX_DEFAULTS.color;
const ratio = style.ratio ?? FLEX_DEFAULTS.ratio;

const mainItems = v2Content?.main_items ?? [];
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.

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

const mainLabel = buildContentLabel(logoBlock ? [logoBlock, ...mainBlocks] : mainBlocks);
const actionLabel = buildContentLabel(actionItems);

return (
<div
className={`pp-message pp-flex ${color} r-${ratio}`}
data-pp-style-layout="flex"
data-pp-style-color={color}
data-pp-style-ratio={ratio}
>
{/* eslint-disable react/no-danger */}
<style
dangerouslySetInnerHTML={{
__html: flexStyles({
fontFamily: style.text?.fontFamily,
fontSource: style.text?.fontSource,
ratio
})
}}
/>
{/* eslint-enable react/no-danger */}
<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

{renderFlexLogo(logoBlock, color)}
</div>
) : null}
<div className="pp-flex__messaging">
<div aria-label={mainLabel} className="pp-flex__main">
{mainBlocks.map((item, idx) => (
// eslint-disable-next-line react/no-array-index-key
<Fragment key={idx}>{renderBlock(item)}</Fragment>
))}
</div>
{actionItems.length > 0 ? (
<div aria-label={actionLabel} className="pp-flex__action">
{actionItems.map((item, idx) => (
// eslint-disable-next-line react/no-array-index-key
<Fragment key={idx}>{renderBlock(item)}</Fragment>
))}
</div>
) : null}
{disclaimerItems.length > 0 ? (
<div className="pp-flex__disclaimer">
{disclaimerItems.map((item, idx) => (
// eslint-disable-next-line react/no-array-index-key
<Fragment key={idx}>{renderBlock(item)}</Fragment>
))}
</div>
) : null}
</div>
</div>
</div>
);
}
Loading
Loading