js/PaymentRequest/helpers/index.js
import type {
PaymentDetailsInit,
PaymentItem,
PaymentShippingOption
} from '../types';
import { isDecimal, isFloat, isInt, toFloat, toInt } from 'validator';
import { DOMException, ConstructorError } from '../errors';
type AmountValue = string | number;
function isNumber(value) {
return typeof value === 'number';
}
function isString(value) {
return typeof value === 'string';
}
export function isValidDecimalMonetaryValue(
amountValue: AmountValue | any
): boolean {
if (!isNumber(amountValue) && !isString(amountValue)) {
return false;
}
return isNumber(amountValue) || isValidStringAmount(amountValue);
}
export function isNegative(amountValue: AmountValue): boolean {
return isNumber(amountValue) ? amountValue < 0 : amountValue.startsWith('-');
}
export function isValidStringAmount(stringAmount): boolean {
if (stringAmount.endsWith('.')) {
return false;
}
return isDecimal(stringAmount);
}
export function toNumber(string: string): number {
if (isFloat(string)) {
return toFloat(string);
}
if (isInt(string)) {
return toInt(string);
}
}
export function toString(amountValue: AmountValue) {
return isNumber(amountValue) ? amountValue.toString() : amountValue;
}
export function convertObjectAmountToString(
objectWithAmount: PaymentItem | PaymentShippingOption
): PaymentItem | PaymentShippingOption {
return Object.assign({}, objectWithAmount, {
amount: Object.assign({}, {
value: toString(objectWithAmount.amount.value),
currency: objectWithAmount.amount.currency
})
});
}
export function convertDetailAmountsToString(
details: PaymentDetailsInit
): PaymentDetailsInit {
const nextDetails = Object.keys(details).reduce((acc, key) => {
if (key === 'total') {
return Object.assign({}, acc, {
[key]: convertObjectAmountToString(details[key])
});
}
if (
Array.isArray(details[key]) &&
(key === 'displayItems' || key === 'shippingOptions')
) {
return Object.assign({}, acc, {
[key]: details[key].map(paymentItemOrShippingOption =>
convertObjectAmountToString(paymentItemOrShippingOption)
)
});
}
return acc;
}, {});
return nextDetails;
}
export function getPlatformMethodData(
methodData: Array<PaymentMethodData>,
platformOS: 'ios' | 'android'
) {
const platformSupportedMethod =
platformOS === 'ios' ? 'apple-pay' : 'android-pay';
const platformMethod = methodData.find(paymentMethodData =>
paymentMethodData.supportedMethods.includes(platformSupportedMethod)
);
if (!platformMethod) {
throw new DOMException('The payment method is not supported');
}
return platformMethod.data;
}
// Validators
export function validateTotal(total, errorType = Error): void {
// Should Vailidator take an errorType to prepopulate "Failed to construct 'PaymentRequest'"
if (total === undefined) {
throw new errorType(`required member total is undefined.`);
}
const hasTotal = total && total.amount && (total.amount.value || total.amount.value === 0)
// Check that there is a total
if (!hasTotal) {
throw new errorType(`Missing required member(s): amount, label.`);
}
const totalAmountValue = total.amount.value;
// Check that total is a valid decimal monetary value.
if (!isValidDecimalMonetaryValue(totalAmountValue)) {
throw new errorType(
`'${totalAmountValue}' is not a valid amount format for total`
);
}
// Check that total isn't negative
if (isNegative(totalAmountValue)) {
throw new errorType(`Total amount value should be non-negative`);
}
}
export function validatePaymentMethods(methodData): Array {
// Check that at least one payment method is passed in
if (methodData.length < 1) {
throw new ConstructorError(`At least one payment method is required`);
}
let serializedMethodData = [];
// Check that each payment method has at least one payment method identifier
methodData.forEach(paymentMethod => {
if (paymentMethod.supportedMethods === undefined) {
throw new ConstructorError(
`required member supportedMethods is undefined.`
);
}
if (!Array.isArray(paymentMethod.supportedMethods)) {
throw new ConstructorError(
`required member supportedMethods is not iterable.`
);
}
if (paymentMethod.supportedMethods.length < 1) {
throw new ConstructorError(
`Each payment method needs to include at least one payment method identifier`
);
}
const serializedData = paymentMethod.data
? JSON.stringify(paymentMethod.data)
: null;
serializedMethodData.push([paymentMethod.supportedMethods, serializedData]);
});
return serializedMethodData;
}
export function validateDisplayItems(displayItems, errorType = Error): void {
// Check that the value of each display item is a valid decimal monetary value
if (displayItems) {
displayItems.forEach((item: PaymentItem) => {
const amountValue = item && item.amount && item.amount.value;
if (!amountValue && amountValue !== 0) {
throw new errorType(`required member value is undefined.`);
}
if (!isValidDecimalMonetaryValue(amountValue)) {
throw new errorType(
`'${amountValue}' is not a valid amount format for display items`
);
}
});
}
}
export function validateShippingOptions(details, errorType = Error): void {
if (details.shippingOptions === undefined) {
return undefined;
}
let selectedShippingOption = null;
if (!Array.isArray(details.shippingOptions)) {
throw new errorType(`Iterator getter is not callable.`);
}
if (details.shippingOptions) {
let seenIDs = [];
details.shippingOptions.forEach((shippingOption: PaymentShippingOption) => {
if (shippingOption.id === undefined) {
throw new errorType(`required member id is undefined.`);
}
// Reproducing how Chrome handlers `null`
if (shippingOption.id === null) {
shippingOption.id = 'null';
}
// 8.2.3.1 If option.amount.value is not a valid decimal monetary value, then throw a TypeError, optionally informing the developer that the value is invalid.
const amountValue = shippingOption.amount.value;
if (!isValidDecimalMonetaryValue(amountValue)) {
throw new errorType(
`'${amountValue}' is not a valid amount format for shippingOptions`
);
}
// 8.2.3.2 If seenIDs contains option.id, then set options to an empty sequence and break.
if (seenIDs.includes(shippingOption.id)) {
details.shippingOptions = [];
console.warn(
`[ReactNativePayments] Duplicate shipping option identifier '${shippingOption.id}' is treated as an invalid address indicator.`
);
return undefined;
}
// 8.2.3.3 Append option.id to seenIDs.
seenIDs.push(shippingOption.id);
});
}
}
export function getSelectedShippingOption(shippingOptions) {
// Return null if shippingOptions isn't an Array
if (!Array.isArray(shippingOptions)) {
return null;
}
// Return null if shippingOptions is empty
if (shippingOptions.length === 0) {
return null;
}
const selectedShippingOption = shippingOptions.find(
shippingOption => shippingOption.selected
);
// Return selectedShippingOption id
if (selectedShippingOption) {
return selectedShippingOption.id;
}
// Return first shippingOption if no shippingOption was marked as selected
return shippingOptions[0].id;
}
// Gateway helpers
export function hasGatewayConfig(platformMethodData = {}) {
if (!platformMethodData) {
return false;
}
if (!platformMethodData.paymentMethodTokenizationParameters) {
return false;
}
if (!platformMethodData.paymentMethodTokenizationParameters.parameters) {
return false;
}
if (
typeof platformMethodData.paymentMethodTokenizationParameters.parameters !==
'object'
) {
return false;
}
if (
!platformMethodData.paymentMethodTokenizationParameters.parameters.gateway
) {
return false;
}
if (
typeof platformMethodData.paymentMethodTokenizationParameters.parameters
.gateway !== 'string'
) {
return false;
}
return true;
}
export function getGatewayName(platformMethodData) {
return platformMethodData.paymentMethodTokenizationParameters.parameters
.gateway;
}
export function validateGateway(selectedGateway = '', supportedGateways = []) {
if (!supportedGateways.includes(selectedGateway)) {
throw new ConstructorError(
`"${selectedGateway}" is not a supported gateway. Visit https://goo.gl/fsxSFi for more info.`
);
}
}