paypal-checkout
Version:
PayPal Checkout components, for integrating checkout products.
553 lines (440 loc) • 20.6 kB
JSX
// eslint-disable-line max-lines
/* @flow */
/** @jsx jsxToHTML */
import { base64encode } from 'belter/src';
import { BUTTON_SIZE, BUTTON_BRANDING, BUTTON_NUMBER, BUTTON_LOGO_COLOR, BUTTON_LABEL, BUTTON_LAYOUT, ENV, ATTRIBUTE, FUNDING, FUNDING_BRAND_LABEL } from '../../constants';
import { getButtonConfig, labelToFunding, fundingToDefaultLabel, BUTTON_CONFIG } from '../config';
import { normalizeProps } from '../props';
import { jsxToHTML, type JsxHTMLNode, type ChildType, jsxRender } from '../../lib/jsx';
import { fundingLogos, cardLogos } from '../../resources';
import { validateButtonProps } from '../validate';
import type { LocaleType, FundingSource, FundingSelection, FundingList, CheckoutCustomizationType } from '../../types';
import { Tagline, Beacon } from './miscComponent';
import { componentStyle, CLASS } from './componentStyle';
import { getComponentScript } from './componentScript';
import { componentContent } from './content';
const allowedPersonalizationLabels = [ BUTTON_LABEL.CHECKOUT, BUTTON_LABEL.BUYNOW, BUTTON_LABEL.PAY ];
function getCommonButtonClasses({ layout, shape, branding, multiple, env }) : string {
return [
`${ CLASS.LAYOUT }-${ layout }`,
`${ CLASS.SHAPE }-${ shape }`,
`${ CLASS.BRANDING }-${ branding ? BUTTON_BRANDING.BRANDED : BUTTON_BRANDING.UNBRANDED }`,
`${ CLASS.NUMBER }-${ multiple ? BUTTON_NUMBER.MULTIPLE : BUTTON_NUMBER.SINGLE }`,
`${ CLASS.ENV }-${ env }`,
`${ CLASS.SHOULD_FOCUS }`
].join(' ');
}
function getButtonClasses({ label, color, logoColor }) : string {
return [
`${ CLASS.LABEL }-${ label }`,
`${ CLASS.COLOR }-${ color }`,
`${ CLASS.LOGO_COLOR }-${ logoColor }`
].join(' ');
}
function getLocaleContent(locale : LocaleType) : Object {
const { country, lang } = locale;
return componentContent[country][lang];
}
function determineLabel({ label, source, multiple, layout } : { label : $Values<typeof BUTTON_LABEL>, source : FundingSource, multiple : boolean, layout : $Values<typeof BUTTON_LAYOUT> }) : $Values<typeof BUTTON_LABEL> {
const defaultLabel = fundingToDefaultLabel(source);
const labelMatchesFunding = (labelToFunding(label) === source);
// If chosen label is not for this funding source, display the default label
if (!labelMatchesFunding) {
return defaultLabel;
}
// If there are multiple horizontal buttons, display the default label
if (multiple && layout === BUTTON_LAYOUT.HORIZONTAL) {
return defaultLabel;
}
return label;
}
function determineButtons({ label, color, sources, multiple, layout } : { label : $Values<typeof BUTTON_LABEL>, color : string, sources : FundingList, multiple : boolean, layout : $Values<typeof BUTTON_LAYOUT> }) :
$ReadOnlyArray<{ label : $Values<typeof BUTTON_LABEL>, color : string, source : FundingSource }> {
return sources.map((source, i) => {
const buttonLabel = determineLabel({ label, source, multiple, layout });
const buttonColor = (multiple && i > 0)
? getButtonConfig(buttonLabel, 'secondaryColors')[color]
: color;
return {
source,
label: buttonLabel,
color: buttonColor
};
});
}
function renderCards({ cards, layout, size } :
{ cards : $ReadOnlyArray<string>, layout? : string, size? : string }) : $ReadOnlyArray<JsxHTMLNode> {
return cards.map(name => {
const logo = cardLogos[name];
return (
<div
{ ...{ [ATTRIBUTE.LAYOUT]: layout ? layout : '' } }
{ ...{ [ATTRIBUTE.SIZE]: size ? size : '' } }
{ ...{ [ATTRIBUTE.FUNDING_SOURCE]: `${ FUNDING.CARD }`, [ATTRIBUTE.CARD]: `${ name }` } }
class={ `${ CLASS.CARD } ${ CLASS.CARD }-${ name }` }
tabindex='0'>
<img
style={ ` display: block; ` }
src={ `data:image/svg+xml;base64,${ base64encode(logo.toString()) }` }
alt={ name } />
</div>
);
});
}
function renderFundingIcons({ cards, fundingicons, size, layout } :
{ cards : $ReadOnlyArray<string>, fundingicons : boolean, layout : string, size : string }) : ?JsxHTMLNode {
if (!fundingicons) {
return;
}
return <div class={ `${ CLASS.FUNDINGICONS }` }>{ renderCards({ cards, size, layout }) }</div>;
}
function renderPersonalizationButtonText(text) : JsxHTMLNode {
const className = `${ CLASS.TEXT } ${ CLASS.PERSONALIZATION_TEXT }`;
return <span class={ className } optional="2">{ text }</span>;
}
function getButtonTextAnimationStyle({ personalizedButtonText, branding, allowedAnimation }) : ?JsxHTMLNode {
if (__TEST__) {
return null;
}
if (!branding) {
return;
}
if (!allowedAnimation) {
return;
}
const MIN_WIDTH = 300;
const LABEL_DURATION = 1;
const PERSONALIZATION_DURATION = 5;
const DELAY = 0;
const COMPRESSED = `
max-width: 0%;
opacity: 0;
`;
const EXPANDED = `
max-width: 100%;
opacity: 1;
`;
const HIDDEN = `
position: absolute;
visibility: hidden;
`;
const VISIBLE = `
position: static;
visibility: visible;
`;
const DOM_READY = '.dom-ready';
const PAYPAL_BUTTON = `.${ CLASS.BUTTON }[${ ATTRIBUTE.FUNDING_SOURCE }=${ FUNDING.PAYPAL }]`;
const PAYPAL_LOGO = `${ PAYPAL_BUTTON } .${ CLASS.LOGO }.${ CLASS.LOGO }-${ FUNDING.PAYPAL }`;
const BUTTON_TEXT = `${ PAYPAL_BUTTON } .${ CLASS.TEXT }:not(.personalization-text)`;
const PERSONALIZATION_TEXT = `${ PAYPAL_BUTTON } .personalization-text`;
return (
<style innerHTML={ `
${ BUTTON_TEXT }, ${ PERSONALIZATION_TEXT } {
${ HIDDEN }
}
${ DOM_READY } ${ BUTTON_TEXT }:not(.${ CLASS.HIDDEN }) {
${ VISIBLE }
${ COMPRESSED }
animation: show-text ${ LABEL_DURATION }s ${ DELAY }s forwards;
}
@media only screen and (max-width: ${ MIN_WIDTH }px) {
${ DOM_READY } ${ PERSONALIZATION_TEXT } {
${ HIDDEN }
}
}
@media only screen and (min-width: ${ MIN_WIDTH }px) {
${ DOM_READY } ${ PAYPAL_LOGO } {
animation: ${ personalizedButtonText ? `toggle-paypal-logo ${ PERSONALIZATION_DURATION }s ${ DELAY }s forwards` : `none` };
}
${ DOM_READY } ${ BUTTON_TEXT }:not(.${ CLASS.HIDDEN }) {
${ COMPRESSED }
${ VISIBLE }
animation: ${ personalizedButtonText ? `show-text-delayed ${ PERSONALIZATION_DURATION }s ${ DELAY }s forwards` : `show-text ${ LABEL_DURATION }s ${ DELAY }s forwards` };
}
${ DOM_READY } ${ PERSONALIZATION_TEXT } {
${ COMPRESSED }
${ VISIBLE }
animation: show-personalization-text ${ PERSONALIZATION_DURATION }s ${ DELAY }s forwards;
}
}
@keyframes show-text {
0% { ${ COMPRESSED } }
100% { ${ EXPANDED } }
}
@keyframes toggle-paypal-logo {
0% { ${ EXPANDED } }
8% { ${ COMPRESSED } }
85% { ${ COMPRESSED } }
100% { ${ EXPANDED } }
}
@keyframes show-text-delayed {
0% { ${ COMPRESSED } }
85% { ${ COMPRESSED } }
100% { ${ EXPANDED } }
}
@keyframes show-personalization-text {
0% { ${ COMPRESSED } }
25% { ${ EXPANDED } }
75% { ${ EXPANDED } }
100% { ${ COMPRESSED } }
}
` } />
);
}
function renderContent(text : string, { label, locale, color, branding, logoColor, funding, env, cards, dynamicContent, layout, size } :
{ layout? : $Values<typeof BUTTON_LAYOUT>, size? : $Values<typeof BUTTON_SIZE>, label? : string, locale : LocaleType, color : string, branding? : boolean, logoColor? : string, funding? : FundingSelection, env : string, cards : $ReadOnlyArray<string>, dynamicContent? : Object }) : JsxHTMLNode {
const content = getLocaleContent(locale);
return jsxRender(text, {
text(value : string) : JsxHTMLNode {
const className = `${ CLASS.TEXT }`;
return <span class={ className } optional>{ value }</span>;
},
logo(name : string) : ?JsxHTMLNode {
if (!branding) {
return;
}
if (!logoColor) {
throw new Error(`Can not determine logo without logo color`);
}
const logo = (typeof fundingLogos[name] === 'function')
? fundingLogos[name]({ label, locale, color, branding, logoColor, funding, env, cards })
: fundingLogos[name][logoColor] || fundingLogos[name][BUTTON_LOGO_COLOR.ANY];
return (
<img
class={ `${ CLASS.LOGO } ${ CLASS.LOGO }-${ name } ${ CLASS.LOGO }-${ color }` }
src={ `data:image/svg+xml;base64,${ base64encode(logo.toString()) }` } alt="" aria-label={ name } />
);
},
content(name : string) : JsxHTMLNode {
let contentString;
for (const key of name.split('|')) {
if (content[key]) {
contentString = content[key];
break;
}
}
const regex = /\[([a-z]+)\]/g;
contentString = contentString && contentString.replace(regex, (match, contentVariable) => {
if (match && contentVariable) {
return dynamicContent && dynamicContent[contentVariable];
}
});
if (!contentString && env === ENV.TEST) {
throw new Error(`Could not find content ${ name } for ${ locale.lang }_${ locale.country }`);
}
return renderContent(contentString || '', { label, locale, color, branding, logoColor, funding, env, cards });
},
cards() : $ReadOnlyArray<JsxHTMLNode> {
if (!funding) {
throw new Error(`Can not determine card types without funding`);
}
return renderCards({ cards, layout, size });
},
separator() : JsxHTMLNode {
return <span class={ CLASS.SEPARATOR } />;
},
break(value : string) : JsxHTMLNode {
const className = `${ CLASS.TEXT }`;
return <span class={ className }>{ value.split('<br>')[0] }<br />{ value.split('<br>')[1] }</span>;
}
});
}
function renderButtonTextDiv({ contentText, personalizedButtonText, impression, branding, allowedAnimation }) : JsxHTMLNode {
return (
<div class={ `${ CLASS.BUTTON_LABEL }` }>
{ getButtonTextAnimationStyle({ personalizedButtonText, branding, allowedAnimation }) }
{ contentText }
{ personalizedButtonText }
{
impression && Beacon(impression)
}
</div>
);
}
export function determineButtonTitle({ locale, label, branding } : { label : $Values<typeof BUTTON_LABEL>, locale : Object, branding : boolean }) : string {
const localeContent = getLocaleContent(locale);
const labelContent = localeContent && localeContent[label];
if (labelContent) {
const regex = /({logo:(pp|paypal)})+(\s)*({logo:(pp|paypal)})*/;
let str = labelContent.replace(regex, FUNDING_BRAND_LABEL.PAYPAL);
// removes PayPal from unbranded BuyNow button
if (label === BUTTON_LABEL.BUYNOW && !branding) {
str = str.replace(FUNDING_BRAND_LABEL.PAYPAL, '');
}
return str;
}
return label;
}
function renderButton({ size, label, color, locale, branding, multiple, layout, shape, source, funding, tagline, i, env, cards, installmentperiod, checkoutCustomization } :
{ size : $Values<typeof BUTTON_SIZE>, label : $Values<typeof BUTTON_LABEL>, color : string, branding : boolean, locale : Object, multiple : boolean, layout : $Values<typeof BUTTON_LAYOUT>, shape : string, funding : FundingSelection, tagline : boolean, source : FundingSource, i : number, env : string, cards : $ReadOnlyArray<string>, checkoutCustomization : ?CheckoutCustomizationType, installmentperiod : number }) : JsxHTMLNode {
const logoColor = getButtonConfig(label, 'logoColors')[color];
const buttonLabel = determineLabel({ label, source, multiple, layout });
// If the determined button label matches up with the label passed by the merchant, use
// the label template, otherwise use the logo template.
let contentText;
let impression;
const morsText = checkoutCustomization && checkoutCustomization.buttonText && checkoutCustomization.buttonText.text;
let personalizedButtonText;
let allowedAnimation;
if (allowedPersonalizationLabels.indexOf(label) !== -1) {
allowedAnimation = true;
}
if (buttonLabel === label && label === BUTTON_LABEL.BUYNOW && !branding) {
contentText = getButtonConfig(label, 'label');
} else if (buttonLabel === label && !__WEB__) {
if (allowedPersonalizationLabels.indexOf(label) !== -1 && morsText && branding && !tagline) {
personalizedButtonText = renderPersonalizationButtonText(morsText);
impression = checkoutCustomization && checkoutCustomization.buttonText && checkoutCustomization.buttonText.tracking && checkoutCustomization.buttonText.tracking.impression;
}
contentText = getButtonConfig(label, 'label');
} else {
contentText = getButtonConfig(label, 'logoLabel');
}
// Add all the variables in dynamic content required to be plugged in content
const dynamicContent = {
installmentperiod,
locale
};
contentText = (typeof contentText === 'function') ? contentText(dynamicContent) : contentText;
contentText = renderContent(contentText, { label, locale, color, branding, logoColor, funding, env, cards, dynamicContent, layout, size });
// button title used to set aria-label for the button div -- a11y
const title = BUTTON_CONFIG[label].title;
const buttonTitle = (typeof title === 'string') ? title : determineButtonTitle({ locale, label, branding });
// Define a list of funding options that will not need a tabindex
const hasTabIndex = [
FUNDING.CARD
].indexOf(source) === -1;
const role = source === FUNDING.CARD ? {} : { role: 'button' };
return (
<div
{ ...{ [ATTRIBUTE.LAYOUT]: layout ? layout : '' } }
{ ...{ [ATTRIBUTE.SIZE]: size ? size : '' } }
{ ...{ [ ATTRIBUTE.FUNDING_SOURCE ]: source, [ ATTRIBUTE.BUTTON ]: true } }
class={ `${ CLASS.BUTTON } ${ CLASS.NUMBER }-${ i } ${ getCommonButtonClasses({ layout, shape, branding, multiple, env }) } ${ getButtonClasses({ label, color, logoColor }) }` }
{ ...role }
tabindex={ hasTabIndex && 0 }
aria-label={ buttonTitle }>
{ source === FUNDING.CARD ? contentText : renderButtonTextDiv({ contentText, personalizedButtonText, impression, branding, allowedAnimation }) }
</div>
);
}
function renderTagline({ label, tagline, color, locale, multiple, env, cards, checkoutCustomization, layout } : { label : string, color : string, tagline : boolean, locale : LocaleType, multiple : boolean, env : string, cards : $ReadOnlyArray<string>, checkoutCustomization : ?CheckoutCustomizationType, layout : $Values<typeof BUTTON_LAYOUT> }) : ?JsxHTMLNode {
if (!tagline) {
return;
}
// tagline is only supported in horizontal layout
if (__WEB__ || layout === BUTTON_LAYOUT.VERTICAL) {
return;
// return LoadingDots(delay);
}
const tag = multiple
? (getButtonConfig(label, 'dualTag') || getButtonConfig(label, 'tag'))
: getButtonConfig(label, 'tag');
const text = checkoutCustomization && checkoutCustomization.tagline && checkoutCustomization.tagline.text ? checkoutCustomization.tagline.text : renderContent(tag, { locale, color, env, cards });
const impression = checkoutCustomization && checkoutCustomization.tagline && checkoutCustomization.tagline.tracking && checkoutCustomization.tagline.tracking.impression;
if (!text) {
return;
}
const tagColor = getButtonConfig(label, 'tagLineColors')[color];
return Tagline(tagColor, impression, text);
}
function renderScript() : JsxHTMLNode {
let script = getComponentScript().toString();
script = script.replace(/\{\s*CLASS\.([A-Z0-9_]+)\s*\}/g, (match, name) => {
return CLASS[name];
});
return (
<script innerHTML={ `(${ script })();` } />
);
}
function renderStyle({ height, cardNumber } : { height? : ?number, cardNumber? : number }) : JsxHTMLNode {
return (
<style innerHTML={ componentStyle({ height, cardNumber }) } />
);
}
function renderPowerByPaypalLogo(props) : ChildType {
if (!props) {
return null;
}
const { layout, sources = [] } = props;
if (!(layout === BUTTON_LAYOUT.VERTICAL)) {
return null;
}
const isCardDisallowed = sources.indexOf(FUNDING.CARD) === -1;
if (isCardDisallowed) {
return null;
}
return (
<div
class="powered-by-paypal"
style={ `
text-align: center;
margin: 10px auto;
height: 14px;
font-family: PayPal-Sans, HelveticaNeue, sans-serif;
font-size: 11px;
font-weight: normal;
font-style: italic;
font-stretch: normal;
color: #7b8388;
position: relative;
margin-right: 3px;
bottom: 3px;
` }>
{ renderContent('{ content: poweredBy }', { ...props, logoColor: 'blue' }) }
</div>
);
}
export function componentTemplate({ props } : { props : Object }) : string {
if (props && props.style) {
const style = props.style;
if (style.label === 'generic') {
style.label = 'paypal';
}
if (style.color === 'creditblue') {
delete style.color;
}
if (style.maxbuttons === 1 && style.tagline === false &&
style.size === 'responsive' && style.layout === 'horizontal' &&
!style.height) {
style.height = 44;
}
}
validateButtonProps(props);
const { label, locale, color, shape, branding,
tagline, funding, layout, sources, multiple,
env, height, cards, installmentperiod, fundingicons, size, checkoutCustomization } = normalizeProps(props);
const buttonNodes = determineButtons({ label, color, sources, multiple, layout })
.map((button, i) => renderButton({
label: button.label,
color: button.color,
source: button.source,
env,
i,
funding,
multiple,
locale,
branding,
tagline,
layout,
shape,
cards,
installmentperiod,
size,
checkoutCustomization
}));
const taglineNode = renderTagline({ label, tagline, color, locale, multiple, env, cards, checkoutCustomization, layout });
const fundingiconNode = renderFundingIcons({ cards, fundingicons, size, layout });
const styleNode = renderStyle({ height, cardNumber: cards.length });
const scriptNode = renderScript();
const labelPowerByPayPal = cards.length > 0 ? renderPowerByPaypalLogo(normalizeProps(props)) : null;
return (
<div { ...{ [ ATTRIBUTE.VERSION ]: __PAYPAL_CHECKOUT__.__MINOR_VERSION__ } } class={ `${ CLASS.CONTAINER } ${ getCommonButtonClasses({ layout, shape, branding, multiple, env }) }` }>
{ styleNode }
{ buttonNodes }
{ taglineNode || fundingiconNode }
{ labelPowerByPayPal }
{ scriptNode }
</div>
).toString();
}