@financial-times/n-conversion-forms
Version:
Containing jsx components and styles for forms included on Accounts and Acquisition apps (next-signup, next-profile, next-retention, etc).
349 lines (335 loc) • 18.5 kB
JSX
"use strict";
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.PaymentTerm = PaymentTerm;
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _react = _interopRequireDefault(require("react"));
var _propTypes = _interopRequireDefault(require("prop-types"));
var _classnames = _interopRequireDefault(require("classnames"));
var _nPricing = require("@financial-times/n-pricing");
var _helpers = require("../helpers");
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2["default"])(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function PaymentTerm(_ref) {
var _ref$fieldId = _ref.fieldId,
fieldId = _ref$fieldId === void 0 ? 'paymentTermField' : _ref$fieldId,
_ref$inputName = _ref.inputName,
inputName = _ref$inputName === void 0 ? 'paymentTerm' : _ref$inputName,
_ref$isPrintOrBundle = _ref.isPrintOrBundle,
isPrintOrBundle = _ref$isPrintOrBundle === void 0 ? false : _ref$isPrintOrBundle,
_ref$isDigitalEdition = _ref.isDigitalEdition,
isDigitalEdition = _ref$isDigitalEdition === void 0 ? false : _ref$isDigitalEdition,
_ref$options = _ref.options,
options = _ref$options === void 0 ? [] : _ref$options,
_ref$showLegal = _ref.showLegal,
showLegal = _ref$showLegal === void 0 ? true : _ref$showLegal,
_ref$largePrice = _ref.largePrice,
largePrice = _ref$largePrice === void 0 ? false : _ref$largePrice,
_ref$optionsInARow = _ref.optionsInARow,
optionsInARow = _ref$optionsInARow === void 0 ? false : _ref$optionsInARow,
_ref$billingCountry = _ref.billingCountry,
billingCountry = _ref$billingCountry === void 0 ? '' : _ref$billingCountry,
_ref$isAutoRenewingSu = _ref.isAutoRenewingSubscriptionTermType,
isAutoRenewingSubscriptionTermType = _ref$isAutoRenewingSu === void 0 ? true : _ref$isAutoRenewingSu,
_ref$isNonRenewingSub = _ref.isNonRenewingSubscriptionTermType,
isNonRenewingSubscriptionTermType = _ref$isNonRenewingSub === void 0 ? false : _ref$isNonRenewingSub;
/**
* Capitalises a string.
* @param {string} Input string to be capitalised
* @returns {string}
*/
var capitalise = function capitalise(string) {
return Boolean(string) ? string[0].toUpperCase() + string.slice(1) : string;
};
/**
* Creates the JSX for a single payment-term option, including the input,
* title, discount messaging, and descriptive pricing copy.
*
* @param {Object} option - Payment term configuration
* @param {string} option.value - ISO 8601 duration of the subscription term of the offer
* @param {string} option.name - term name, e.g. "monthly", "annual", "2 yearly
* @param {string} option.price - Formatted display price
* @param {string|number} [option.amount] - Price expressed in numerical terms
* @param {string} [option.symbol] - Currency symbol, e.g. £
* @param {string} [option.monthlyPrice] - Precomputed monthly equivalent price (can be with or without currency symbol)
* @param {boolean} [option.discount] - Whether the option should display discount messaging
* @param {boolean} [option.bestOffer] - Whether the option should show "Best offer" instead of standard discount copy
* @param {boolean} [option.selected] - Whether the option is selected by default
* @param {boolean} [option.isTrial] - Whether the option is a trial offer
* @param {boolean} [option.subscriptionAutoRenewTerm] - Whether the option is for an auto-renewing subscription
* @param {number} [option.trialAmount] - Amount used for trial pricing
* @param {string} [option.trialDuration] - Human-readable trial duration copy
* @param {string} [option.trialPrice] - Formatted trial price
* @param {string} [option.displayName] - Override label for the term title
* @param {string} [option.title] - Fallback title for legacy or non-period terms
* @param {string} [option.subTitle] - Optional subtitle shown alongside the term title
* @param {string} [option.chargeOnText] - Optional charge timing copy for non-period offers
* @param {boolean} [option.b2cPartnership] - Whether the option is part of a B2C partnership offer
* @param {string} [option.b2cDiscountCopy] - Partnership-specific discount copy
* @returns {React.ReactElement} A rendered payment term option
*/
var createPaymentTerm = function createPaymentTerm(option) {
var className = (0, _classnames["default"])(['ncf__payment-term__item', 'o-forms-input--radio-round', {
'ncf__payment-term__item--discount': option.discount
}]);
var props = _objectSpread({
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
});
/**
* Determines whether input is a valid ISO 8601 duration value that can be decoded by the Period class.
* @returns {boolean}
*/
var isValidPeriod = function isValidPeriod() {
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 _nPricing.Period(typeof option.value === 'string' ? option.value : '');
return true;
} catch (error) {
return false;
}
};
/**
* Compute monthly price for given term name.
* @returns {string}
*/
var getMonthlyPriceFromPeriod = function getMonthlyPriceFromPeriod() {
var periodObj = new _nPricing.Period(option.value);
var monthlyPrice = periodObj.calculatePrice('P1M', option.amount);
return new _nPricing.Monthly({
value: monthlyPrice,
symbol: option.symbol
}).getAmount('monthly');
};
/**
* Returns elements that include the text describing the price of the offer option.
* @returns {React.ReactElement}
*/
var getPriceText = function getPriceText() {
var isExpressedAsSinglePayment = !option.subscriptionAutoRenewTerm ||
// With an auto-renewing annual term there is a high chance
// it will not be the same price in the second year,
// so we do not want to imply that the price will remain consistent.
// For shorter auto-renewing terms there is higher confidence that the price
// will remain consistent across subsequent terms.
option.subscriptionAutoRenewTerm && (0, _helpers.is52WeeksOrLonger)(option.value);
var isExpressedAsRecurringPayment = !isExpressedAsSinglePayment;
if (isExpressedAsSinglePayment) {
return /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, "Single", ' ', /*#__PURE__*/_react["default"].createElement("span", {
className: "ncf__payment-term__price"
}, option.price), ' ', "payment");
}
if (isExpressedAsRecurringPayment) {
return /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement("span", {
className: "ncf__payment-term__price"
}, option.price), " per", ' ', (0, _helpers.getDurationFromISO8601Value)({
iso8601Value: option.value
}));
}
};
/**
* Returns elements that include the text describing the price increase following the trial period.
* @returns {React.ReactElement}
*/
var getTrialPriceExplanatoryText = function getTrialPriceExplanatoryText() {
return /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, "Unless you cancel during your trial you will be billed", ' ', /*#__PURE__*/_react["default"].createElement("span", {
className: "ncf__payment-term__price"
}, option.price), " per", ' ', (0, _helpers.getDurationFromISO8601Value)({
iso8601Value: option.value
}), ' ', "after the trial period.");
};
/**
* Returns elements that include the text describing how regularly an auto-reniewing subscription will renew.
* @returns {React.ReactElement}
*/
var getRenewalPeriodText = function getRenewalPeriodText() {
return /*#__PURE__*/_react["default"].createElement("p", {
className: "ncf__payment-term__renews-text"
}, "Renews every", ' ', (0, _helpers.getDurationFromISO8601Value)({
iso8601Value: option.value
}), " unless cancelled");
};
/**
* Returns elements that include the text describing the monthly equivalent of the subscription.
* @returns {string}
*/
var getEquivalentMonthlyPrice = function getEquivalentMonthlyPrice() {
if (Boolean(option.monthlyPrice) && option.monthlyPrice !== '0') {
return isNaN(option.monthlyPrice) ? option.monthlyPrice : "".concat(option.symbol).concat(option.monthlyPrice);
}
return getMonthlyPriceFromPeriod();
};
/**
* Returns elements that include the text describing the monthly equivalent of the subscription.
* @returns {React.ReactElement}
*/
var getEquivalentMonthlyPriceText = function getEquivalentMonthlyPriceText() {
return /*#__PURE__*/_react["default"].createElement("span", {
className: "ncf__payment-term__equivalent-price"
}, "That\u2019s equivalent to", ' ', /*#__PURE__*/_react["default"].createElement("span", {
className: "ncf__payment-term__monthly-price"
}, getEquivalentMonthlyPrice()), ' ', "per month");
};
/**
* Creates the standard discount badge for an option.
* Displays either "Best offer" or "Save X off RRP" when a discount exists.
*
* @returns {React.ReactElement} The discount element, or false when no discount should be shown
*/
var createDiscount = function createDiscount() {
return option.discount && /*#__PURE__*/_react["default"].createElement("span", {
className: "ncf__payment-term__discount"
}, option.bestOffer ? 'Best offer' : "Save ".concat(option.discount, " off RRP"));
};
/**
* Creates B2C partnership discount copy for eligible annual offers.
*
* @returns {React.ReactElement} The B2C discount element, or false when the option is not eligible
*/
var createB2cDiscountCopy = function createB2cDiscountCopy() {
return option.name === 'annual' && option.b2cPartnership && option.b2cDiscountCopy && /*#__PURE__*/_react["default"].createElement("span", {
className: "ncf__payment-term__discount"
}, option.b2cDiscountCopy);
};
/**
* Creates the description shown beneath the term title.
* This may include trial copy, price-per-period copy, equivalent monthly price,
* renewal messaging, or fallback non-period pricing text.
*
* @returns {React.ReactElement} The description block for the payment term
*/
var createDescription = function createDescription() {
if (option.isTrial) {
return /*#__PURE__*/_react["default"].createElement("div", {
className: "ncf__payment-term__description"
}, option.trialDuration || '4 weeks', " for", ' ', /*#__PURE__*/_react["default"].createElement("span", {
className: "ncf__payment-term__trial-price"
}, option.trialPrice), /*#__PURE__*/_react["default"].createElement("br", null), getTrialPriceExplanatoryText());
}
if (isValidPeriod(option.value)) {
return /*#__PURE__*/_react["default"].createElement("div", {
className: "ncf__payment-term__description"
}, getPriceText(), (0, _helpers.is90DaysOrLonger)(option.value) && getEquivalentMonthlyPriceText(), option.subscriptionAutoRenewTerm && getRenewalPeriodText());
}
return /*#__PURE__*/_react["default"].createElement("div", null, /*#__PURE__*/_react["default"].createElement("span", {
className: largePrice ? 'ncf__payment-term__large-price' : ''
}, option.price), option.chargeOnText && /*#__PURE__*/_react["default"].createElement("p", {
className: "ncf__payment-term__charge-on-text"
}, option.chargeOnText));
};
/**
* Builds the display name shown as the payment term title.
* May prepend trial copy, use the capitalised term name for auto-renewing terms,
* derive a human-readable period for valid non-renewing terms, or fall back to
* a legacy title when no valid period is available.
*
* @returns {string} The formatted display name for the payment term
*/
var getTermDisplayName = function getTermDisplayName() {
var showTrialCopyInTitle = option.isTrial && !isPrintOrBundle && !isDigitalEdition;
var termDisplayName = '';
if (showTrialCopyInTitle) {
var termName = option.displayName ? option.displayName : 'Premium Digital';
termDisplayName = "Trial: ".concat(termName, " - ");
}
var getTermPeriod = function getTermPeriod() {
if (option.subscriptionAutoRenewTerm && option.name) {
return capitalise(option.name);
}
// custom offer with period provided
if (!option.subscriptionAutoRenewTerm && isValidPeriod(option.value)) {
return (0, _helpers.getDurationFromISO8601Value)({
iso8601Value: option.value,
excludeAmountWhenSingular: false
});
}
// custom legacy cases, where period is not provided
return option.title;
};
var termPeriod = getTermPeriod();
termDisplayName = "".concat(termDisplayName).concat(termPeriod, " ");
return termDisplayName;
};
return /*#__PURE__*/_react["default"].createElement("div", {
key: option.value,
className: className
}, /*#__PURE__*/_react["default"].createElement("input", props), /*#__PURE__*/_react["default"].createElement("label", {
htmlFor: option.value,
className: "o-forms-input__label ncf__payment-term__label"
}, createDiscount(), createB2cDiscountCopy(), /*#__PURE__*/_react["default"].createElement("span", {
className: (0, _classnames["default"])(['ncf__payment-term__title', {
'ncf__payment-term__title--large-price': largePrice
}])
}, getTermDisplayName(), option.subTitle && /*#__PURE__*/_react["default"].createElement("span", {
className: "ncf__regular ncf__payment-term__sub-title"
}, option.subTitle)), createDescription()));
};
return /*#__PURE__*/_react["default"].createElement("div", {
id: fieldId,
className: "o-forms__group ncf__payment-term",
"data-country-code": billingCountry
}, /*#__PURE__*/_react["default"].createElement("div", {
className: optionsInARow ? 'ncf__payment-term__options-grid' : ''
}, options.map(function (option) {
return createPaymentTerm(option);
})), showLegal && /*#__PURE__*/_react["default"].createElement("div", {
className: "ncf__payment-term__legal"
}, isAutoRenewingSubscriptionTermType && /*#__PURE__*/_react["default"].createElement(_react["default"].Fragment, null, /*#__PURE__*/_react["default"].createElement("p", null, "With all subscription types, we will automatically renew your subscription using the payment method provided unless you cancel before your renewal date."), /*#__PURE__*/_react["default"].createElement("p", {
className: "o3-type-body-base"
}, "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", ' ', /*#__PURE__*/_react["default"].createElement("a", {
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"), ".")), isNonRenewingSubscriptionTermType && /*#__PURE__*/_react["default"].createElement("p", {
className: "o3-type-body-base"
}, "Find out more about our cancellation policy in our", ' ', /*#__PURE__*/_react["default"].createElement("a", {
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"), ".")));
}
PaymentTerm.propTypes = {
fieldId: _propTypes["default"].string,
inputName: _propTypes["default"].string,
isPrintOrBundle: _propTypes["default"].bool,
isDigitalEdition: _propTypes["default"].bool,
options: _propTypes["default"].arrayOf(_propTypes["default"].shape({
b2cDiscountCopy: _propTypes["default"].string,
isB2cPartnership: _propTypes["default"].bool,
discount: _propTypes["default"].string,
isTrial: _propTypes["default"].bool,
subscriptionAutoRenewTerm: _propTypes["default"].bool,
name: _propTypes["default"].string.isRequired,
price: _propTypes["default"].string.isRequired,
selected: _propTypes["default"].bool,
trialDuration: _propTypes["default"].string,
trialPrice: _propTypes["default"].string,
amount: _propTypes["default"].string,
symbol: _propTypes["default"].string,
trialAmount: _propTypes["default"].number,
value: _propTypes["default"].string.isRequired,
monthlyPrice: _propTypes["default"].string,
title: _propTypes["default"].string,
subTitle: _propTypes["default"].string,
bestOffer: _propTypes["default"].bool,
chargeOnText: _propTypes["default"].string,
fulfilmentOption: _propTypes["default"].string
})),
isAutoRenewingSubscriptionTermType: _propTypes["default"].bool,
isNonRenewingSubscriptionTermType: _propTypes["default"].bool,
showLegal: _propTypes["default"].bool,
largePrice: _propTypes["default"].bool,
optionsInARow: _propTypes["default"].bool,
billingCountry: _propTypes["default"].string
};