Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Apple Pay): allow for customization of event updates #808

Merged
merged 2 commits into from
Apr 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
cbarton marked this conversation as resolved.
Show resolved Hide resolved

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

Expand Down
16 changes: 15 additions & 1 deletion test/types/apple-pay.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,15 @@
import { ApplePayPaymentRequest, ApplePayLineItem } from 'lib/apple-pay/native';
import {
ApplePayPaymentRequest,
ApplePayLineItem,
ApplePayPaymentContact,
ApplePaySelectionUpdate,
} from 'lib/apple-pay/native';

function getTaxes(billingContact: ApplePayPaymentContact | undefined): ApplePaySelectionUpdate | void {
if (billingContact?.postalCode === '12345') {
return { newLineItems: [{ label: 'Tax', amount: '1.00' }] };
}
}

export default function applePay() {
const applePaySimple = recurly.ApplePay({
Expand Down Expand Up @@ -45,6 +56,9 @@ export default function applePay() {
const applePay = recurly.ApplePay({
country: 'US',
currency: 'USD',
callbacks: {
onPaymentMethodSelected: ({ paymentMethod: { billingContact } }) => getTaxes(billingContact),
},
paymentRequest,
});

Expand Down
Loading