naoufal/react-native-apple-pay

View on GitHub
js/PaymentRequest/PaymentRequestUpdateEvent.js

Summary

Maintainability
A
3 hrs
Test Coverage
import { Platform } from 'react-native';

import {
  MODULE_SCOPING,
  SHIPPING_ADDRESS_CHANGE_EVENT,
  SHIPPING_OPTION_CHANGE_EVENT,
  INTERNAL_SHIPPING_ADDRESS_CHANGE_EVENT,
  INTERNAL_SHIPPING_OPTION_CHANGE_EVENT,
  USER_DISMISS_EVENT,
  USER_ACCEPT_EVENT,
  SUPPORTED_METHOD_NAME
} from './constants';
const noop = () => {};

import PaymentRequest from '.';
import NativePayments from '../NativeBridge';

// Helpers
import {
  validateTotal,
  validateDisplayItems,
  validateShippingOptions,
  convertDetailAmountsToString
} from './helpers';

// Errors
import { DOMException } from './errors';

export default class PaymentRequestUpdateEvent {
  name: string;
  target: PaymentRequest;
  _waitForUpdate: boolean;
  _handleDetailsChange: PaymentDetailsModifier => Promise<any>;
  _resetEvent: any;

  constructor(name, target) {
    if (
      name !== SHIPPING_ADDRESS_CHANGE_EVENT &&
      name !== SHIPPING_OPTION_CHANGE_EVENT
    ) {
      throw new Error(
        `Only "${SHIPPING_ADDRESS_CHANGE_EVENT}" and "${SHIPPING_OPTION_CHANGE_EVENT}" event listeners are supported.`
      );
    }

    this.name = name;
    this.target = target;
    this._waitForUpdate = false;

    this._handleDetailsChange = this._handleDetailsChange.bind(this);
    this._resetEvent = this._resetEvent.bind(this);
  }

  _handleDetailsChange(value: PaymentDetailsBase) {
    const target = this.target;

    validateTotal(value.total, DOMException);
    validateDisplayItems(value.displayItems, DOMException);
    validateShippingOptions(value.shippingOptions, DOMException);

    // 1. Let details be the result of converting value to a PaymentDetailsUpdate dictionary. If this throws an exception, abort the update with the thrown exception.
    const details: PaymentDetailsUpdate = Object.assign(target._details, value);

    // 2. Let serializedModifierData be an empty list.
    let serializedModifierData = [];

    // 3. Let selectedShippingOption be null.
    let selectedShippingOption = null;

    // 4. Let shippingOptions be an empty sequence<PaymentShippingOption>.
    let shippingOptions = [];

    // 5. Validate and canonicalize the details:
    // TODO: implmentation

    // 6. Update the PaymentRequest using the new details:
    target._details = details;

    // 6.1 If the total member of details is present, then:
    // if (details.total) {
    //   target._details = Object.assign({}, target._details, { total: details.total });
    // }

    // // 6.2 If the displayItems member of details is present, then:
    // if (details.displayItems) {
    //   target._details = Object.assign({}, target._details, { displayItems: details.displayItems });
    // }

    // // 6.3 If the shippingOptions member of details is present, and target.[[options]].requestShipping is true, then:
    // if (details.shippingOptions && target._options.requestShipping === true) {
    //   // 6.3.1 Set target.[[details]].shippingOptions to shippingOptions.
    //   shippingOptions = details.shippingOptions;
    //   target._details = Object.assign({}, target._details, { shippingOptions });

    //   // 6.3.2 Set the value of target's shippingOption attribute to selectedShippingOption.
    //   selectedShippingOption = target.shippingOption;
    // }

    // // 6.4 If the modifiers member of details is present, then:
    // if (details.modifiers) {
    //   // 6.4.1 Set target.[[details]].modifiers to details.modifiers.
    //   target._details = Object.assign({}, target._details, { modifiers: details.modifiers });

    //   // 6.4.2 Set target.[[serializedModifierData]] to serializedModifierData.
    //   target._serializedModifierData = serializedModifierData;
    // }

    // 6.5 If target.[[options]].requestShipping is true, and target.[[details]].shippingOptions is empty,
    // then the developer has signified that there are no valid shipping options for the currently-chosen shipping address (given by target's shippingAddress).
    // In this case, the user agent should display an error indicating this, and may indicate that that the currently-chosen shipping address is invalid in some way.
    // The user agent should use the error member of details, if it is present, to give more information about why there are no valid shipping options for that address.

    // React Native Payments specific 👇
    // ---------------------------------
    const normalizedDetails = convertDetailAmountsToString(target._details);
    return (
      NativePayments.handleDetailsUpdate(normalizedDetails, DOMException)
        // 14. Upon fulfillment of detailsPromise with value value
        .then(this._resetEvent())
        // On iOS the `selectedShippingMethod` defaults back to the first option
        // when updating shippingMethods.  So we call the `_handleShippingOptionChange`
        // method with the first shippingOption id so that JS is in sync with Apple Pay.
        .then(() => {
          if (Platform.OS !== 'ios') {
            return;
          }

          if (
            target._details.shippingOptions
            && target._details.shippingOptions.length > 0
            && value.shippingOptions
            && ((value.shippingOptions.find(op => op.selected) || {}).id || null) !== target._shippingOption
          ) {
            target._handleShippingOptionChange({
              selectedShippingOptionId: target._details.shippingOptions[0].id
            });
          }
        })
        // 13. Upon rejection of detailsPromise:
        .catch(e => {
          this._resetEvent();

          throw new Error('AbortError');
        })
    );
  }

