naoufal/react-native-apple-pay

View on GitHub
js/PaymentRequest/helpers/index.js

Summary

Maintainability
B
6 hrs
Test Coverage
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.`
    );
  }
}