UNPKG

@rnw-community/react-native-payments

Version:

Accept Payments with Apple Pay and Android Pay using the Payment Request API.

100 lines (80 loc) 3.72 kB
import { emptyFn, isDefined, wait } from '@rnw-community/shared'; import { changeEventTimeoutMs } from '../../constant/change-event-timeout-ms.js'; import { PaymentsErrorEnum } from '../../enum/payments-error.enum.js'; import { DOMException } from '../../error/dom.exception.js'; import { warnChangeEventError } from '../../util/warn-change-event-error.util.js'; import { PaymentMethodChangeEvent } from '../payment-method-change-event/payment-method-change-event.js'; import { PaymentRequestUpdateEvent } from '../payment-request-update-event/payment-request-update-event.js'; import type { PaymentDetailsUpdate } from '../../@standard/w3c/payment-details-update.js'; import type { PaymentRequestEventPayloadInterface } from '../../interface/payment-request-event-payload.interface.js'; import type { PaymentRequestEventListener } from '../../type/payment-request-event-listener.type.js'; import type { PaymentRequestEventType } from '../../type/payment-request-event.type.js'; import type { EmptyFn, Maybe } from '@rnw-community/shared'; export class ChangeEventDispatcher { private answer: Maybe<Promise<PaymentDetailsUpdate>> = null; private isOpen = true; private releaseAbandoned: EmptyFn = emptyFn; private readonly abandoned = new Promise<null>(resolve => { this.releaseAbandoned = () => { resolve(null); }; }); private readonly isRequestActive: () => boolean; private readonly event: PaymentRequestUpdateEvent; constructor( type: PaymentRequestEventType, payload: PaymentRequestEventPayloadInterface, isRequestActive: () => boolean ) { this.isRequestActive = isRequestActive; this.event = type === 'paymentmethodchange' ? new PaymentMethodChangeEvent( payload.methodName ?? '', payload.methodDetails ?? null, this.handleUpdateWith ) : new PaymentRequestUpdateEvent(type, this.handleUpdateWith); } async dispatch(listeners: readonly PaymentRequestEventListener[]): Promise<Maybe<PaymentDetailsUpdate>> { const deadline = wait(changeEventTimeoutMs).then(() => null); await Promise.race([this.callListeners(listeners), this.abandoned, deadline]); this.isOpen = false; return await this.resolveAnswer(deadline); } abandon(): void { this.isOpen = false; this.releaseAbandoned(); } private async callListeners(listeners: readonly PaymentRequestEventListener[]): Promise<void> { for (const listener of [...listeners]) { if (this.event.isAnswered) { return; } if (listeners.includes(listener)) { // eslint-disable-next-line no-await-in-loop await this.callListener(listener); } } } private async callListener(listener: PaymentRequestEventListener): Promise<void> { try { await listener(this.event); } catch (error) { warnChangeEventError(error); } } private async resolveAnswer(deadline: Promise<null>): Promise<Maybe<PaymentDetailsUpdate>> { if (!isDefined(this.answer)) { return null; } return await Promise.race([this.answer, this.abandoned, deadline]); } private readonly handleUpdateWith = (detailsUpdate: Promise<PaymentDetailsUpdate>): void => { if (!this.isOpen || !this.isRequestActive()) { throw new DOMException(PaymentsErrorEnum.InvalidStateError); } detailsUpdate.catch(emptyFn); this.answer = detailsUpdate; }; }