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 1 commit
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
Next Next commit
feat(apple pay): add support for recurringPaymentRequest
Can be specified as the option or will be generated if the `total` line
item is set to `paymentTiming='recurring'`:

```typescript
const total: ApplePayLineItem = {
  label: 'My Subscription',
  paymentTiming: 'recurring',
  amount: '29.00',
  recurringPaymentIntervalUnit: 'month',
  recurringPaymentIntervalCount: 1,
  recurringPaymentStartDate: new Date(),
};

const applePay = recurly.ApplePay({
  total,
  recurringPaymentRequest: {
    paymentDescription: 'A recurring subscription',
    regularBilling: total,
    billingAgreement: 'Will recur forever',
  }
});
```
  • Loading branch information
cbarton committed Apr 3, 2023
commit 196d207cf2eb842d604914fcdae3ae73505a02de
11 changes: 11 additions & 0 deletions lib/recurly/apple-pay/apple-pay.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ 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
Expand Down Expand Up @@ -259,6 +267,7 @@ export class ApplePay extends Emitter {
this.session.completePaymentMethodSelection({
newTotal: this.finalTotalLineItem,
newLineItems: this.lineItems,
...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }),
});
});
}
Expand All @@ -277,6 +286,7 @@ export class ApplePay extends Emitter {
newTotal: this.finalTotalLineItem,
newLineItems: this.lineItems,
newShippingMethods: [],
...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }),
});
});
}
Expand All @@ -294,6 +304,7 @@ export class ApplePay extends Emitter {
newTotal: this.finalTotalLineItem,
newLineItems: this.lineItems,
newShippingMethods: [],
...(this.recurringPaymentRequest && { newRecurringPaymentRequest: this.recurringPaymentRequest }),
});
}

Expand Down
17 changes: 16 additions & 1 deletion lib/recurly/apple-pay/util/build-apple-pay-payment-request.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@ 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;
const { total, lineItems = [], recurringPaymentRequest } = options;
['total', 'lineItems'].forEach(k => delete options[k]);

if (isLineItem(total)) {
paymentRequest.total = total;
if (!recurringPaymentRequest && total.paymentTiming === 'recurring') {
paymentRequest.recurringPaymentRequest = {
paymentDescription: total.label,
regularBilling: total,
};
}
} else if (typeof total === 'object') {
const missing = (total.label === undefined) ? 'label' : 'amount';
return errors('apple-pay-config-missing', { opt: `total.${missing}` });
Expand Down Expand Up @@ -103,6 +109,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"
}
18 changes: 17 additions & 1 deletion test/types/apple-pay.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { ApplePayLineItem } from 'lib/apple-pay/native';

export default function applePay() {
const applePayDeprecated = recurly.ApplePay({
country: 'US',
Expand All @@ -7,10 +9,19 @@ export default function applePay() {
pricing: window.recurly.Pricing.Checkout()
});

const total: ApplePayLineItem = {
label: 'My Subscription',
paymentTiming: 'recurring',
amount: '29.00',
recurringPaymentIntervalUnit: 'month',
recurringPaymentIntervalCount: 1,
recurringPaymentStartDate: new Date(),
};

const applePay = recurly.ApplePay({
country: 'US',
currency: 'USD',
total: { label: 'My Subscription', amount: '29.00' },
total,
lineItems: [{ label: 'Subtotal', amount: '1.00' }],
requiredShippingContactFields: ['email', 'phone'],
billingContact: {
Expand All @@ -26,6 +37,11 @@ export default function applePay() {
phoneNumber: '1231231234',
emailAddress: 'ebrown@example.com'
},
recurringPaymentRequest: {
paymentDescription: 'A recurring subscription',
regularBilling: total,
billingAgreement: 'Will recur forever',
},
pricing: window.recurly.Pricing.Checkout()
});

Expand Down
184 changes: 155 additions & 29 deletions test/unit/apple-pay.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down Expand Up @@ -239,6 +242,7 @@ function applePayTest (integrationType, requestMethod) {
let applePay = this.recurly.ApplePay(options);
applePay.ready(ensureDone(done, () => {
assert.equal(applePay.session.total, options.total);
assert.equal(applePay.session.recurringPaymentRequest, undefined);
}));
});

Expand Down Expand Up @@ -423,6 +427,59 @@ function applePayTest (integrationType, requestMethod) {
});
});

