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): add support for recurringPaymentRequest, move to options.paymentRequest #804

Merged
merged 2 commits into from
Apr 4, 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
46 changes: 24 additions & 22 deletions lib/recurly/apple-pay/apple-pay.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ const I18N = {
* @param {Recurly} options.recurly
* @param {String} options.country
* @param {String} options.currency
* @param {String|Object} [options.total] either in dollar format, '1.00', or an ApplePayLineItem object that represents the total for the payment. Optional and discarded if 'pricing' is supplied
* @param {String} [options.label] The short, localized description of the total charge. Deprecated, use 'i18n.totalLineItemLabel' if not using an ApplePayLineItem as the total
* @param {String|Object} [options.total] total for the payment in dollar format, eg: '1.00'. Optional and discarded if 'pricing' is supplied
* @param {String} [options.label] The short, localized description of the total charge. Deprecated, use 'i18n.totalLineItemLabel'
* @param {Boolean} [options.recurring] whether or not the total line item is recurring
* @param {Object} [options.paymentRequest] an ApplePayPaymentRequest
* @param {HTMLElement} [options.form] to provide additional customer data
* @param {Pricing} [options.pricing] to provide line items and total from Pricing
* @param {Boolean} [options.enforceVersion] to ensure that the client supports the minimum version to support required fields
Expand Down Expand Up @@ -79,25 +81,29 @@ export class ApplePay extends Emitter {
return this._session = session;
}

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

/**
* @return {Array} subtotal line items for display on payment sheet
* @private
*/
get lineItems () {
if (!this._ready) return [];

// Clone configured line items
return [].concat(this._paymentRequest.lineItems);
return this._paymentRequest?.lineItems ? [].concat(this._paymentRequest.lineItems) : [];
}

/**
* @return {Object} total cost line item
* @private
*/
get totalLineItem () {
if (!this._ready) return {};

return this._paymentRequest.total;
return this._paymentRequest?.total ? this._paymentRequest.total : {};
}

/**
Expand Down Expand Up @@ -127,30 +133,23 @@ export class ApplePay extends Emitter {
* @private
*/
configure (options) {
if (options.recurly) {
this.recurly = options.recurly;
delete options.recurly;
} else return this.initError = this.error('apple-pay-factory-only');

if (options.form) {
this.config.form = options.form;
delete options.form;
}
if (options.recurly) this.recurly = options.recurly;
else return this.initError = this.error('apple-pay-factory-only');

if (options.form) this.config.form = options.form;
if (options.recurring) this.config.recurring = options.recurring;
Copy link
Member

@chrissrogers chrissrogers Apr 4, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this being a boolean will prevent it from setting through consistently when the option is provided. The side-effect of this will result in success, since false and undefined are functionally equivalent to the way this option is interpreted.

Simple fix would be to do if ('recurring' in options) /* ... */ or if (typeof options.recurring !== 'undefined')


this.config.i18n = {
...I18N,
...(options.label && { totalLineItemLabel: options.label }),
...options.i18n,
};
delete options.label;
delete options.i18n;

if (options.pricing instanceof PricingPromise) {
this.config.pricing = options.pricing.pricing;
} else if (options.pricing instanceof Pricing) {
this.config.pricing = options.pricing;
}
delete options.pricing;

buildApplePayPaymentRequest(this, options, (err, paymentRequest) => {
if (err) return this.initError = this.error(err);
Expand Down Expand Up @@ -190,9 +189,9 @@ export class ApplePay extends Emitter {
* @private
*/
onPricingChange () {
const { pricing } = this.config;
const { pricing, recurring } = this.config;

this._paymentRequest.total = lineItem(this.config.i18n.totalLineItemLabel, pricing.totalNow);
this._paymentRequest.total = lineItem(this.config.i18n.totalLineItemLabel, pricing.totalNow, { recurring });
this._paymentRequest.lineItems = [];

if (!pricing.hasPrice) return;
Expand Down Expand Up @@ -259,6 +258,7 @@ export class ApplePay extends Emitter {
this.session.completePaymentMethodSelection({
newTotal: this.finalTotalLineItem,
newLineItems: this.lineItems,
...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }),
});
});
}
Expand All @@ -277,6 +277,7 @@ export class ApplePay extends Emitter {
newTotal: this.finalTotalLineItem,
newLineItems: this.lineItems,
newShippingMethods: [],
...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }),
});
});
}
Expand All @@ -294,6 +295,7 @@ export class ApplePay extends Emitter {
newTotal: this.finalTotalLineItem,
newLineItems: this.lineItems,
newShippingMethods: [],
...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }),
});
}

Expand Down
17 changes: 6 additions & 11 deletions lib/recurly/apple-pay/util/apple-pay-line-item.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,10 @@ import decimalize from '../../../util/decimalize';
* @param {Number} amount
* @return {object}
*/
export function lineItem (label = '', amount = 0) {
return { label, amount: decimalize(amount) };
}

