Skip to content

Commit

Permalink
feat(Apple Pay): allow for customization of event updates
Browse files Browse the repository at this point in the history
Adds the `options.callbacks` option which supports the following
callbacks:
- `onPaymentMethodSelected`
- `onShippingContactSelected`
- `onShippingMethodSelected`
- `onPaymentAuthorized`
to correspond with the `ApplePaySession` events.

A callback must return either the Apple Pay update object or a Promise
that resolves with that object.

That update object will be used to complete the `ApplePaySession` event
to open up the ability to fetch custom taxes on payment method selection,
update shipping costs on shipping contact/method selection and validate
email/phone addresses on authorization.

This does not remove the existing events being emitted but the workflow
should be updated to prefer using `onPaymentAuthorized` to complete the
purchase instead of async on the `token` event. The event that is sent
to that callback includes the `payment.recurlyToken` for use.

This also enhances the `token` event to include the raw payment event as
the second argument.
  • Loading branch information
cbarton committed Apr 6, 2023
1 parent 31fc194 commit 9044668
Show file tree
Hide file tree
Showing 2 changed files with 238 additions and 114 deletions.
147 changes: 97 additions & 50 deletions lib/recurly/apple-pay/apple-pay.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,17 @@ const I18N = {
giftCardLineItemLabel: 'Gift card'
};

const CALLBACK_EVENT_MAP = {
onPaymentMethodSelected: 'paymentMethodSelected',
onShippingContactSelected: 'shippingContactSelected',
onShippingMethodSelected: 'shippingMethodSelected',
onPaymentAuthorized: 'paymentAuthorized'
};

const UPDATE_PROPERTIES = [
'newTotal', 'newLineItems', 'newRecurringPaymentRequest',
];

/**
* Initializes an Apple Pay session.
* Accepts all members of ApplePayPaymentRequest with the same name.
Expand Down Expand Up @@ -71,39 +82,47 @@ export class ApplePay extends Emitter {
debug('Creating new Apple Pay session', paymentRequest);

const session = new window.ApplePaySession(MINIMUM_SUPPORTED_VERSION, paymentRequest);
session.onvalidatemerchant = this.onValidateMerchant.bind(this);
session.onshippingcontactselected = this.onShippingContactSelected.bind(this);
session.onshippingmethodselected = this.onShippingMethodSelected.bind(this);
session.onpaymentmethodselected = this.onPaymentMethodSelected.bind(this);
session.onpaymentauthorized = this.onPaymentAuthorized.bind(this);
session.oncancel = this.onCancel.bind(this);
session.onvalidatemerchant = this.onValidateMerchant.bind(this);
session.onpaymentmethodselected = makeCallback(this, 'onPaymentMethodSelected');
session.onshippingcontactselected = makeCallback(this, 'onShippingContactSelected');
session.onshippingmethodselected = makeCallback(this, 'onShippingMethodSelected');
session.onpaymentauthorized = this.token.bind(this);

return this._session = session;
return this.session = session;
}

/**
* Resets the session
* @private
*/
set session (session) {
UPDATE_PROPERTIES.forEach(p => delete this[p]);
this._session = session;
}

/**
* @return {Object} recurring payment request for display on payment sheet
* @private
*/
get recurringPaymentRequest () {
return this._paymentRequest?.recurringPaymentRequest;
return this.newRecurringPaymentRequest ?? this._paymentRequest?.recurringPaymentRequest;
}

/**
* @return {Array} subtotal line items for display on payment sheet
* @private
*/
get lineItems () {
// Clone configured line items
return this._paymentRequest?.lineItems ? [].concat(this._paymentRequest.lineItems) : [];
return this.newLineItems ?? this._paymentRequest?.lineItems ?? [];
}

/**
* @return {Object} total cost line item
* @private
*/
get totalLineItem () {
return this._paymentRequest?.total ? this._paymentRequest.total : {};
return this.newTotal ?? this._paymentRequest?.total ?? {};
}

/**
Expand Down Expand Up @@ -136,8 +155,9 @@ export class ApplePay extends Emitter {
if (options.recurly) this.recurly = options.recurly;
else return this.initError = this.error('apple-pay-factory-only');

if (options.callbacks) this.config.callbacks = options.callbacks;
if (options.form) this.config.form = options.form;
if (options.recurring) this.config.recurring = options.recurring;
if ('recurring' in options) this.config.recurring = options.recurring;

this.config.i18n = {
...I18N,
Expand Down Expand Up @@ -221,7 +241,7 @@ export class ApplePay extends Emitter {
* @private
*/
onValidateMerchant (event) {
debug('Validating Apple Pay merchant session', event);
debug('validateMerchant', event);

const validationURL = event.validationURL;

Expand All @@ -235,6 +255,14 @@ export class ApplePay extends Emitter {
});
}