  _resetEvent() {
    // 1. Set event.[[waitForUpdate]] to false.
    this._waitForUpdate = false;

    // 2. Set target.[[updating]] to false.
    this.target._updating = false;

    // 3. The user agent should update the user interface based on any changed values in target.
    // The user agent should re-enable user interface elements that might have been disabled in the steps above if appropriate.
    noop();
  }

  async updateWith(
    PaymentDetailsModifierOrPromise:
      | PaymentDetailsUpdate
      | ((
          PaymentDetailsModifier,
          PaymentAddress
        ) => Promise<PaymentDetailsUpdate>)
  ) {
    // 1. Let event be this PaymentRequestUpdateEvent instance.
    let event = this;

    // 2. Let target be the value of event's target attribute.
    let target = this.target;

    // 3. If target is not a PaymentRequest object, then throw a TypeError.
    if (!(target instanceof PaymentRequest)) {
      throw new Error('TypeError');
    }

    // 5. If event.[[waitForUpdate]] is true, then throw an "InvalidStateError" DOMException.
    if (event._waitForUpdate === true) {
      throw new Error('InvalidStateError');
    }

    // 6. If target.[[state]] is not " interactive", then throw an " InvalidStateError" DOMException.
    if (target._state !== 'interactive') {
      throw new Error('InvalidStateError');
    }

    // 7. If target.[[updating]] is true, then throw an "InvalidStateError" DOMException.
    if (target._updating === true) {
      throw new Error('InvalidStateError');
    }

    // 8. Set event's stop propagation flag and stop immediate propagation flag.
    noop();

    // 9. Set event.[[waitForUpdate]] to true.
    event._waitForUpdate = true;

    // 10. Set target.[[updating]] to true.
    target._updating = true;

    // 11. The user agent should disable the user interface that allows the user to accept the payment request.
    // This is to ensure that the payment is not accepted until the web page has made changes required by the change.
    // The web page must settle the detailsPromise to indicate that the payment request is valid again.
    // The user agent should disable any part of the user interface that could cause another update event to be fired.
    // Only one update may be processed at a time.
    noop(); // NativePayments does this for us (iOS does at the time of this comment)

    // 12. Return from the method and perform the remaining steps in parallel.

    if (typeof PaymentDetailsModifierOrPromise === 'object') {
      const paymentDetailsModifier = PaymentDetailsModifierOrPromise;

      return this._handleDetailsChange(paymentDetailsModifier);
    }

    if (typeof PaymentDetailsModifierOrPromise === 'function') {
      const detailsFromPromise = await PaymentDetailsModifierOrPromise();

      this._handleDetailsChange(detailsFromPromise);
    }

    // 13 & 14 happen in `this._handleDetailsChange`.
  }
}