/**
* Determine if the val is a valid ApplePayLineItem
* @param {object} val
* @return {boolean}
*/
export function isLineItem (val) {
return typeof val === 'object' && val.label !== undefined && val.amount !== undefined;
export function lineItem (label = '', amount = 0, { recurring = false } = {}) {
return {
label,
amount: decimalize(amount),
...(recurring && { paymentTiming: 'recurring' }),
};
}
61 changes: 31 additions & 30 deletions lib/recurly/apple-pay/util/build-apple-pay-payment-request.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,25 @@
import intersection from 'intersect';
import errors from '../../errors';
import filterSupportedNetworks from './filter-supported-networks';
import { lineItem, isLineItem } from './apple-pay-line-item';
import { lineItem } from './apple-pay-line-item';

const REQUIRED_SHIPPING_FIELDS_VERSION = 6;

function buildOrder (config, options, paymentRequest) {
if (!options.total) return errors('apple-pay-config-missing', { opt: 'total' });

const { total, lineItems = [] } = options;
['total', 'lineItems'].forEach(k => delete options[k]);

if (isLineItem(total)) {
paymentRequest.total = total;
} else if (typeof total === 'object') {
const missing = (total.label === undefined) ? 'label' : 'amount';
return errors('apple-pay-config-missing', { opt: `total.${missing}` });
} else {
paymentRequest.total = lineItem(config.i18n.totalLineItemLabel, total);
if (!paymentRequest.total && !options.total) return errors('apple-pay-config-missing', { opt: 'total' });

let { total: totalLineItem } = paymentRequest;
if (!totalLineItem) {
const { recurring, total } = options;
paymentRequest.total = totalLineItem = lineItem(config.i18n.totalLineItemLabel, total, { recurring });
}

paymentRequest.lineItems = lineItems;
if (!paymentRequest.recurringPaymentRequest && totalLineItem.paymentTiming === 'recurring') {
paymentRequest.recurringPaymentRequest = {
paymentDescription: totalLineItem.label,
regularBilling: totalLineItem,
};
}
}

/**
Expand All @@ -38,33 +37,26 @@ function buildOrder (config, options, paymentRequest) {
*/
export default function buildApplePayPaymentRequest (applePay, options, cb) {
const { recurly, config } = applePay;

const { currency: currencyCode, country: countryCode } = options;
if (!currencyCode) return cb(errors('apple-pay-config-missing', { opt: 'currency' }));
if (!countryCode) return cb(errors('apple-pay-config-missing', { opt: 'country' }));
['currency', 'country'].forEach(k => delete options[k]);

let paymentRequest = {
currencyCode,
countryCode,
const paymentRequest = {
currencyCode: options.currency,
countryCode: options.country,
requiredShippingContactFields: options.requiredShippingContactFields, // deprecated
...options.paymentRequest,
requiredBillingContactFields: ['postalAddress'],
};

if (!paymentRequest.currencyCode) return cb(errors('apple-pay-config-missing', { opt: 'currency' }));
if (!paymentRequest.countryCode) return cb(errors('apple-pay-config-missing', { opt: 'country' }));

// The order is handled by pricing if set
if (!config.pricing) {
const error = buildOrder(config, options, paymentRequest);
if (error) return cb(error);
}

if (options.enforceVersion &&
options.requiredShippingContactFields &&
paymentRequest.requiredShippingContactFields &&
!window.ApplePaySession.supportsVersion(REQUIRED_SHIPPING_FIELDS_VERSION)) return cb(errors('apple-pay-not-supported'));
delete options.enforceVersion;

paymentRequest = {
...options,
...paymentRequest,
};

recurly.request.get({
route: '/apple_pay/info',
Expand Down Expand Up @@ -103,6 +95,15 @@ function applyRemoteConfig (paymentRequest, cb) {
: info.supportedNetworks;
paymentRequest.supportedNetworks = filterSupportedNetworks(supportedNetworks);

const { recurringPaymentRequest } = paymentRequest;
if (recurringPaymentRequest) {
if (!recurringPaymentRequest.managementURL && !info.managementURL) {
return cb(errors('apple-pay-config-invalid', { opt: 'recurringPaymentRequest.managementURL' }));
} else if (!recurringPaymentRequest.managementURL) {
recurringPaymentRequest.managementURL = info.managementURL;
}
}

cb(null, paymentRequest);
};
}
3 changes: 2 additions & 1 deletion test/server/fixtures/apple_pay/info.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@
"countries": ["US", "CA"],
"currencies": ["USD", "CAD"],
"merchantCapabilities": ["supportsCredit", "supports3DS", "supportsDebit"],
"supportedNetworks": ["visa", "amex"]
"supportedNetworks": ["visa", "amex"],
"managementURL": "https://example.com/account"
}
31 changes: 25 additions & 6 deletions test/types/apple-pay.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { ApplePayPaymentRequest, ApplePayLineItem } from 'lib/apple-pay/native';

export default function applePay() {
const applePayDeprecated = recurly.ApplePay({
const applePaySimple = recurly.ApplePay({
country: 'US',
currency: 'USD',
label: 'My Subscription',
total: '29.00',
pricing: window.recurly.Pricing.Checkout()
});

const applePay = recurly.ApplePay({
country: 'US',
currency: 'USD',
total: { label: 'My Subscription', amount: '29.00' },
const total: ApplePayLineItem = {
label: 'My Subscription',
paymentTiming: 'recurring',
amount: '29.00',
recurringPaymentIntervalUnit: 'month',
recurringPaymentIntervalCount: 1,
recurringPaymentStartDate: new Date(),
};

const paymentRequest: ApplePayPaymentRequest = {
total,
lineItems: [{ label: 'Subtotal', amount: '1.00' }],
requiredShippingContactFields: ['email', 'phone'],
billingContact: {
Expand All @@ -26,7 +35,17 @@ export default function applePay() {
phoneNumber: '1231231234',
emailAddress: 'ebrown@example.com'
},
pricing: window.recurly.Pricing.Checkout()
recurringPaymentRequest: {
paymentDescription: 'A recurring subscription',
regularBilling: total,
billingAgreement: 'Will recur forever',
},
};

const applePay = recurly.ApplePay({
country: 'US',
currency: 'USD',
paymentRequest,
});

applePay.ready(() => {});
Expand Down
Loading