/**
* sets the `contact` to the proper address on the pricing object.
*
* @param {string} addressType 'shippingAddress' or 'address'
* @param {object} contact The Apple Pay contact
* @param {Function} done
* @private
*/
setAddress (addressType, contact, done) {
if (!contact || !this.config.pricing) return done?.();

Expand All @@ -243,23 +271,32 @@ export class ApplePay extends Emitter {
return this.config.pricing[addressType](address).done(done);
}

/**
* Stores the updates if any and completes the Apple Pay selection
* @param {Function} onComplete the session completion function
* @param {object} [update] the Apple Pay update object
* @private
*/
completeSelection (onComplete, update) {
UPDATE_PROPERTIES.forEach(p => this[p] = update?.[p] ?? this[p]);

onComplete.call(this.session, {
newTotal: this.finalTotalLineItem,
newLineItems: this.lineItems,
newRecurringPaymentRequest: this.recurringPaymentRequest,
...update,
});
}

/**
* Handles payment method selection
*
* @param {Event} event
* @private
*/
onPaymentMethodSelected (event) {
debug('Payment method selected', event);

this.emit('paymentMethodSelected', event);

this.setAddress('address', event.paymentMethod.billingContact, () => {
this.session.completePaymentMethodSelection({
newTotal: this.finalTotalLineItem,
newLineItems: this.lineItems,
...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }),
});
onPaymentMethodSelected ({ paymentMethod: { billingContact } }, update) {
this.setAddress('address', billingContact, () => {
this.completeSelection(this.session.completePaymentMethodSelection, update);
});
}

Expand All @@ -269,16 +306,9 @@ export class ApplePay extends Emitter {
* @param {Event} event
* @private
*/
onShippingContactSelected (event) {
this.emit('shippingContactSelected', event);

this.setAddress('shippingAddress', event.shippingContact, () => {
this.session.completeShippingContactSelection({
newTotal: this.finalTotalLineItem,
newLineItems: this.lineItems,
newShippingMethods: [],
...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }),
});
onShippingContactSelected ({ shippingContact }, update) {
this.setAddress('shippingAddress', shippingContact, () => {
this.completeSelection(this.session.completeShippingContactSelection, update);
});
}

Expand All @@ -288,15 +318,8 @@ export class ApplePay extends Emitter {
* @param {Event} event
* @private
*/
onShippingMethodSelected (event) {
this.emit('shippingMethodSelected', event);

this.session.completeShippingMethodSelection({
newTotal: this.finalTotalLineItem,
newLineItems: this.lineItems,
newShippingMethods: [],
...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }),
});
onShippingMethodSelected (event, update) {
this.completeSelection(this.session.completeShippingMethodSelection, update);
}

/**
Expand All @@ -307,12 +330,15 @@ export class ApplePay extends Emitter {
* @emit 'token'
* @private
*/
onPaymentAuthorized (event) {
debug('Payment authorization received', event);

this.emit('paymentAuthorized', event);
onPaymentAuthorized (event, { errors } = {}) {
if (typeof errors === 'object' && errors.length > 0) {
this.session.completePayment({ status: this.session.STATUS_FAILURE, errors });
return;
}

return this.token(event);
this.session.completePayment({ status: this.session.STATUS_SUCCESS });
this.emit('authorized', event); // deprecated
this.emit('token', event.payment.recurlyToken, event);
}

token (event) {
Expand All @@ -329,9 +355,8 @@ export class ApplePay extends Emitter {

debug('Token received', token);

this.session.completePayment({ status: this.session.STATUS_SUCCESS });
this.emit('authorized', event);
this.emit('token', token);
event.payment.recurlyToken = token;
makeCallback(this, 'onPaymentAuthorized')(event);
}
});
}
Expand Down Expand Up @@ -383,6 +408,28 @@ export class ApplePay extends Emitter {
}
}

function makeCallback (applePay, callbackName) {
const callback = applePay.config.callbacks?.[callbackName];
const eventName = CALLBACK_EVENT_MAP[callbackName];
const handler = applePay[callbackName].bind(applePay);

return function (event) {
debug(eventName, event);
applePay.emit(eventName, event);
runCallback(callback, event, handler);
};
}

function runCallback (callback, event, done) {
const retVal = callback?.(event);

if (typeof retVal?.finally === 'function') {
retVal.finally(val => done(event, val));
} else {
done(event, retVal);
}
}

function restorePricing (pricing, state, done) {
if (!pricing) return done();

Expand Down
Loading

0 comments on commit 9044668

Please sign in to comment.