@financial-times/n-conversion-forms
Version:
Containing jsx components and styles for forms included on Accounts and Acqusition apps (next-signup, next-profile, next-retention, etc).
412 lines (392 loc) • 11.9 kB
JSX
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { Period, Monthly } from '@financial-times/n-pricing';
export function PaymentTerm({
fieldId = 'paymentTermField',
inputName = 'paymentTerm',
isPrintOrBundle = false,
isEpaper = false,
options = [],
isFixedTermOffer = false,
offerDisplayName,
showLegal = true,
largePrice = false,
optionsInARow = false,
}) {
/**
* Compute monthly price for given term name
* @param {number} amount price in number format
* @param {string} currency country id of the currency
* @param {string} period PxY (yearly) or PxM (montly) where x is the amount of years/months
* @returns {string}
*/
const getMontlyPriceFromPeriod = (amount, currency, period) => {
const periodObj = new Period(period);
const monthlyPrice = periodObj.calculatePrice('P1M', amount);
return new Monthly({ value: monthlyPrice, currency }).getAmount('monthly');
};
/**
* returns period converted to time if found
* otherwise returns empty string to avoid show information not mapped
* @param {string} period PxY (yearly) or PxM (montly) where x is the amount of years/months
* @returns {string}
*/
const getTimeFromPeriod = (period) => {
const freq =
period.substring(period.length - 1) === 'Y' ? 'years' : 'months';
const amount = period.substring(1, period.length - 1);
return period ? `${amount} ${freq}` : '';
};
const isValidPeriod = (period) => {
try {
// Period should throw an error if it is not properly provided
// in order to validate it, we just send in case type is string
new Period(typeof period === 'string' ? period : '');
return true;
} catch (e) {
return false;
}
};
const nameMap = {
annual: {
title: 'Annual',
price: (price) => (
<React.Fragment>
Single{' '}
<span className="ncf__payment-term__price ncf__strong">{price}</span>{' '}
payment
</React.Fragment>
),
trialPrice: (price) => (
<React.Fragment>
Unless you cancel during your trial you will be billed{' '}
<span className="ncf__payment-term__price">{price}</span> per year
after the trial period.
</React.Fragment>
),
monthlyPrice: (price) =>
price && (
<span className="ncf__payment-term__equivalent-price">
That’s equivalent to{' '}
<span className="ncf__payment-term__monthly-price">{price}</span>{' '}
per month
</span>
),
renewsText: () => (
<p className="ncf__payment-term__renews-text">
Renews annually unless cancelled
</p>
),
},
quarterly: {
title: 'Quarterly',
price: (price) => (
<React.Fragment>
<span className="ncf__payment-term__price">{price}</span> per quarter
</React.Fragment>
),
trialPrice: (price) => (
<React.Fragment>
Unless you cancel during your trial you will be billed{' '}
<span className="ncf__payment-term__price">{price}</span> per quarter
after the trial period.
</React.Fragment>
),
monthlyPrice: () => {},
renewsText: () => (
<p className="ncf__payment-term__renews-text">
Renews quarterly unless cancelled
</p>
),
},
monthly: {
title: 'Monthly',
price: (price) => (
<React.Fragment>
<span className="ncf__payment-term__price">{price}</span> per month
</React.Fragment>
),
trialPrice: (price) => (
<React.Fragment>
Unless you cancel during your trial you will be billed{' '}
<span className="ncf__payment-term__price">{price}</span> per month
after the trial period.
</React.Fragment>
),
monthlyPrice: () => {},
renewsText: (isFixedTermOffer) => {
const textToDisplay = isFixedTermOffer
? 'This subscription is for 3 months, charged monthly. You can cancel at anytime'
: 'Renews monthly unless cancelled';
return (
<p className="ncf__payment-term__renews-text">{textToDisplay}</p>
);
},
},
custom: {
price: (price) => (
<React.Fragment>
Single{' '}
<span className="ncf__payment-term__price ncf__strong">{price}</span>{' '}
payment
</React.Fragment>
),
trialPrice: (trialPrice, trialPeriod) => (
<React.Fragment>
Unless you cancel during your trial you will be billed{' '}
<span className="ncf__payment-term__price">{trialPrice}</span> per{' '}
{trialPeriod}
after the trial period.
</React.Fragment>
),
monthlyPrice: (monthlyPrice) =>
Boolean(monthlyPrice) && (
<span className="ncf__payment-term__equivalent-price">
That’s equivalent to{' '}
<span className="ncf__payment-term__monthly-price">
{monthlyPrice}
</span>{' '}
per month
</span>
),
renewsText: (renewalPeriod) =>
Boolean(renewalPeriod) && (
<p className="ncf__payment-term__renews-text">
Renews every {renewalPeriod} unless cancelled
</p>
),
},
};
const createPaymentTerm = (option) => {
const className = classNames([
'ncf__payment-term__item',
'o-forms-input--radio-round',
{ 'ncf__payment-term__item--discount': option.discount },
]);
const props = {
type: 'radio',
id: option.value,
'data-base-amount': option.isTrial ? option.trialAmount : option.amount,
name: inputName,
value: option.value,
className:
'o-forms-input__radio o-forms-input__radio--right ncf__payment-term__input',
...(option.selected && { defaultChecked: true }),
};
const createDiscount = () => {
return (
option.discount && (
<span className="ncf__payment-term__discount">
{option.bestOffer
? 'Best offer'
: `Save ${option.discount} off RRP`}
</span>
)
);
};
const createB2cDiscountCopy = () => {
return (
option.name === 'annual' &&
option.b2cPartnership &&
option.b2cDiscountCopy && (
<span className="ncf__payment-term__discount">
{option.b2cDiscountCopy}
</span>
)
);
};
const createDescription = () => {
return option.isTrial ? (
<div className="ncf__payment-term__description">
{option.trialDuration || '4 weeks'} for{' '}
<span className="ncf__payment-term__trial-price">
{option.trialPrice}
</span>
<br />
{nameMap[option.name] &&
nameMap[option.name].trialPrice(option.price)}
</div>
) : (
<React.Fragment>
{nameMap[option.name] ? (
<div className="ncf__payment-term__description">
{nameMap[option.name].price(option.price)}
{nameMap[option.name].monthlyPrice(option.monthlyPrice)}
{nameMap[option.name].renewsText(isFixedTermOffer)}
{/* Remove this discount text temporarily in favour of monthly price */}
{/* <br />Save up to 25% when you pay annually */}
</div>
) : // this should cover the cases different than annual, quarterly and monthly
// for those containing period on option.value, render custom template, for the rest keep legacy render
isValidPeriod(option.value) ? (
<div className="ncf__payment-term__description">
{nameMap['custom'].price(option.price)}
{nameMap['custom'].monthlyPrice(
option.monthlyPrice && option.monthlyPrice !== '0'
? Number(option.monthlyPrice)
: getMontlyPriceFromPeriod(
option.amount,
option.currency,
option.value
)
)}
{nameMap['custom'].renewsText(getTimeFromPeriod(option.value))}
</div>
) : (
<div>
<span
className={largePrice ? 'ncf__payment-term__large-price' : ''}
>
{option.price}
</span>
{option.chargeOnText && (
<p className="ncf__payment-term__charge-on-text">
{option.chargeOnText}
</p>
)}
</div>
)}
</React.Fragment>
);
};
const getTermDisplayName = () => {
const showTrialCopyInTitle =
option.isTrial && !isPrintOrBundle && !isEpaper;
const defaultTitle =
option.name && nameMap[option.name] ? nameMap[option.name].title : '';
const title = isFixedTermOffer
? `${offerDisplayName} - ${defaultTitle}`
: defaultTitle;
let termDisplayName = '';
if (showTrialCopyInTitle) {
const termName = option.displayName
? option.displayName
: 'Premium Digital';
termDisplayName = `Trial: ${termName} - `;
}
const getTermPeriod = () => {
// annual, quarterly and monthly
if (nameMap[option.name]) {
return title;
}
// custom offer with period provided
if (isValidPeriod(option.value)) {
return getTimeFromPeriod(option.value);
}
// custom legacy cases, where period is not provided
return option.title;
};
const termPeriod = getTermPeriod();
termDisplayName = `${termDisplayName}${termPeriod} `;
return termDisplayName;
};
return (
<div key={option.value} className={className}>
<input {...props} />
<label
htmlFor={option.value}
className="o-forms-input__label ncf__payment-term__label"
>
{createDiscount()}
{createB2cDiscountCopy()}
<span
className={classNames([
'ncf__payment-term__title',
{ 'ncf__payment-term__title--large-price': largePrice },
])}
>
{getTermDisplayName()}
{option.subTitle && (
<span className="ncf__regular ncf__payment-term__sub-title">
{option.subTitle}
</span>
)}
</span>
{createDescription()}
</label>
</div>
);
};
return (
<div id={fieldId} className="o-forms__group ncf__payment-term">
<div className={optionsInARow ? 'ncf__payment-term__options-grid' : ''}>
{options.map((option) => createPaymentTerm(option))}
</div>
{showLegal && (
<div className="ncf__payment-term__legal">
{isFixedTermOffer ? (
<p>
Find out more about our cancellation policy in our{' '}
<a
className="ncf__link--external"
href="https://help.ft.com/legal-privacy/terms-and-conditions/"
title="FT Legal Terms and Conditions help page"
target="_blank"
rel="noopener noreferrer"
>
Terms & Conditions
</a>
.
</p>
) : (
<React.Fragment>
<p>
With all subscription types, we will automatically renew your
subscription using the payment method provided unless you cancel
before your renewal date.
</p>
<p>
We will notify you at least 14 days in advance of any changes to
the price in your subscription that would apply upon next
renewal. Find out more about our cancellation policy in our{' '}
<a
className="ncf__link--external"
href="https://help.ft.com/legal-privacy/terms-and-conditions/"
title="FT Legal Terms and Conditions help page"
target="_blank"
rel="noopener noreferrer"
>
Terms & Conditions
</a>
.
</p>
</React.Fragment>
)}
</div>
)}
</div>
);
}
PaymentTerm.propTypes = {
fieldId: PropTypes.string,
inputName: PropTypes.string,
isPrintOrBundle: PropTypes.bool,
isEpaper: PropTypes.bool,
options: PropTypes.arrayOf(
PropTypes.shape({
b2cDiscountCopy: PropTypes.string,
isB2cPartnership: PropTypes.bool,
discount: PropTypes.string,
isTrial: PropTypes.bool,
name: PropTypes.string.isRequired,
price: PropTypes.string.isRequired,
selected: PropTypes.bool,
trialDuration: PropTypes.string,
trialPrice: PropTypes.string,
amount: PropTypes.string,
trialAmount: PropTypes.number,
value: PropTypes.string.isRequired,
monthlyPrice: PropTypes.string,
title: PropTypes.string,
subTitle: PropTypes.string,
bestOffer: PropTypes.bool,
chargeOnText: PropTypes.string,
})
),
isFixedTermOffer: PropTypes.bool,
offerDisplayName: PropTypes.string,
showLegal: PropTypes.bool,
largePrice: PropTypes.bool,
optionsInARow: PropTypes.bool,
};