describe('recurringPaymentRequest', function () {
it('is configured when the options.total is a recurring line item', function (done) {
const applePay = this.recurly.ApplePay(merge({}, validOpts, {
total: { label: 'Apple Pay testing', amount: '3.00', paymentTiming: 'recurring' },
}));

applePay.ready(() => {
assert.deepEqual(applePay.session.recurringPaymentRequest, {
paymentDescription: applePay.session.total.label,
regularBilling: applePay.session.total,
managementURL: infoFixture.managementURL,
});
done();
});
});

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, {
recurringPaymentRequest: {
...recurringPaymentRequest,
managementURL: 'https://example.com',
},
}));

applePay.ready(() => {
assert.deepEqual(applePay.session.recurringPaymentRequest, {
...recurringPaymentRequest,
managementURL: 'https://example.com',
});
done();
});
});

it('uses the managementURL from the server', function (done) {
const applePay = this.recurly.ApplePay(merge({}, validOpts, { recurringPaymentRequest, }));

applePay.ready(() => {
assert.deepEqual(applePay.session.recurringPaymentRequest, {
...recurringPaymentRequest,
managementURL: infoFixture.managementURL,
});
done();
});
});
});
});

it('sets other ApplePayPaymentRequest options and does not include configuration options', function (done) {
const applePay = this.recurly.ApplePay(merge({}, validOpts, {
requiredShippingContactFields: ['email'],
Expand Down Expand Up @@ -824,12 +881,35 @@ 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, {
total: { label: 'Total', amount: '3.00', paymentTiming: 'recurring' }
}));

this.applePay.ready(() => {
this.applePay.begin();
done();
});
});

it('includes the newRecurringPaymentRequest', function (done) {
this.applePay.session.on('completePaymentMethodSelection', () => {
assert.notEqual(this.applePay.session.newRecurringPaymentRequest, undefined);
assert.deepEqual(this.applePay.session.newRecurringPaymentRequest, this.applePay.recurringPaymentRequest);
done();
});
this.applePay.session.onpaymentmethodselected({ paymentMethod: { billingContact: { postalCode: '94114' } } });
});
});

describe('with options.pricing set', function () {
beforeEach(function (done) {
this.pricing = this.recurly.Pricing.Checkout();
Expand All @@ -843,10 +923,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');
}));

Expand All @@ -860,10 +940,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({});
});
Expand All @@ -876,6 +957,28 @@ 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, {
total: { label: 'Total', amount: '3.00', paymentTiming: 'recurring' }
}));

this.applePay.ready(() => {
this.applePay.begin();
done();
});
});

it('includes the newRecurringPaymentRequest', function (done) {
this.applePay.session.on('completeShippingContactSelection', () => {
assert.notEqual(this.applePay.session.newRecurringPaymentRequest, undefined);
assert.deepEqual(this.applePay.session.newRecurringPaymentRequest, this.applePay.recurringPaymentRequest);
done();
});
this.applePay.session.onshippingcontactselected({});
});
});

describe('with options.pricing set', function () {
beforeEach(function (done) {
this.pricing = this.recurly.Pricing.Checkout();
Expand All @@ -890,10 +993,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');
}));

Expand All @@ -907,10 +1010,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();
});
Expand All @@ -922,6 +1026,28 @@ 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, {
total: { label: 'Total', amount: '3.00', paymentTiming: 'recurring' }
}));

this.applePay.ready(() => {
this.applePay.begin();
done();
});
});

it('includes the newRecurringPaymentRequest', function (done) {
this.applePay.session.on('completeShippingMethodSelection', () => {
assert.notEqual(this.applePay.session.newRecurringPaymentRequest, undefined);
assert.deepEqual(this.applePay.session.newRecurringPaymentRequest, this.applePay.recurringPaymentRequest);
done();
});
this.applePay.session.onshippingmethodselected({});
});
});
});

describe('onPaymentAuthorized', function () {
Expand Down
Loading