Skip to content
Open
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
22 changes: 22 additions & 0 deletions src/server/message/font.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,3 +90,25 @@ export const getFontRules = style => {
}
return rules;
};

// Shared validation and @font-face generation for v2 stylesheet.
const isSafeFontName = value => typeof value === 'string' && /^[\w\s-]+$/.test(value.trim());
const isSafeFontSource = value => typeof value === 'string' && /^https:\/\/[^'")\s]+$/i.test(value);

export function buildFontRules({ fontSource, fontFamily, fallbackStack, defaultFontFamily, fontNamePrefix }) {
const sources = Array.isArray(fontSource) ? fontSource.filter(isSafeFontSource) : [];
const families = Array.isArray(fontFamily) ? fontFamily.filter(isSafeFontName).map(formatFontFamilyName) : [];

const fontFaceRules = sources
.map((url, i) => `@font-face { font-family: '${fontNamePrefix} ${i + 1}'; src: url('${url}'); }`)
.join('\n');

const customSourceNames = sources.map((_, i) => `'${fontNamePrefix} ${i + 1}'`);

const effectiveFontFamily =
customSourceNames.length > 0 || families.length > 0
? [...customSourceNames, ...families, fallbackStack].join(', ')
: defaultFontFamily;

return { fontFaceRules, effectiveFontFamily };
}
36 changes: 24 additions & 12 deletions src/server/v2/message.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,27 @@ function renderBlock(item) {
}
}

function renderLogo(block, className) {
if (!block) return null;
return (
<span role="img" aria-label={block.alternative_text || 'PayPal'} className={className}>
{renderBlock(block)}
</span>
);
}

export default function V2Message({ options, v2Content }) {
const { style } = options;
const logoPosition = style.logo?.type === 'inline' ? 'inline' : style.logo?.position ?? 'left';
const logoType = style.logo?.type ?? 'primary';
const logoPosition = style.logo?.position ?? 'left';
const textColor = style.layout === 'flex' ? style.color ?? 'black' : style.text?.color ?? 'black';

const mainItems = v2Content?.main_items ?? [];
const actionItems = v2Content?.action_items ?? [];
const disclaimerItems = v2Content?.disclaimer_items ?? [];

const { logoBlock, hasInitialLogo, hasRightLogo, mainBlocks } = buildLogoConfiguration({
logoType,
logoPosition,
mainItems
});
Expand All @@ -53,24 +63,30 @@ export default function V2Message({ options, v2Content }) {
const actionLabel = buildContentLabel(actionItems);

return (
<div className="pp-message">
<div
className="pp-message"
data-pp-style-layout={style.layout}
data-pp-style-logo-position={logoPosition}
Comment thread
Braluna-pp marked this conversation as resolved.
data-pp-style-logo-type={logoType}
data-pp-style-text-align={style.text?.align}
data-pp-style-text-color={textColor}
data-pp-style-text-size={style.text?.size}
>
{/* eslint-disable react/no-danger */}
<style
dangerouslySetInnerHTML={{
__html: styles({
fontFamily: style.text?.fontFamily,
fontSource: style.text?.fontSource,
fontSize: style.text?.size,
textAlign: style.text?.align
})
}}
/>
{/* eslint-enable react/no-danger */}
{hasInitialLogo && logoBlock && logoType !== 'none' ? (
<span role="img" aria-label={logoBlock.alternative_text || 'PayPal'} className={logoClasses}>
{renderBlock(logoBlock)}
</span>
) : null}
{hasInitialLogo && logoType !== 'none' ? renderLogo(logoBlock, logoClasses) : null}
<span aria-label={mainLabel} className={mainClasses}>
{logoType === 'inline' ? renderLogo(logoBlock, logoClasses) : null}
{preparedMainBlocks.map((item, idx) => (
// eslint-disable-next-line react/no-array-index-key
<Fragment key={idx}>{renderBlock(item)}</Fragment>
Expand All @@ -84,11 +100,7 @@ export default function V2Message({ options, v2Content }) {
))}
</span>
) : null}
{hasRightLogo && logoBlock && logoType !== 'none' ? (
<span role="img" aria-label={logoBlock.alternative_text || 'PayPal'} className={logoClasses}>
{renderBlock(logoBlock)}
</span>
) : null}
{hasRightLogo && logoType !== 'none' ? renderLogo(logoBlock, logoClasses) : null}
</div>
);
}
51 changes: 43 additions & 8 deletions src/server/v2/styles.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
export default ({
fontFamily = '"PayPal Pro", Helvetica, Arial, "Liberation Sans", sans-serif',
fontSize = 12,
textAlign = 'left'
} = {}) => `
import { buildFontRules } from '../message/font';

const DEFAULT_FONT_FAMILY = '"PayPal Pro", Helvetica, Arial, "Liberation Sans", sans-serif';
const FONT_FALLBACKS = 'Helvetica, Arial, "Liberation Sans", sans-serif';

export default ({ fontFamily, fontSource, fontSize = 12, textAlign = 'left' } = {}) => {
const { fontFaceRules, effectiveFontFamily } = buildFontRules({
fontSource,
fontFamily,
fallbackStack: FONT_FALLBACKS,
defaultFontFamily: DEFAULT_FONT_FAMILY,
fontNamePrefix: 'PP Merchant Font'
});

return `${fontFaceRules ? `${fontFaceRules}\n` : ''}
body {
margin: 0;
padding: 0;
}

