UNPKG

react-native-payments-androidx

Version:

Welcome to the best and most comprehensive library for integrating payments like Apple Pay and Google Pay into your React Native app.

319 lines (256 loc) 8.72 kB
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.` ); } }