diff --git a/lib/recurly/apple-pay/apple-pay.js b/lib/recurly/apple-pay/apple-pay.js index ff997096d..c6ef66376 100644 --- a/lib/recurly/apple-pay/apple-pay.js +++ b/lib/recurly/apple-pay/apple-pay.js @@ -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 @@ -79,15 +81,21 @@ 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) : []; } /** @@ -95,9 +103,7 @@ export class ApplePay extends Emitter { * @private */ get totalLineItem () { - if (!this._ready) return {}; - - return this._paymentRequest.total; + return this._paymentRequest?.total ? this._paymentRequest.total : {}; } /** @@ -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; 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); @@ -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; @@ -259,6 +258,7 @@ export class ApplePay extends Emitter { this.session.completePaymentMethodSelection({ newTotal: this.finalTotalLineItem, newLineItems: this.lineItems, + ...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }), }); }); } @@ -277,6 +277,7 @@ export class ApplePay extends Emitter { newTotal: this.finalTotalLineItem, newLineItems: this.lineItems, newShippingMethods: [], + ...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }), }); }); } @@ -294,6 +295,7 @@ export class ApplePay extends Emitter { newTotal: this.finalTotalLineItem, newLineItems: this.lineItems, newShippingMethods: [], + ...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }), }); } diff --git a/lib/recurly/apple-pay/util/apple-pay-line-item.js b/lib/recurly/apple-pay/util/apple-pay-line-item.js index 4e2a9fce9..f16c6afd7 100644 --- a/lib/recurly/apple-pay/util/apple-pay-line-item.js +++ b/lib/recurly/apple-pay/util/apple-pay-line-item.js @@ -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' }), + }; } diff --git a/lib/recurly/apple-pay/util/build-apple-pay-payment-request.js b/lib/recurly/apple-pay/util/build-apple-pay-payment-request.js index 15b7c6050..21d97378b 100644 --- a/lib/recurly/apple-pay/util/build-apple-pay-payment-request.js +++ b/lib/recurly/apple-pay/util/build-apple-pay-payment-request.js @@ -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, + }; + } } /** @@ -38,18 +37,17 @@ 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); @@ -57,14 +55,8 @@ export default function buildApplePayPaymentRequest (applePay, options, cb) { } 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', @@ -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); }; } diff --git a/test/server/fixtures/apple_pay/info.json b/test/server/fixtures/apple_pay/info.json index e7d0c258f..667ff5688 100644 --- a/test/server/fixtures/apple_pay/info.json +++ b/test/server/fixtures/apple_pay/info.json @@ -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" } diff --git a/test/types/apple-pay.ts b/test/types/apple-pay.ts index 18d4da3e3..7fe05c486 100644 --- a/test/types/apple-pay.ts +++ b/test/types/apple-pay.ts @@ -1,5 +1,7 @@ +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', @@ -7,10 +9,17 @@ export default function applePay() { 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: { @@ -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(() => {}); diff --git a/test/unit/apple-pay.test.js b/test/unit/apple-pay.test.js index 05370ba25..6a2dfcac1 100644 --- a/test/unit/apple-pay.test.js +++ b/test/unit/apple-pay.test.js @@ -29,21 +29,24 @@ class ApplePaySessionStub extends Emitter { this.merchantSession = ms; this.emit('completeMerchantValidation'); } - completePaymentMethodSelection ({ newTotal: t, newLineItems: li }) { - this.total = t; - this.lineItems = li; + completePaymentMethodSelection ({ newTotal: t, newLineItems: li, newRecurringPaymentRequest: r }) { + this.newTotal = t; + this.newLineItems = li; + this.newRecurringPaymentRequest = r this.emit('completePaymentMethodSelection'); } - completeShippingContactSelection ({ newTotal: t, newLineItems: li, newShippingMethods: sm }) { - this.shippingMethods = sm; - this.total = t; - this.lineItems = li; + completeShippingContactSelection ({ newTotal: t, newLineItems: li, newRecurringPaymentRequest: r, newShippingMethods: sm }) { + this.newShippingMethods = sm; + this.newTotal = t; + this.newLineItems = li; + this.newRecurringPaymentRequest = r this.emit('completeShippingContactSelection'); } - completeShippingMethodSelection ({ newTotal: t, newLineItems: li, newShippingMethods: sm }) { - this.shippingMethods = sm; - this.total = t; - this.lineItems = li; + completeShippingMethodSelection ({ newTotal: t, newLineItems: li, newRecurringPaymentRequest: r, newShippingMethods: sm }) { + this.newShippingMethods = sm; + this.newTotal = t; + this.newLineItems = li; + this.newRecurringPaymentRequest = r this.emit('completeShippingMethodSelection'); } completePayment ({ status }) { @@ -233,21 +236,13 @@ function applePayTest (integrationType, requestMethod) { })); }); - it('uses options.total as the total line item', function (done) { + it('uses options.paymentRequest.total as the total line item', function (done) { let options = omit(validOpts, 'total'); - options.total = { label: 'Subscription', amount: '10.00' }; + options.paymentRequest = { total: { label: 'Subscription', amount: '10.00' }, }; let applePay = this.recurly.ApplePay(options); applePay.ready(ensureDone(done, () => { - assert.equal(applePay.session.total, options.total); - })); - }); - - it('uses options.lineItems as the line items', function (done) { - let options = clone(validOpts); - options.lineItems = [{ label: 'Taxes', amount: '10.00' }, { label: 'Discount', amount: '-10.00' }]; - let applePay = this.recurly.ApplePay(options); - applePay.ready(ensureDone(done, () => { - assert.equal(applePay.session.lineItems, options.lineItems); + assert.equal(applePay.session.total, options.paymentRequest.total); + assert.equal(applePay.session.recurringPaymentRequest, undefined); })); }); }); @@ -399,7 +394,7 @@ function applePayTest (integrationType, requestMethod) { it('returns an initError if the browser version for requiredShippingContactFields is not met', function (done) { this.sandbox.stub(ApplePaySessionStub, 'version').value(4); let applePay = this.recurly.ApplePay(merge({}, validOpts, { - enforceVersion: true, requiredShippingContactFields: ['email'] + enforceVersion: true, paymentRequest: { requiredShippingContactFields: ['email'] }, })); applePay.on('error', (err) => { @@ -413,20 +408,85 @@ function applePayTest (integrationType, requestMethod) { it('sets requiredShippingContactFields if the browser version is met', function (done) { this.sandbox.stub(ApplePaySessionStub, 'version').value(14); let applePay = this.recurly.ApplePay(merge({}, validOpts, { - enforceVersion: true, requiredShippingContactFields: ['email'] + enforceVersion: true, paymentRequest: { requiredShippingContactFields: ['email'] }, })); applePay.ready(ensureDone(done, () => { assert.deepEqual(applePay.session.requiredShippingContactFields, ['email']); - assert.equal(applePay.session.enforceVersion, undefined); })); }); }); + describe('recurringPaymentRequest', function () { + it('is configured when the options.total is a recurring line item', function (done) { + const applePay = this.recurly.ApplePay(merge({}, validOpts, { + paymentRequest: { total: { label: 'Apple Pay testing', amount: '3.00', paymentTiming: 'recurring' }, }, + })); + + applePay.ready(ensureDone(done, () => { + assert.deepEqual(applePay.session.recurringPaymentRequest, { + paymentDescription: applePay.session.total.label, + regularBilling: applePay.session.total, + managementURL: infoFixture.managementURL, + }); + })); + }); + + it('is configured when the options.recurring is set', function (done) { + const applePay = this.recurly.ApplePay(merge({}, validOpts, { total: '3.00', recurring: true, })); + + applePay.ready(ensureDone(done, () => { + assert.deepEqual(applePay.session.recurringPaymentRequest, { + paymentDescription: applePay.session.total.label, + regularBilling: applePay.session.total, + managementURL: infoFixture.managementURL, + }); + })); + }); + + describe('when options.recurringPaymentRequest is provided', function () { + const recurringPaymentRequest = { + paymentDescription: 'Recurring Test', + regularBilling: { label: 'Total', amount: '3.00', paymentTiming: 'recurring' }, + }; + + it('uses it as the recurringPaymentRequest', function (done) { + const applePay = this.recurly.ApplePay(merge({}, validOpts, { + paymentRequest: { + recurringPaymentRequest: { + ...recurringPaymentRequest, + managementURL: 'https://example.com', + }, + }, + })); + + applePay.ready(ensureDone(done, () => { + assert.deepEqual(applePay.session.recurringPaymentRequest, { + ...recurringPaymentRequest, + managementURL: 'https://example.com', + }); + })); + }); + + it('uses the managementURL from the server', function (done) { + const applePay = this.recurly.ApplePay(merge({}, validOpts, { paymentRequest: { recurringPaymentRequest, } })); + + applePay.ready(ensureDone(done,() => { + assert.deepEqual(applePay.session.recurringPaymentRequest, { + ...recurringPaymentRequest, + managementURL: infoFixture.managementURL, + }); + })); + }); + }); + }); + it('sets other ApplePayPaymentRequest options and does not include configuration options', function (done) { const applePay = this.recurly.ApplePay(merge({}, validOpts, { - requiredShippingContactFields: ['email'], - supportedCountries: ['US'], + paymentRequest: { + requiredShippingContactFields: ['email'], + supportedCountries: ['US'], + }, })); applePay.ready(ensureDone(done, () => { @@ -434,8 +494,6 @@ function applePayTest (integrationType, requestMethod) { assert.deepEqual(applePay.session.supportedCountries, ['US']); assert.equal(applePay.session.currencyCode, validOpts.currency); assert.equal(applePay.session.countryCode, validOpts.country); - assert.equal(applePay.session.currency, undefined); - assert.equal(applePay.session.country, undefined); assert.equal(applePay.session.form, undefined); })); }); @@ -465,7 +523,7 @@ function applePayTest (integrationType, requestMethod) { it('limits the supportedNetworks to the configuration', function (done) { const applePay = this.recurly.ApplePay(merge({}, validOpts, { - supportedNetworks: ['visa'], + paymentRequest: { supportedNetworks: ['visa'], }, })); applePay.ready(ensureDone(done, () => { assert.deepEqual(applePay.session.supportedNetworks, ['visa']); @@ -522,7 +580,7 @@ function applePayTest (integrationType, requestMethod) { }; const pricing = this.recurly.Pricing.Checkout(); pricing.address(form).done(() => { - const applePay = this.recurly.ApplePay(merge({}, validOpts, { form, pricing, billingContact })); + const applePay = this.recurly.ApplePay(merge({}, validOpts, { form, pricing, paymentRequest: { billingContact } })); applePay.ready(ensureDone(done, () => { assert.deepEqual(applePay.session.billingContact, billingContact); assert.equal(applePay.session.shippingContact, undefined); @@ -620,7 +678,7 @@ function applePayTest (integrationType, requestMethod) { const pricing = this.recurly.Pricing.Checkout(); pricing.shippingAddress(form).done(() => { - const applePay = this.recurly.ApplePay(merge({}, validOpts, { form, pricing, shippingContact })); + const applePay = this.recurly.ApplePay(merge({}, validOpts, { form, pricing, paymentRequest: { shippingContact } })); applePay.ready(ensureDone(done, () => { assert.deepEqual(applePay.session.shippingContact, shippingContact); })); @@ -824,12 +882,30 @@ function applePayTest (integrationType, requestMethod) { describe('onPaymentMethodSelected', function () { it('calls ApplePaySession.completePaymentSelection with a total and line items', function (done) { this.applePay.session.on('completePaymentMethodSelection', ensureDone(done, () => { - assert.deepEqual(this.applePay.session.total, this.applePay.finalTotalLineItem); - assert.deepEqual(this.applePay.session.lineItems, this.applePay.lineItems); + assert.deepEqual(this.applePay.session.newTotal, this.applePay.finalTotalLineItem); + assert.deepEqual(this.applePay.session.newLineItems, this.applePay.lineItems); + assert.equal(this.applePay.session.newRecurringPaymentRequest, undefined); })); this.applePay.session.onpaymentmethodselected({ paymentMethod: { billingContact: { postalCode: '94114' } } }); }); + describe('with options.recurringPaymentRequest set', function () { + beforeEach(function (done) { + this.applePay = this.recurly.ApplePay(merge({}, validOpts, { recurring: true })); + this.applePay.ready(ensureDone(done, () => { + this.applePay.begin(); + })); + }); + + it('includes the newRecurringPaymentRequest', function (done) { + this.applePay.session.on('completePaymentMethodSelection', ensureDone(done, () => { + assert.notEqual(this.applePay.session.newRecurringPaymentRequest, undefined); + assert.deepEqual(this.applePay.session.newRecurringPaymentRequest, this.applePay.recurringPaymentRequest); + })); + this.applePay.session.onpaymentmethodselected({ paymentMethod: { billingContact: { postalCode: '94114' } } }); + }); + }); + describe('with options.pricing set', function () { beforeEach(function (done) { this.pricing = this.recurly.Pricing.Checkout(); @@ -843,10 +919,10 @@ function applePayTest (integrationType, requestMethod) { const spy = this.sandbox.spy(this.pricing, 'reprice'); this.applePay.session.on('completePaymentMethodSelection', ensureDone(done, () => { assert.deepEqual(this.pricing.items.address, { postal_code: '94110', country: 'US' }); - assert.deepEqual(this.applePay.session.total, this.applePay.finalTotalLineItem); - assert.deepEqual(this.applePay.session.lineItems, this.applePay.lineItems); - assert.equal(this.applePay.session.lineItems[1].label, 'Tax'); - assert.equal(this.applePay.session.lineItems[1].amount, this.pricing.price.now.taxes); + assert.deepEqual(this.applePay.session.newTotal, this.applePay.finalTotalLineItem); + assert.deepEqual(this.applePay.session.newLineItems, this.applePay.lineItems); + assert.equal(this.applePay.session.newLineItems[1].label, 'Tax'); + assert.equal(this.applePay.session.newLineItems[1].amount, this.pricing.price.now.taxes); assert(spy.called, 'should have repriced'); })); @@ -860,10 +936,11 @@ function applePayTest (integrationType, requestMethod) { describe('onShippingContactSelected', function () { it('calls ApplePaySession.completeShippingContactSelection with empty methods, a total, and line items', function (done) { this.applePay.session.on('completeShippingContactSelection', ensureDone(done, () => { - assert(Array.isArray(this.applePay.session.shippingMethods)); - assert.equal(this.applePay.session.shippingMethods.length, 0); - assert.deepEqual(this.applePay.session.total, this.applePay.finalTotalLineItem); - assert.deepEqual(this.applePay.session.lineItems, this.applePay.lineItems); + assert(Array.isArray(this.applePay.session.newShippingMethods)); + assert.equal(this.applePay.session.newShippingMethods.length, 0); + assert.deepEqual(this.applePay.session.newTotal, this.applePay.finalTotalLineItem); + assert.deepEqual(this.applePay.session.newLineItems, this.applePay.lineItems); + assert.equal(this.applePay.session.newRecurringPaymentRequest, undefined); })); this.applePay.session.onshippingcontactselected({}); }); @@ -876,6 +953,23 @@ function applePayTest (integrationType, requestMethod) { this.applePay.session.onshippingcontactselected(example); }); + describe('with options.recurringPaymentRequest set', function () { + beforeEach(function (done) { + this.applePay = this.recurly.ApplePay(merge({}, validOpts, { recurring: true })); + this.applePay.ready(ensureDone(done, () => { + this.applePay.begin(); + })); + }); + + it('includes the newRecurringPaymentRequest', function (done) { + this.applePay.session.on('completeShippingContactSelection', ensureDone(done, () => { + assert.notEqual(this.applePay.session.newRecurringPaymentRequest, undefined); + assert.deepEqual(this.applePay.session.newRecurringPaymentRequest, this.applePay.recurringPaymentRequest); + })); + this.applePay.session.onshippingcontactselected({}); + }); + }); + describe('with options.pricing set', function () { beforeEach(function (done) { this.pricing = this.recurly.Pricing.Checkout(); @@ -890,10 +984,10 @@ function applePayTest (integrationType, requestMethod) { this.applePay.session.on('completeShippingContactSelection', ensureDone(done, () => { assert.deepEqual(this.pricing.items.shippingAddress, { postal_code: '94110', country: 'US' }); - assert.deepEqual(this.applePay.session.total, this.applePay.finalTotalLineItem); - assert.deepEqual(this.applePay.session.lineItems, this.applePay.lineItems); - assert.equal(this.applePay.session.lineItems[1].label, 'Tax'); - assert.equal(this.applePay.session.lineItems[1].amount, this.pricing.price.now.taxes); + assert.deepEqual(this.applePay.session.newTotal, this.applePay.finalTotalLineItem); + assert.deepEqual(this.applePay.session.newLineItems, this.applePay.lineItems); + assert.equal(this.applePay.session.newLineItems[1].label, 'Tax'); + assert.equal(this.applePay.session.newLineItems[1].amount, this.pricing.price.now.taxes); assert(spy.called, 'should have repriced'); })); @@ -907,10 +1001,11 @@ function applePayTest (integrationType, requestMethod) { describe('onShippingMethodSelected', function () { it('calls ApplePaySession.completeShippingMethodSelection with status, a total, and line items', function (done) { this.applePay.session.on('completeShippingMethodSelection', ensureDone(done, () => { - assert(Array.isArray(this.applePay.session.shippingMethods)); - assert.equal(this.applePay.session.shippingMethods.length, 0); - assert.deepEqual(this.applePay.session.total, this.applePay.finalTotalLineItem); - assert.deepEqual(this.applePay.session.lineItems, this.applePay.lineItems); + assert(Array.isArray(this.applePay.session.newShippingMethods)); + assert.equal(this.applePay.session.newShippingMethods.length, 0); + assert.deepEqual(this.applePay.session.newTotal, this.applePay.finalTotalLineItem); + assert.deepEqual(this.applePay.session.newLineItems, this.applePay.lineItems); + assert.equal(this.applePay.session.newRecurringPaymentRequest, undefined); })); this.applePay.session.onshippingmethodselected(); }); @@ -922,6 +1017,23 @@ function applePayTest (integrationType, requestMethod) { })); this.applePay.session.onshippingmethodselected(example); }); + + describe('with options.recurringPaymentRequest set', function () { + beforeEach(function (done) { + this.applePay = this.recurly.ApplePay(merge({}, validOpts, { recurring: true })); + this.applePay.ready(ensureDone(done, () => { + this.applePay.begin(); + })); + }); + + it('includes the newRecurringPaymentRequest', function (done) { + this.applePay.session.on('completeShippingMethodSelection', ensureDone(done, () => { + assert.notEqual(this.applePay.session.newRecurringPaymentRequest, undefined); + assert.deepEqual(this.applePay.session.newRecurringPaymentRequest, this.applePay.recurringPaymentRequest); + })); + this.applePay.session.onshippingmethodselected({}); + }); + }); }); describe('onPaymentAuthorized', function () { @@ -1080,10 +1192,9 @@ function applePayTest (integrationType, requestMethod) { this.pricing = this.recurly.Pricing.Checkout(); this.applePay = this.recurly.ApplePay(merge({}, validOpts, { pricing: this.pricing })); this.pricing[addressType]({ postalCode: '91411', countryCode: 'US' }).done(() => { - this.applePay.ready(() => { + this.applePay.ready(ensureDone(done,() => { this.applePay.begin(); - done(); - }); + })); }); }); diff --git a/types/lib/apple-pay/index.d.ts b/types/lib/apple-pay/index.d.ts index 372d9b418..9713c3c70 100644 --- a/types/lib/apple-pay/index.d.ts +++ b/types/lib/apple-pay/index.d.ts @@ -1,17 +1,45 @@ import { Emitter } from '../emitter'; import { CheckoutPricingInstance, CheckoutPricingPromise } from '../pricing/checkout'; -import { ApplePayPaymentRequest, ApplePayLineItem } from './native'; +import { ApplePayPaymentRequest } from './native'; + +export type I18n = { + /** + * The short, localized description of the subtotal line item + */ + subtotalLineItemLabel: string; + + /** + * The short, localized description of the total line item + */ + totalLineItemLabel: string; + + /** + * The short, localized description of the discount line item + */ + discountLineItemLabel: string; + + /** + * The short, localized description of the tax line item + */ + taxLineItemLabel: string; + + /** + * The short, localized description of the gift card line item + */ + giftCardLineItemLabel: string; +}; export type ApplePayConfig = { /** - * Your ISO 3166 country code (ex: ‘US’). This is your country code as the merchant. + * Your ISO 3166 country code (ex: ‘US’). This is your country code as the merchant. Required if not + * set in `options.paymentRequest.countryCode`. */ - country: string; + country?: string; /** - * ISO 4217 purchase currency (ex: ‘USD’) + * ISO 4217 purchase currency (ex: ‘USD’). Required if not set in `options.paymentRequest.currencyCode`. */ - currency: string; + currency?: string; /** * Purchase description to display in the Apple Pay payment sheet. @@ -19,9 +47,20 @@ export type ApplePayConfig = { label?: string; /** - * Total cost to display in the Apple Pay payment sheet. Required if `options.pricing` is not provided. + * Total cost to display in the Apple Pay payment sheet. Required if `options.pricing` or + * `options.paymentRequest.total` is not provided. + */ + total?: string; + + /** + * Display the recurring payment request on a monthly cadence */ - total?: string | ApplePayLineItem; + recurring?: boolean; + + /** + * `options.pricing` line item descriptions to display in the Apple Pay payment sheet. + */ + i18n?: I18n; /** * If provided, will override `options.total` and provide the current total price on the CheckoutPricing instance @@ -39,7 +78,7 @@ export type ApplePayConfig = { form?: HTMLFormElement; /** - * If `options.requiredShippingContactFields` is present, validate that the browser supports the minimum version required for that option. + * If `requiredShippingContactFields` is specified, validate that the browser supports the minimum version required for that option. */ enforceVersion?: boolean; @@ -49,7 +88,12 @@ export type ApplePayConfig = { braintree?: { clientAuthorization: string; }; -} | ApplePayPaymentRequest; + + /** + * The request for a payment. + */ + paymentRequest?: ApplePayPaymentRequest; +}; export type ApplePayEvent = | 'token' diff --git a/types/lib/apple-pay/native.d.ts b/types/lib/apple-pay/native.d.ts index 5c157d34e..3b7ab15f7 100644 --- a/types/lib/apple-pay/native.d.ts +++ b/types/lib/apple-pay/native.d.ts @@ -101,6 +101,40 @@ export type ApplePayLineItem = { recurringPaymentEndDate?: Date; }; +/** + * A dictionary that represents a request to set up a subscription. + */ +export type ApplePayRecurringPaymentRequest = { + /** + * A description of the recurring payment that Apple Pay displays to the user in the payment sheet. + */ + paymentDescription: string; + + /** + * The regular billing cycle for the recurring payment, including start and end dates, an interval, and an interval count. + */ + regularBilling: ApplePayLineItem ; + + /** + * The trial billing cycle for the recurring payment. + */ + trialBilling?: ApplePayLineItem ; + + /** + * A localized billing agreement that the payment sheet displays to the user before the user authorizes the payment. + */ + billingAgreement?: string; + + /** + * A URL to a web page where the user can update or delete the payment method for the recurring payment. + * Defaults to the managment URL set in the Recurly Apple Pay configuration. + */ + managementURL?: string; +}; + +/** + * A request for payment, which includes information about payment-processing capabilities, the payment amount, and shipping information + */ export type ApplePayPaymentRequest = { /** * Total cost to display in the Apple Pay payment sheet. Required if `options.pricing` is not provided. @@ -132,4 +166,9 @@ export type ApplePayPaymentRequest = { * A set of line items that explain recurring payments and additional charges and discounts. */ lineItems?: ApplePayLineItem[]; + + /** + * A property that requests a subscription. + */ + recurringPaymentRequest?: ApplePayRecurringPaymentRequest; };