.pp-message {
font-family: ${fontFamily};
display: block;
width: 100%;
font-family: ${effectiveFontFamily};
font-weight: 450;
font-size: ${fontSize}px;
text-align: ${textAlign};
}

.pp-message .main { vertical-align: middle; }
.pp-message .main.black { color: #000; }
.pp-message .main.monochrome { color: #000; }
.pp-message .main.grayscale { color: #000; }
Expand All @@ -19,12 +37,16 @@ export default ({
color: #0070ba;
white-space: nowrap;
}
.pp-message .action {
margin-left: 0.25em;
vertical-align: middle;
}
.pp-message .action.monochrome > [data-iframe-url] { color: #000; }
.pp-message .action.grayscale > [data-iframe-url] { color: #000; }
.pp-message .action.white > [data-iframe-url] { color: #fff; }

.pp-message .logo { display: inline-block; }
.pp-message .logo.top { display: block; }
.pp-message .logo { display: inline-block; vertical-align: middle; }
.pp-message .logo.top { display: block; vertical-align: initial; }

.pp-message img {
max-height: 1.25em;
Expand All @@ -33,4 +55,17 @@ export default ({
vertical-align: middle;
margin-right: 0.3125em;
}
.pp-message .logo.right img {
margin-right: 0;
margin-left: 0.3125em;
}
.pp-message .logo.top img {
margin-right: 0;
margin-bottom: 0.3125em;
}

.pp-message .logo.white img { filter: brightness(0) invert(1); }
.pp-message .logo.monochrome img { filter: grayscale(100%) brightness(0); }
.pp-message .logo.grayscale img { filter: grayscale(100%); }
`;
};
10 changes: 5 additions & 5 deletions src/server/v2/utils/buildLogoConfiguration.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
export function buildLogoConfiguration({ logoPosition, mainItems }) {
if (logoPosition === 'inline') {
return { hasInitialLogo: false, hasRightLogo: false, logoBlock: undefined, mainBlocks: mainItems };
}

export function buildLogoConfiguration({ logoType, logoPosition, mainItems }) {
const logoBlock = mainItems.find(item => item.type === 'IMAGE');
const mainBlocks = mainItems.filter(item => item.type !== 'IMAGE');

if (logoType === 'inline') {
return { hasInitialLogo: false, hasRightLogo: false, logoBlock, mainBlocks };
}

return {
hasInitialLogo: !!logoBlock && (logoPosition === 'left' || logoPosition === 'top'),
hasRightLogo: !!logoBlock && logoPosition === 'right',
Expand Down
119 changes: 119 additions & 0 deletions tests/unit/spec/server/v2/__snapshots__/render.test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`v2 render snapshots full render snapshot for representative case 1`] = `
"<div class=\\"pp-message\\" data-pp-style-layout=\\"text\\" data-pp-style-logo-position=\\"left\\" data-pp-style-logo-type=\\"primary\\" data-pp-style-text-align=\\"left\\" data-pp-style-text-color=\\"black\\" data-pp-style-text-size=\\"12\\"><style>
body {
margin: 0;
padding: 0;
}

.pp-message {
display: block;
width: 100%;
font-family: \\"PayPal Pro\\", Helvetica, Arial, \\"Liberation Sans\\", sans-serif;
font-weight: 450;
font-size: 12px;
text-align: left;
}

.pp-message .main { vertical-align: middle; }
.pp-message .main.black { color: #000; }
.pp-message .main.monochrome { color: #000; }
.pp-message .main.grayscale { color: #000; }
.pp-message .main.white { color: #fff; }

.pp-message .action [data-iframe-url] {
color: #0070ba;
white-space: nowrap;
}
.pp-message .action {
margin-left: 0.25em;
vertical-align: middle;
}
.pp-message .action.monochrome > [data-iframe-url] { color: #000; }
.pp-message .action.grayscale > [data-iframe-url] { color: #000; }
.pp-message .action.white > [data-iframe-url] { color: #fff; }

.pp-message .logo { display: inline-block; vertical-align: middle; }
.pp-message .logo.top { display: block; vertical-align: initial; }

.pp-message img {
max-height: 1.25em;
height: 1.25em;
width: auto;
vertical-align: middle;
margin-right: 0.3125em;
}
.pp-message .logo.right img {
margin-right: 0;
margin-left: 0.3125em;
}
.pp-message .logo.top img {
margin-right: 0;
margin-bottom: 0.3125em;
}

.pp-message .logo.white img { filter: brightness(0) invert(1); }
.pp-message .logo.monochrome img { filter: grayscale(100%) brightness(0); }
.pp-message .logo.grayscale img { filter: grayscale(100%); }
</style><span role=\\"img\\" aria-label=\\"PayPal\\" class=\\"logo black left primary\\"><img src=\\"https://example.com/logo.svg\\" alt=\\"PayPal\\" /></span><span aria-label=\\"Pay Later. Subject to approval.\\" class=\\"main left black\\">Pay Later. Subject to approval.</span><span aria-label=\\"Learn more\\" class=\\"action black\\"><span data-iframe-url=\\"https://example.com/lander\\" data-embeddable=\\"true\\">Learn more</span></span></div>"
`;

exports[`v2 render snapshots renders the v2 stylesheet once 1`] = `
"<style>
body {
margin: 0;
padding: 0;
}

.pp-message {
display: block;
width: 100%;
font-family: \\"PayPal Pro\\", Helvetica, Arial, \\"Liberation Sans\\", sans-serif;
font-weight: 450;
font-size: 12px;
text-align: left;
}

.pp-message .main { vertical-align: middle; }
.pp-message .main.black { color: #000; }
.pp-message .main.monochrome { color: #000; }
.pp-message .main.grayscale { color: #000; }
.pp-message .main.white { color: #fff; }

.pp-message .action [data-iframe-url] {
color: #0070ba;
white-space: nowrap;
}
.pp-message .action {
margin-left: 0.25em;
vertical-align: middle;
}
.pp-message .action.monochrome > [data-iframe-url] { color: #000; }
.pp-message .action.grayscale > [data-iframe-url] { color: #000; }
.pp-message .action.white > [data-iframe-url] { color: #fff; }

.pp-message .logo { display: inline-block; vertical-align: middle; }
.pp-message .logo.top { display: block; vertical-align: initial; }

.pp-message img {
max-height: 1.25em;
height: 1.25em;
width: auto;
vertical-align: middle;
margin-right: 0.3125em;
}
.pp-message .logo.right img {
margin-right: 0;
margin-left: 0.3125em;
}
.pp-message .logo.top img {
margin-right: 0;
margin-bottom: 0.3125em;
}

.pp-message .logo.white img { filter: brightness(0) invert(1); }
.pp-message .logo.monochrome img { filter: grayscale(100%) brightness(0); }
.pp-message .logo.grayscale img { filter: grayscale(100%); }
</style>"
`;
Loading
Loading