UNPKG

paypal-checkout

Version:

PayPal Checkout components, for integrating checkout products.

553 lines (440 loc) 20.6 kB
// 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(); }