From ebc042913a5b934a78d78a2079e7368ab4971b1e Mon Sep 17 00:00:00 2001 From: Chris Barton Date: Thu, 22 Aug 2024 17:34:01 -0700 Subject: [PATCH] feat: add Google Pay through Braintree To use Google Pay through Braintree, ensure that you have enabled Google Pay on a Braintree gateway in the Recurly Admin UI and on your Braintree account. Then, initialize `recurly.GooglePay(...)` with the `braintree.clientAuthorization` option: ```js const googlePay = recurly.GooglePay({ ...options, gatewayCode: '', braintree: { clientAuthorization: '', }, }) ``` --- .../google-pay/google-pay.braintree.js | 64 + lib/recurly/google-pay/index.js | 5 +- test/unit/google-pay/google-pay.test.js | 1200 +++++++++-------- types/lib/google-pay/index.d.ts | 7 + 4 files changed, 721 insertions(+), 555 deletions(-) create mode 100644 lib/recurly/google-pay/google-pay.braintree.js diff --git a/lib/recurly/google-pay/google-pay.braintree.js b/lib/recurly/google-pay/google-pay.braintree.js new file mode 100644 index 00000000..69677f41 --- /dev/null +++ b/lib/recurly/google-pay/google-pay.braintree.js @@ -0,0 +1,64 @@ +import Promise from 'promise'; +import { GooglePay } from './google-pay'; +import Debug from 'debug'; +import BraintreeLoader from '../../util/braintree-loader'; +import errors from '../errors'; +import { payWithGoogle } from './pay-with-google'; + +const debug = Debug('recurly:google-pay:braintree'); + +export class GooglePayBraintree extends GooglePay { + configure (options) { + debug('Initializing client'); + + const authorization = options.braintree.clientAuthorization; + + BraintreeLoader.loadModules('googlePayment', 'dataCollector') + .then(() => window.braintree.client.create({ authorization })) + .then(client => Promise.all([ + window.braintree.dataCollector.create({ client }), + window.braintree.googlePayment.create({ + client, + googlePayVersion: 2, + googleMerchantId: options.googleMerchantId, + }) + ])) + .then(([dataCollector, googlePayment]) => { this.braintree = { dataCollector, googlePayment }; }) + .catch(err => this.emit('error', errors('google-pay-init-error', { err }))) + .then(() => super.configure(options)); + } + + createButton ({ paymentOptions, isReadyToPayRequest, paymentDataRequest: recurlyPaymentDataRequest, buttonOptions }) { + // allow Braintree to set the tokenizationSpecification for its gateway + recurlyPaymentDataRequest.allowedPaymentMethods.forEach(method => { + if (method.tokenizationSpecification) delete method.tokenizationSpecification; + }); + const paymentDataRequest = this.braintree.googlePayment.createPaymentDataRequest(recurlyPaymentDataRequest); + debug('Creating button', recurlyPaymentDataRequest, paymentDataRequest); + return payWithGoogle({ + paymentOptions, + isReadyToPayRequest, + paymentDataRequest, + buttonOptions, + }); + } + + token (paymentData) { + debug('Creating token', paymentData); + + return this.braintree.googlePayment + .parseResponse(paymentData) + .then(token => { + token.deviceData = this.braintree.dataCollector.deviceData; + paymentData.paymentMethodData.gatewayToken = token; + return super.token(paymentData); + }); + } + + mapPaymentData (paymentData) { + return { + type: 'braintree', + ...super.mapPaymentData(paymentData), + }; + } +} diff --git a/lib/recurly/google-pay/index.js b/lib/recurly/google-pay/index.js index 66af41d9..88653241 100644 --- a/lib/recurly/google-pay/index.js +++ b/lib/recurly/google-pay/index.js @@ -1,4 +1,5 @@ import { GooglePay } from './google-pay'; +import { GooglePayBraintree } from './google-pay.braintree'; /** * Returns a GooglePay instance. @@ -7,7 +8,9 @@ import { GooglePay } from './google-pay'; * @return {GooglePay} */ export function factory (options) { - const factoryClass = GooglePay; + const factoryClass = options?.braintree?.clientAuthorization + ? GooglePayBraintree + : GooglePay; return new factoryClass(Object.assign({}, options, { recurly: this })); } diff --git a/test/unit/google-pay/google-pay.test.js b/test/unit/google-pay/google-pay.test.js index 794bd8c9..d611fd30 100644 --- a/test/unit/google-pay/google-pay.test.js +++ b/test/unit/google-pay/google-pay.test.js @@ -1,9 +1,35 @@ - import assert from 'assert'; import recurlyError from '../../../lib/recurly/errors'; +import BraintreeLoader from '../../../lib/util/braintree-loader'; import { initRecurly, nextTick, assertDone, stubGooglePaymentAPI } from '../support/helpers'; import dom from '../../../lib/util/dom'; +const INTEGRATION = { + DIRECT: 'Direct Integration', + BRAINTREE: 'Braintree Integration', +}; + +const getBraintreeStub = () => ({ + client: { + VERSION: '3.101.0', + create: sinon.stub().resolves('CLIENT'), + }, + dataCollector: { + create: sinon.stub().resolves({ deviceData: 'DEVICE_DATA' }), + }, + googlePayment: { + create: sinon.stub().resolves({ + createPaymentDataRequest: sinon.stub().callsFake((paymentRequest) => { + paymentRequest.allowedPaymentMethods.forEach(method => { + method.tokenizationSpecification = { type: 'PAYMENT_GATEWAY', parameters: { gateway: 'braintree' } }; + }); + return paymentRequest; + }), + parseResponse: sinon.stub().resolves({ nonce: 'GATEWAY_TOKEN' }), + }), + }, +}); + describe('Google Pay', function () { beforeEach(function () { this.sandbox = sinon.createSandbox(); @@ -60,688 +86,711 @@ describe('Google Pay', function () { } }); - it('requests to Recurly the merchant Google Pay info with the initial options provided', function (done) { - this.stubRequestAndGoogleApi(); - this.recurly.GooglePay(this.googlePayOpts); - - nextTick(() => assertDone(done, () => { - assert.equal(this.recurly.request.get.called, true); - assert.deepEqual(this.recurly.request.get.getCall(0).args[0], { - route: '/google_pay/info', - data: { - country: 'US', - currency: 'USD', - gateway_code : 'CODE_123', - }, - }); - })); - }); - - context('when missing a required option', function () { - const requiredKeys = ['country', 'currency']; - requiredKeys.forEach(key => { - describe(`:${key}`, function () { - beforeEach(function () { - this.googlePayOpts[key] = undefined; - this.stubRequestAndGoogleApi(); - }); - - it('emits a google-pay-config-missing error', function (done) { - const result = this.recurly.GooglePay(this.googlePayOpts); - - result.on('error', (err) => assertDone(done, () => { - assert.ok(err); - assert.equal(err.code, 'google-pay-config-missing'); - assert.equal(err.message, `Missing Google Pay configuration option: '${key}'`); - })); - }); - - it('do not initiate the pay-with-google nor requests to Recurly the merchant Google Pay info', function (done) { - this.recurly.GooglePay(this.googlePayOpts); - - nextTick(() => assertDone(done, () => { - assert.equal(this.recurly.request.get.called, false); - assert.equal(window.google.payments.api.PaymentsClient.called, false); - })); - }); - - it('do not emit any token nor the on ready event', function (done) { - const result = this.recurly.GooglePay(this.googlePayOpts); + googlePayTest(INTEGRATION.DIRECT); + googlePayTest(INTEGRATION.BRAINTREE); +}); - result.on('ready', () => done(new Error('expected to not emit a ready event'))); - result.on('token', () => done(new Error('expected to not emit a token event'))); - nextTick(done); - }); - }); - }); - }); +function googlePayTest (integrationType) { + const isDirectIntegration = integrationType === INTEGRATION.DIRECT; + const isBraintreeIntegration = integrationType === INTEGRATION.BRAINTREE; - context('when fails requesting to Recurly the merchant Google Pay info', function () { + describe(`Recurly.GooglePay ${integrationType}`, function () { beforeEach(function () { - this.stubRequestOpts.info = Promise.reject(recurlyError('api-error')); - this.stubRequestAndGoogleApi(); - }); - - it('emits an api-error', function (done) { - const result = this.recurly.GooglePay(this.googlePayOpts); - - result.on('error', (err) => assertDone(done, () => { - assert.ok(err); - assert.equal(err.code, 'api-error'); - assert.equal(err.message, 'There was an error with your request.'); - })); + if (isBraintreeIntegration) { + this.googlePayOpts.braintree = { clientAuthorization: 'valid' }; + window.braintree = getBraintreeStub(); + } }); - it('do not initiate the pay-with-google', function (done) { + it('requests to Recurly the merchant Google Pay info with the initial options provided', function (done) { + this.stubRequestAndGoogleApi(); this.recurly.GooglePay(this.googlePayOpts); nextTick(() => assertDone(done, () => { - assert.equal(window.google.payments.api.PaymentsClient.called, false); + assert.equal(this.recurly.request.get.called, true); + assert.deepEqual(this.recurly.request.get.getCall(0).args[0], { + route: '/google_pay/info', + data: { + country: 'US', + currency: 'USD', + gateway_code : 'CODE_123', + }, + }); })); }); - it('do not emit any token nor the on ready event', function (done) { - const result = this.recurly.GooglePay(this.googlePayOpts); - - result.on('ready', () => done(new Error('expected to not emit a ready event'))); - result.on('token', () => done(new Error('expected to not emit a token event'))); - nextTick(done); - }); - }); - - context('when the requested merchant Google Pay info returns an empty list of payment methods', function () { - beforeEach(function () { - this.stubRequestOpts.info = Promise.resolve({ - siteMode: 'test', - paymentMethods: [], - }); - this.stubRequestAndGoogleApi(); - }); + context('when missing a required option', function () { + const requiredKeys = ['country', 'currency']; + requiredKeys.forEach(key => { + describe(`:${key}`, function () { + beforeEach(function () { + this.googlePayOpts[key] = undefined; + this.stubRequestAndGoogleApi(); + }); - it('emits a google-pay-not-configured error', function (done) { - const result = this.recurly.GooglePay(this.googlePayOpts); + it('emits a google-pay-config-missing error', function (done) { + const result = this.recurly.GooglePay(this.googlePayOpts); - result.on('error', (err) => assertDone(done, () => { - assert.ok(err); - assert.equal(err.code, 'google-pay-not-configured'); - assert.equal(err.message, 'There are no Payment Methods enabled to support Google Pay'); - })); - }); + result.on('error', (err) => assertDone(done, () => { + assert.ok(err); + assert.equal(err.code, 'google-pay-config-missing'); + assert.equal(err.message, `Missing Google Pay configuration option: '${key}'`); + })); + }); - it('do not initiate the pay-with-google', function (done) { - this.recurly.GooglePay(this.googlePayOpts); + it('do not initiate the pay-with-google nor requests to Recurly the merchant Google Pay info', function (done) { + this.recurly.GooglePay(this.googlePayOpts); - nextTick(() => assertDone(done, () => { - assert.equal(window.google.payments.api.PaymentsClient.called, false); - })); - }); + nextTick(() => assertDone(done, () => { + assert.equal(this.recurly.request.get.called, false); + assert.equal(window.google.payments.api.PaymentsClient.called, false); + })); + }); - it('do not emit any token nor the on ready event', function (done) { - const result = this.recurly.GooglePay(this.googlePayOpts); + it('do not emit any token nor the on ready event', function (done) { + const result = this.recurly.GooglePay(this.googlePayOpts); - result.on('ready', () => done(new Error('expected to not emit a ready event'))); - result.on('token', () => done(new Error('expected to not emit a token event'))); - nextTick(done); + result.on('ready', () => done(new Error('expected to not emit a ready event'))); + result.on('token', () => done(new Error('expected to not emit a token event'))); + nextTick(done); + }); + }); + }); }); - }); - - context('when the requested merchant Google Pay info returns a valid non-empty list of payment methods', function () { - it('initiates the pay-with-google with the expected Google Pay Configuration', function (done) { - this.stubRequestAndGoogleApi(); - this.recurly.GooglePay(this.googlePayOpts); - nextTick(() => assertDone(done, () => { - assert.equal(window.google.payments.api.PaymentsClient.called, true); - assert.deepEqual(window.google.payments.api.PaymentsClient.getCall(0).args[0], { - environment: 'TEST', - merchantInfo: { - merchantId: 'GOOGLE_MERCHANT_ID_123', - merchantName: 'RECURLY', - }, - paymentDataCallbacks: undefined, - }); - assert.deepEqual(window.google.payments.api.PaymentsClient.prototype.isReadyToPay.getCall(0).args[0], { - apiVersion: 2, - apiVersionMinor: 0, - allowedPaymentMethods: [ - { - type: 'CARD', - parameters: { - allowedCardNetworks: ['VISA'], - allowedAuthMethods: ['PAN_ONLY'], - billingAddressRequired: true, - billingAddressParameters: { - format: 'FULL', - }, - }, - tokenizationSpecification: { - type: 'PAYMENT_GATEWAY', - parameters: 'PAYMENT_GATEWAY_PARAMETERS', - }, - }, - { - type: 'CARD', - parameters: { - allowedCardNetworks: ['MASTERCARD'], - allowedAuthMethods: ['PAN_ONLY'], - billingAddressRequired: true, - billingAddressParameters: { - format: 'FULL', - }, - }, - tokenizationSpecification: { - type: 'DIRECT', - parameters: 'DIRECT_PARAMETERS', - }, - } - ], + if (isBraintreeIntegration) { + describe('when the libs are not loaded', function () { + beforeEach(function () { + delete window.braintree; + this.sandbox.stub(BraintreeLoader, 'loadModules').rejects(new Error('boom')); }); - })); - }); - context('when the site mode is production but an environment option is provided', function () { - beforeEach(function () { - this.stubRequestOpts.info = Promise.resolve({ - siteMode: 'production', - paymentMethods: this.paymentMethods, + it('load the libs', function (done) { + const result = this.recurly.GooglePay(this.googlePayOpts); + result.on('error', (err) => assertDone(done, () => { + assert(BraintreeLoader.loadModules.calledWith('googlePayment', 'dataCollector')); + assert.ok(err); + assert.equal(err.code, 'google-pay-init-error'); + assert.match(err.message, /boom/); + })); }); - this.googlePayOpts.environment = 'TEST'; }); - it('initiates the pay-with-google in the specified environment', function (done) { - this.stubRequestAndGoogleApi(); - this.recurly.GooglePay(this.googlePayOpts); + it('assigns the braintree configuration', function (done) { + const googlePay = this.recurly.GooglePay(this.googlePayOpts); nextTick(() => assertDone(done, () => { - assert.deepEqual(window.google.payments.api.PaymentsClient.getCall(0).args[0].environment, 'TEST'); + assert.ok(googlePay.braintree.dataCollector); + assert.ok(googlePay.braintree.googlePayment); })); }); - }); + } - context('when the site mode is production and no environment option is provided', function () { + context('when fails requesting to Recurly the merchant Google Pay info', function () { beforeEach(function () { - this.stubRequestOpts.info = Promise.resolve({ - siteMode: 'production', - paymentMethods: this.paymentMethods, - }); - this.googlePayOpts.environment = undefined; + this.stubRequestOpts.info = Promise.reject(recurlyError('api-error')); + this.stubRequestAndGoogleApi(); }); - it('initiates the pay-with-google in PRODUCTION mode', function (done) { - this.stubRequestAndGoogleApi(); - this.recurly.GooglePay(this.googlePayOpts); + it('emits an api-error', function (done) { + const result = this.recurly.GooglePay(this.googlePayOpts); - nextTick(() => assertDone(done, () => { - assert.deepEqual(window.google.payments.api.PaymentsClient.getCall(0).args[0].environment, 'PRODUCTION'); + result.on('error', (err) => assertDone(done, () => { + assert.ok(err); + assert.equal(err.code, 'api-error'); + assert.equal(err.message, 'There was an error with your request.'); })); }); - }); - - context('when the site mode is any other than production but an environment option is provided', function () { - beforeEach(function () { - this.stubRequestOpts.info = Promise.resolve({ - siteMode: 'sandbox', - paymentMethods: this.paymentMethods, - }); - this.googlePayOpts.environment = 'PRODUCTION'; - }); - it('initiates the pay-with-google in the specified environment', function (done) { - this.stubRequestAndGoogleApi(); + it('do not initiate the pay-with-google', function (done) { this.recurly.GooglePay(this.googlePayOpts); nextTick(() => assertDone(done, () => { - assert.deepEqual(window.google.payments.api.PaymentsClient.getCall(0).args[0].environment, 'PRODUCTION'); + assert.equal(window.google.payments.api.PaymentsClient.called, false); })); }); + + it('do not emit any token nor the on ready event', function (done) { + const result = this.recurly.GooglePay(this.googlePayOpts); + + result.on('ready', () => done(new Error('expected to not emit a ready event'))); + result.on('token', () => done(new Error('expected to not emit a token event'))); + nextTick(done); + }); }); - context('when the site mode is any other than production and no environment option is provided', function () { + context('when the requested merchant Google Pay info returns an empty list of payment methods', function () { beforeEach(function () { this.stubRequestOpts.info = Promise.resolve({ - siteMode: 'sandbox', - paymentMethods: this.paymentMethods, + siteMode: 'test', + paymentMethods: [], }); - this.googlePayOpts.environment = undefined; + this.stubRequestAndGoogleApi(); }); - it('initiates the pay-with-google in TEST mode', function (done) { - this.stubRequestAndGoogleApi(); - this.recurly.GooglePay(this.googlePayOpts); + it('emits a google-pay-not-configured error', function (done) { + const result = this.recurly.GooglePay(this.googlePayOpts); - nextTick(() => assertDone(done, () => { - assert.deepEqual(window.google.payments.api.PaymentsClient.getCall(0).args[0].environment, 'TEST'); + result.on('error', (err) => assertDone(done, () => { + assert.ok(err); + assert.equal(err.code, 'google-pay-not-configured'); + assert.equal(err.message, 'There are no Payment Methods enabled to support Google Pay'); })); }); - }); - context('options.billingAddressRequired = false', function () { - beforeEach(function () { - this.googlePayOpts.billingAddressRequired = false; - }); - - it('initiates the pay-with-google without the billing address requirement', function (done) { - this.stubRequestAndGoogleApi(); + it('do not initiate the pay-with-google', function (done) { this.recurly.GooglePay(this.googlePayOpts); nextTick(() => assertDone(done, () => { - const { allowedPaymentMethods: [{ parameters }] } = window.google.payments.api.PaymentsClient.prototype.isReadyToPay.getCall(0).args[0]; - assert.deepEqual(parameters.billingAddressRequired, undefined); - assert.deepEqual(parameters.billingAddressParameters, undefined); + assert.equal(window.google.payments.api.PaymentsClient.called, false); })); }); - }); - context('@deprecated options.requireBillingAddress = false', function () { - beforeEach(function () { - delete this.googlePayOpts.billingAddressRequired; - this.googlePayOpts.requireBillingAddress = false; + it('do not emit any token nor the on ready event', function (done) { + const result = this.recurly.GooglePay(this.googlePayOpts); + + result.on('ready', () => done(new Error('expected to not emit a ready event'))); + result.on('token', () => done(new Error('expected to not emit a token event'))); + nextTick(done); }); + }); - it('initiates the pay-with-google without the billing address requirement', function (done) { + context('when the requested merchant Google Pay info returns a valid non-empty list of payment methods', function () { + it('initiates the pay-with-google with the expected Google Pay Configuration', function (done) { this.stubRequestAndGoogleApi(); this.recurly.GooglePay(this.googlePayOpts); nextTick(() => assertDone(done, () => { - const { allowedPaymentMethods: [{ parameters }] } = window.google.payments.api.PaymentsClient.prototype.isReadyToPay.getCall(0).args[0]; - assert.deepEqual(parameters.billingAddressRequired, undefined); - assert.deepEqual(parameters.billingAddressParameters, undefined); + assert.equal(window.google.payments.api.PaymentsClient.called, true); + assert.deepEqual(window.google.payments.api.PaymentsClient.getCall(0).args[0], { + environment: 'TEST', + merchantInfo: { + merchantId: 'GOOGLE_MERCHANT_ID_123', + merchantName: 'RECURLY', + }, + paymentDataCallbacks: undefined, + }); + + if (isDirectIntegration) { + assert.deepEqual(window.google.payments.api.PaymentsClient.prototype.isReadyToPay.getCall(0).args[0], { + apiVersion: 2, + apiVersionMinor: 0, + allowedPaymentMethods: [ + { + type: 'CARD', + parameters: { + allowedCardNetworks: ['VISA'], + allowedAuthMethods: ['PAN_ONLY'], + billingAddressRequired: true, + billingAddressParameters: { + format: 'FULL', + }, + }, + tokenizationSpecification: { + type: 'PAYMENT_GATEWAY', + parameters: 'PAYMENT_GATEWAY_PARAMETERS', + }, + }, + { + type: 'CARD', + parameters: { + allowedCardNetworks: ['MASTERCARD'], + allowedAuthMethods: ['PAN_ONLY'], + billingAddressRequired: true, + billingAddressParameters: { + format: 'FULL', + }, + }, + tokenizationSpecification: { + type: 'DIRECT', + parameters: 'DIRECT_PARAMETERS', + }, + } + ], + }); + } else if (isBraintreeIntegration) { + assert.deepEqual(window.google.payments.api.PaymentsClient.prototype.isReadyToPay.getCall(0).args[0], { + apiVersion: 2, + apiVersionMinor: 0, + allowedPaymentMethods: [ + { + type: 'CARD', + parameters: { + allowedCardNetworks: ['VISA'], + allowedAuthMethods: ['PAN_ONLY'], + billingAddressRequired: true, + billingAddressParameters: { + format: 'FULL', + }, + }, + tokenizationSpecification: { + type: 'PAYMENT_GATEWAY', + parameters: { gateway: 'braintree' }, + }, + }, + { + type: 'CARD', + parameters: { + allowedCardNetworks: ['MASTERCARD'], + allowedAuthMethods: ['PAN_ONLY'], + billingAddressRequired: true, + billingAddressParameters: { + format: 'FULL', + }, + }, + tokenizationSpecification: { + type: 'PAYMENT_GATEWAY', + parameters: { gateway: 'braintree' }, + }, + } + ], + }); + } else { + throw new Error('Invalid integration type'); + } })); }); - }); - context('with options.paymentDataRequest attributes', function () { - it('merges them into the actual paymentDataRequest', function (done) { - this.stubRequestAndGoogleApi(); - const merchantInfo = { - merchantId: 'GOOGLE_MERCHANT_ID_123', - merchantName: 'RECURLY', - }; - const transactionInfo = { - currencyCode: 'USD', - countryCode: 'US', - totalPrice: '1', - }; - - this.recurly.GooglePay({ - ...this.googlePayOpts, - billingAddressRequired: false, - paymentDataRequest: { - emailRequired: true, - shippingAddressRequired: true, - shippingOptionRequired: true, - shippingOptionParameters: [], - shippingAddressParameters: [], - }, + context('when the site mode is production but an environment option is provided', function () { + beforeEach(function () { + this.stubRequestOpts.info = Promise.resolve({ + siteMode: 'production', + paymentMethods: this.paymentMethods, + }); + this.googlePayOpts.environment = 'TEST'; }); - nextTick(() => assertDone(done, () => { - const paymentDataRequest = window.google.payments.api.PaymentsClient.prototype.prefetchPaymentData.getCall(0).args[0]; - assert.deepEqual(paymentDataRequest.merchantInfo, merchantInfo); - assert.deepEqual(paymentDataRequest.transactionInfo, { totalPriceStatus: 'NOT_CURRENTLY_KNOWN', ...transactionInfo }); - assert.equal(paymentDataRequest.emailRequired, true); - assert.equal(paymentDataRequest.shippingAddressRequired, true); - assert.equal(paymentDataRequest.shippingOptionRequired, true); - assert.equal(paymentDataRequest.shippingOptionParameters.length, 0); - assert.equal(paymentDataRequest.shippingAddressParameters.length, 0); - })); + it('initiates the pay-with-google in the specified environment', function (done) { + this.stubRequestAndGoogleApi(); + this.recurly.GooglePay(this.googlePayOpts); + + nextTick(() => assertDone(done, () => { + assert.deepEqual(window.google.payments.api.PaymentsClient.getCall(0).args[0].environment, 'TEST'); + })); + }); }); - it('uses them if not provided at the top level', function (done) { - this.stubRequestAndGoogleApi(); - const merchantInfo = { - merchantId: 'GOOGLE_MERCHANT_ID_123', - merchantName: 'RECURLY', - }; - const transactionInfo = { - currencyCode: 'USD', - countryCode: 'US', - totalPrice: '1', - }; - - this.recurly.GooglePay({ - billingAddressRequired: false, - paymentDataRequest: { - merchantInfo, - transactionInfo, - emailRequired: true, - shippingAddressRequired: true, - shippingOptionRequired: true, - shippingOptionParameters: [], - shippingAddressParameters: [], - }, + context('when the site mode is production and no environment option is provided', function () { + beforeEach(function () { + this.stubRequestOpts.info = Promise.resolve({ + siteMode: 'production', + paymentMethods: this.paymentMethods, + }); + this.googlePayOpts.environment = undefined; }); - nextTick(() => assertDone(done, () => { - const paymentDataRequest = window.google.payments.api.PaymentsClient.prototype.prefetchPaymentData.getCall(0).args[0]; - assert.deepEqual(paymentDataRequest.merchantInfo, merchantInfo); - assert.deepEqual(paymentDataRequest.transactionInfo, { totalPriceStatus: 'NOT_CURRENTLY_KNOWN', ...transactionInfo }); - assert.equal(paymentDataRequest.emailRequired, true); - assert.equal(paymentDataRequest.shippingAddressRequired, true); - assert.equal(paymentDataRequest.shippingOptionRequired, true); - assert.equal(paymentDataRequest.shippingOptionParameters.length, 0); - assert.equal(paymentDataRequest.shippingAddressParameters.length, 0); - })); + it('initiates the pay-with-google in PRODUCTION mode', function (done) { + this.stubRequestAndGoogleApi(); + this.recurly.GooglePay(this.googlePayOpts); + + nextTick(() => assertDone(done, () => { + assert.deepEqual(window.google.payments.api.PaymentsClient.getCall(0).args[0].environment, 'PRODUCTION'); + })); + }); }); - }); - context('with options.callbacks', function () { - it('handles the shipping address intent if onPaymentDataChanged is provided and requiring shipping address', function (done) { - this.stubRequestAndGoogleApi(); - const callbacks = { onPaymentDataChanged: () => {} }; - this.recurly.GooglePay({ - ...this.googlePayOpts, - callbacks, - paymentDataRequest: { - shippingAddressRequired: true, - }, + context('when the site mode is any other than production but an environment option is provided', function () { + beforeEach(function () { + this.stubRequestOpts.info = Promise.resolve({ + siteMode: 'sandbox', + paymentMethods: this.paymentMethods, + }); + this.googlePayOpts.environment = 'PRODUCTION'; }); - nextTick(() => assertDone(done, () => { - assert.equal(window.google.payments.api.PaymentsClient.getCall(0).args[0].paymentDataCallbacks, callbacks); - const { callbackIntents } = window.google.payments.api.PaymentsClient.prototype.prefetchPaymentData.getCall(0).args[0]; - assert.deepEqual(callbackIntents, ['SHIPPING_ADDRESS']); - })); + it('initiates the pay-with-google in the specified environment', function (done) { + this.stubRequestAndGoogleApi(); + this.recurly.GooglePay(this.googlePayOpts); + + nextTick(() => assertDone(done, () => { + assert.deepEqual(window.google.payments.api.PaymentsClient.getCall(0).args[0].environment, 'PRODUCTION'); + })); + }); }); - it('handles the shipping option intent if onPaymentDataChanged is provided and requiring shipping option', function (done) { - this.stubRequestAndGoogleApi(); - const callbacks = { onPaymentDataChanged: () => {} }; - this.recurly.GooglePay({ - ...this.googlePayOpts, - callbacks, - paymentDataRequest: { - shippingOptionRequired: true, - }, + context('when the site mode is any other than production and no environment option is provided', function () { + beforeEach(function () { + this.stubRequestOpts.info = Promise.resolve({ + siteMode: 'sandbox', + paymentMethods: this.paymentMethods, + }); + this.googlePayOpts.environment = undefined; }); - nextTick(() => assertDone(done, () => { - assert.equal(window.google.payments.api.PaymentsClient.getCall(0).args[0].paymentDataCallbacks, callbacks); - const { callbackIntents } = window.google.payments.api.PaymentsClient.prototype.prefetchPaymentData.getCall(0).args[0]; - assert.deepEqual(callbackIntents, ['SHIPPING_OPTION']); - })); + it('initiates the pay-with-google in TEST mode', function (done) { + this.stubRequestAndGoogleApi(); + this.recurly.GooglePay(this.googlePayOpts); + + nextTick(() => assertDone(done, () => { + assert.deepEqual(window.google.payments.api.PaymentsClient.getCall(0).args[0].environment, 'TEST'); + })); + }); }); - context('with onPaymentAuthorized provided', function () { + context('options.billingAddressRequired = false', function () { beforeEach(function () { - this.stubRequestAndGoogleApi(); - this.clickGooglePayButton = (emitter, done) => { - emitter.on('ready', button => { - nextTick(() => { - const { onPaymentAuthorized } = window.google.payments.api.PaymentsClient.getCall(0).args[0].paymentDataCallbacks; - button.click().then(onPaymentAuthorized).then(done); - }); - }); - }; + this.googlePayOpts.billingAddressRequired = false; }); - it('handles the payment authorized intent', function (done) { - const callbacks = { onPaymentAuthorized: () => {} }; - this.recurly.GooglePay({ - ...this.googlePayOpts, - callbacks, - }); + it('initiates the pay-with-google without the billing address requirement', function (done) { + this.stubRequestAndGoogleApi(); + this.recurly.GooglePay(this.googlePayOpts); nextTick(() => assertDone(done, () => { - assert.equal(window.google.payments.api.PaymentsClient.getCall(0).args[0].paymentDataCallbacks.onPaymentAuthorized === undefined, false); - const { callbackIntents } = window.google.payments.api.PaymentsClient.prototype.prefetchPaymentData.getCall(0).args[0]; - assert.deepEqual(callbackIntents, ['PAYMENT_AUTHORIZATION']); + const { allowedPaymentMethods: [{ parameters }] } = window.google.payments.api.PaymentsClient.prototype.isReadyToPay.getCall(0).args[0]; + assert.deepEqual(parameters.billingAddressRequired, undefined); + assert.deepEqual(parameters.billingAddressParameters, undefined); })); }); + }); + + context('@deprecated options.requireBillingAddress = false', function () { + beforeEach(function () { + delete this.googlePayOpts.billingAddressRequired; + this.googlePayOpts.requireBillingAddress = false; + }); - it('is called after the button is clicked with the paymentData and token', function (done) { - let paymentData; - const emitter = this.recurly.GooglePay({ - ...this.googlePayOpts, - callbacks: { onPaymentAuthorized: (pd) => paymentData = pd }, - }); + it('initiates the pay-with-google without the billing address requirement', function (done) { + this.stubRequestAndGoogleApi(); + this.recurly.GooglePay(this.googlePayOpts); - this.clickGooglePayButton(emitter, (res) => assertDone(done, () => { - assert.equal(res.transactionState, 'SUCCESS'); - assert.equal(res.error, undefined); - assert.equal(paymentData.recurlyToken.id, 'TOKEN_123'); + nextTick(() => assertDone(done, () => { + const { allowedPaymentMethods: [{ parameters }] } = window.google.payments.api.PaymentsClient.prototype.isReadyToPay.getCall(0).args[0]; + assert.deepEqual(parameters.billingAddressRequired, undefined); + assert.deepEqual(parameters.billingAddressParameters, undefined); })); }); + }); - it('allows for errors from fetching the token', function (done) { - this.recurly.request.post.restore(); - this.sandbox.stub(this.recurly.request, 'post').rejects('boom'); + context('with options.paymentDataRequest attributes', function () { + it('merges them into the actual paymentDataRequest', function (done) { + this.stubRequestAndGoogleApi(); + const merchantInfo = { + merchantId: 'GOOGLE_MERCHANT_ID_123', + merchantName: 'RECURLY', + }; + const transactionInfo = { + currencyCode: 'USD', + countryCode: 'US', + totalPrice: '1', + }; - const onPaymentAuthorized = this.sandbox.stub(); - const emitter = this.recurly.GooglePay({ + this.recurly.GooglePay({ ...this.googlePayOpts, - callbacks: { onPaymentAuthorized }, + billingAddressRequired: false, + paymentDataRequest: { + emailRequired: true, + shippingAddressRequired: true, + shippingOptionRequired: true, + shippingOptionParameters: [], + shippingAddressParameters: [], + }, }); - this.clickGooglePayButton(emitter, (res) => assertDone(done, () => { - assert.equal(res.transactionState, 'ERROR'); - assert.deepEqual(res.error, { - reason: 'OTHER_ERROR', - message: 'Error processing payment information, please try again later', - intent: 'PAYMENT_AUTHORIZATION' - }); - assert(!onPaymentAuthorized.called, 'onPaymentAuthorized should not be called'); + nextTick(() => assertDone(done, () => { + const paymentDataRequest = window.google.payments.api.PaymentsClient.prototype.prefetchPaymentData.getCall(0).args[0]; + assert.deepEqual(paymentDataRequest.merchantInfo, merchantInfo); + assert.deepEqual(paymentDataRequest.transactionInfo, { totalPriceStatus: 'NOT_CURRENTLY_KNOWN', ...transactionInfo }); + assert.equal(paymentDataRequest.emailRequired, true); + assert.equal(paymentDataRequest.shippingAddressRequired, true); + assert.equal(paymentDataRequest.shippingOptionRequired, true); + assert.equal(paymentDataRequest.shippingOptionParameters.length, 0); + assert.equal(paymentDataRequest.shippingAddressParameters.length, 0); })); }); - it('allows for errors to be passed back', function (done) { - const error = { - reason: 'PAYMENT_DATA_INVALID', - message: 'Cannot pay with payment credentials', - intent: 'PAYMENT_AUTHORIZATION', + it('uses them if not provided at the top level', function (done) { + this.stubRequestAndGoogleApi(); + const merchantInfo = { + merchantId: 'GOOGLE_MERCHANT_ID_123', + merchantName: 'RECURLY', }; - const emitter = this.recurly.GooglePay({ - ...this.googlePayOpts, - callbacks: { onPaymentAuthorized: () => ({ error }) }, + const transactionInfo = { + currencyCode: 'USD', + countryCode: 'US', + totalPrice: '1', + }; + + this.recurly.GooglePay({ + billingAddressRequired: false, + paymentDataRequest: { + merchantInfo, + transactionInfo, + emailRequired: true, + shippingAddressRequired: true, + shippingOptionRequired: true, + shippingOptionParameters: [], + shippingAddressParameters: [], + }, }); - this.clickGooglePayButton(emitter, (res) => assertDone(done, () => { - assert.equal(res.transactionState, 'ERROR'); - assert.deepEqual(res.error, error); + nextTick(() => assertDone(done, () => { + const paymentDataRequest = window.google.payments.api.PaymentsClient.prototype.prefetchPaymentData.getCall(0).args[0]; + assert.deepEqual(paymentDataRequest.merchantInfo, merchantInfo); + assert.deepEqual(paymentDataRequest.transactionInfo, { totalPriceStatus: 'NOT_CURRENTLY_KNOWN', ...transactionInfo }); + assert.equal(paymentDataRequest.emailRequired, true); + assert.equal(paymentDataRequest.shippingAddressRequired, true); + assert.equal(paymentDataRequest.shippingOptionRequired, true); + assert.equal(paymentDataRequest.shippingOptionParameters.length, 0); + assert.equal(paymentDataRequest.shippingAddressParameters.length, 0); })); }); }); - }); - context('when cannot proceed with the pay-with-google', function () { - context('when the GooglePay is not available', function () { - beforeEach(function () { - this.stubGoogleAPIOpts.isReadyToPay = Promise.resolve({ result: false }); + context('with options.callbacks', function () { + it('handles the shipping address intent if onPaymentDataChanged is provided and requiring shipping address', function (done) { this.stubRequestAndGoogleApi(); + const callbacks = { onPaymentDataChanged: () => {} }; + this.recurly.GooglePay({ + ...this.googlePayOpts, + callbacks, + paymentDataRequest: { + shippingAddressRequired: true, + }, + }); + + nextTick(() => assertDone(done, () => { + assert.equal(window.google.payments.api.PaymentsClient.getCall(0).args[0].paymentDataCallbacks, callbacks); + const { callbackIntents } = window.google.payments.api.PaymentsClient.prototype.prefetchPaymentData.getCall(0).args[0]; + assert.deepEqual(callbackIntents, ['SHIPPING_ADDRESS']); + })); }); - it('emits the same error the pay-with-google throws', function (done) { - const result = this.recurly.GooglePay(this.googlePayOpts); + it('handles the shipping option intent if onPaymentDataChanged is provided and requiring shipping option', function (done) { + this.stubRequestAndGoogleApi(); + const callbacks = { onPaymentDataChanged: () => {} }; + this.recurly.GooglePay({ + ...this.googlePayOpts, + callbacks, + paymentDataRequest: { + shippingOptionRequired: true, + }, + }); - result.on('error', (err) => assertDone(done, () => { - assert.ok(err); - assert.equal(err.code, 'google-pay-not-available'); - assert.equal(err.message, 'Google Pay is not available'); + nextTick(() => assertDone(done, () => { + assert.equal(window.google.payments.api.PaymentsClient.getCall(0).args[0].paymentDataCallbacks, callbacks); + const { callbackIntents } = window.google.payments.api.PaymentsClient.prototype.prefetchPaymentData.getCall(0).args[0]; + assert.deepEqual(callbackIntents, ['SHIPPING_OPTION']); })); }); - it('do not emit any token nor the on ready event', function (done) { - const result = this.recurly.GooglePay(this.googlePayOpts); + context('with onPaymentAuthorized provided', function () { + beforeEach(function () { + this.stubRequestAndGoogleApi(); + this.clickGooglePayButton = (emitter, done) => { + emitter.on('ready', button => { + nextTick(() => { + const { onPaymentAuthorized } = window.google.payments.api.PaymentsClient.getCall(0).args[0].paymentDataCallbacks; + button.click().then(onPaymentAuthorized).then(done); + }); + }); + }; + }); - result.on('ready', () => done(new Error('expected to not emit a ready event'))); - result.on('token', () => done(new Error('expected to not emit a token event'))); - nextTick(done); - }); - }); + it('handles the payment authorized intent', function (done) { + const callbacks = { onPaymentAuthorized: () => {} }; + this.recurly.GooglePay({ + ...this.googlePayOpts, + callbacks, + }); - context('when the GooglePay is available but does not support user cards', function () { - beforeEach(function () { - this.googlePayOpts.existingPaymentMethodRequired = true; - this.stubGoogleAPIOpts.isReadyToPay = Promise.resolve({ result: true, paymentMethodPresent: false }); - this.stubRequestAndGoogleApi(); - }); + nextTick(() => assertDone(done, () => { + assert.equal(window.google.payments.api.PaymentsClient.getCall(0).args[0].paymentDataCallbacks.onPaymentAuthorized === undefined, false); + const { callbackIntents } = window.google.payments.api.PaymentsClient.prototype.prefetchPaymentData.getCall(0).args[0]; + assert.deepEqual(callbackIntents, ['PAYMENT_AUTHORIZATION']); + })); + }); - it('initiates pay-with-google with the expected Google Pay Configuration', function (done) { - this.recurly.GooglePay(this.googlePayOpts); + it('is called after the button is clicked with the paymentData and token', function (done) { + let paymentData; + const emitter = this.recurly.GooglePay({ + ...this.googlePayOpts, + callbacks: { onPaymentAuthorized: (pd) => paymentData = pd }, + }); - nextTick(() => assertDone(done, () => { - assert.equal(window.google.payments.api.PaymentsClient.called, true); - const isReadyToPayRequest = window.google.payments.api.PaymentsClient.prototype.isReadyToPay.getCall(0).args[0]; - assert.equal(isReadyToPayRequest.existingPaymentMethodRequired, true); - })); - }); + this.clickGooglePayButton(emitter, (res) => assertDone(done, () => { + assert.equal(res.transactionState, 'SUCCESS'); + assert.equal(res.error, undefined); + assert.equal(paymentData.recurlyToken.id, 'TOKEN_123'); + })); + }); - it('emits the same error the pay-with-google throws', function (done) { - const result = this.recurly.GooglePay(this.googlePayOpts); + it('allows for errors from fetching the token', function (done) { + this.recurly.request.post.restore(); + this.sandbox.stub(this.recurly.request, 'post').rejects('boom'); - result.on('error', (err) => assertDone(done, () => { - assert.ok(err); - assert.equal(err.code, 'google-pay-not-available'); - assert.equal(err.message, 'Google Pay is not available'); - })); - }); + const onPaymentAuthorized = this.sandbox.stub(); + const emitter = this.recurly.GooglePay({ + ...this.googlePayOpts, + callbacks: { onPaymentAuthorized }, + }); - it('do not emit any token nor the on ready event', function (done) { - const result = this.recurly.GooglePay(this.googlePayOpts); + this.clickGooglePayButton(emitter, (res) => assertDone(done, () => { + assert.equal(res.transactionState, 'ERROR'); + assert.deepEqual(res.error, { + reason: 'OTHER_ERROR', + message: 'Error processing payment information, please try again later', + intent: 'PAYMENT_AUTHORIZATION' + }); + assert(!onPaymentAuthorized.called, 'onPaymentAuthorized should not be called'); + })); + }); - result.on('ready', () => done(new Error('expected to not emit a ready event'))); - result.on('token', () => done(new Error('expected to not emit a token event'))); - nextTick(done); + it('allows for errors to be passed back', function (done) { + const error = { + reason: 'PAYMENT_DATA_INVALID', + message: 'Cannot pay with payment credentials', + intent: 'PAYMENT_AUTHORIZATION', + }; + const emitter = this.recurly.GooglePay({ + ...this.googlePayOpts, + callbacks: { onPaymentAuthorized: () => ({ error }) }, + }); + + this.clickGooglePayButton(emitter, (res) => assertDone(done, () => { + assert.equal(res.transactionState, 'ERROR'); + assert.deepEqual(res.error, error); + })); + }); }); }); - }); - context('when the pay-with-google success', function () { - it('emits the ready event with the google-pay button', function (done) { - this.stubRequestAndGoogleApi(); - const result = this.recurly.GooglePay(this.googlePayOpts); + context('when cannot proceed with the pay-with-google', function () { + context('when the GooglePay is not available', function () { + beforeEach(function () { + this.stubGoogleAPIOpts.isReadyToPay = Promise.resolve({ result: false }); + this.stubRequestAndGoogleApi(); + }); - result.on('ready', button => assertDone(done, () => { - assert.ok(button); - })); - }); + it('emits the same error the pay-with-google throws', function (done) { + const result = this.recurly.GooglePay(this.googlePayOpts); - context('when the google-pay button is clicked', function () { - beforeEach(function () { - this.clickGooglePayButton = (cb) => { - this.stubRequestAndGoogleApi(); + result.on('error', (err) => assertDone(done, () => { + assert.ok(err); + assert.equal(err.code, 'google-pay-not-available'); + assert.equal(err.message, 'Google Pay is not available'); + })); + }); + + it('do not emit any token nor the on ready event', function (done) { const result = this.recurly.GooglePay(this.googlePayOpts); - result.on('ready', button => { - cb(result); - nextTick(() => button.click()); - }); - }; + result.on('ready', () => done(new Error('expected to not emit a ready event'))); + result.on('token', () => done(new Error('expected to not emit a token event'))); + nextTick(done); + }); }); - it('requests the user Payment Data', function (done) { - this.clickGooglePayButton(result => { - result.on('token', () => assertDone(done, () => { - assert.equal(window.google.payments.api.PaymentsClient.prototype.loadPaymentData.called, true); + context('when the GooglePay is available but does not support user cards', function () { + beforeEach(function () { + this.googlePayOpts.existingPaymentMethodRequired = true; + this.stubGoogleAPIOpts.isReadyToPay = Promise.resolve({ result: true, paymentMethodPresent: false }); + this.stubRequestAndGoogleApi(); + }); + + it('initiates pay-with-google with the expected Google Pay Configuration', function (done) { + this.recurly.GooglePay(this.googlePayOpts); + + nextTick(() => assertDone(done, () => { + assert.equal(window.google.payments.api.PaymentsClient.called, true); + const isReadyToPayRequest = window.google.payments.api.PaymentsClient.prototype.isReadyToPay.getCall(0).args[0]; + assert.equal(isReadyToPayRequest.existingPaymentMethodRequired, true); + })); + }); + + it('emits the same error the pay-with-google throws', function (done) { + const result = this.recurly.GooglePay(this.googlePayOpts); + + result.on('error', (err) => assertDone(done, () => { + assert.ok(err); + assert.equal(err.code, 'google-pay-not-available'); + assert.equal(err.message, 'Google Pay is not available'); })); }); + + it('do not emit any token nor the on ready event', function (done) { + const result = this.recurly.GooglePay(this.googlePayOpts); + + result.on('ready', () => done(new Error('expected to not emit a ready event'))); + result.on('token', () => done(new Error('expected to not emit a token event'))); + nextTick(done); + }); }); + }); - context('when fails retrieving the user Payment Data', function () { + context('when the pay-with-google success', function () { + it('emits the ready event with the google-pay button', function (done) { + this.stubRequestAndGoogleApi(); + const result = this.recurly.GooglePay(this.googlePayOpts); + + result.on('ready', button => assertDone(done, () => { + assert.ok(button); + })); + }); + + context('when the google-pay button is clicked', function () { beforeEach(function () { - this.stubGoogleAPIOpts.loadPaymentData = Promise.reject('boom'); - }); + this.clickGooglePayButton = (cb) => { + this.stubRequestAndGoogleApi(); + const result = this.recurly.GooglePay(this.googlePayOpts); - it('emits the same error that the retrieving process throws', function (done) { - this.clickGooglePayButton(result => { - result.on('error', err => { - assertDone(done, () => { - assert.ok(err); - assert.equal(err.code, 'google-pay-payment-failure'); - assert.equal(err.message, 'Google Pay could not get the Payment Data'); - }); + result.on('ready', button => { + cb(result); + nextTick(() => button.click()); }); - }); + }; }); - it('do not request any token to Recurly', function (done) { + it('requests the user Payment Data', function (done) { this.clickGooglePayButton(result => { - result.on('token', () => done(new Error('expected to not emit the token'))); - result.on('error', () => assertDone(done, () => { - assert.equal(this.recurly.request.post.called, false); + result.on('token', () => assertDone(done, () => { + assert.equal(window.google.payments.api.PaymentsClient.prototype.loadPaymentData.called, true); })); }); }); - }); - context('when success retrieving the user Payment Data', function () { - it('request to Recurly to create the token with the billing address from the user Payment Data', function (done) { - this.clickGooglePayButton(result => { - result.on('token', () => assertDone(done, () => { - assert.equal(this.recurly.request.post.called, true); - - assert.deepEqual(this.recurly.request.post.getCall(0).args[0], { - route: '/google_pay/token', - data: { - first_name: 'John', - last_name: 'Smith', - country: 'US', - state: 'CA', - city: 'Mountain View', - postal_code: '94043', - address1: '1600 Amphitheatre Parkway', - address2: '', - paymentData: { - paymentMethodData: { - description: 'Visa •••• 1111', - tokenizationData: { - type: 'PAYMENT_GATEWAY', - token: '{"id": "tok_123"}', - }, - type: 'CARD', - info: { - cardNetwork: 'VISA', - cardDetails: '1111', - billingAddress: { - address3: '', - sortingCode: '', - address2: '', - countryCode: 'US', - address1: '1600 Amphitheatre Parkway', - postalCode: '94043', - name: 'John Smith', - locality: 'Mountain View', - administrativeArea: 'CA', - }, - }, - }, - }, - gateway_code: 'gateway_123', - } + context('when fails retrieving the user Payment Data', function () { + beforeEach(function () { + this.stubGoogleAPIOpts.loadPaymentData = Promise.reject('boom'); + }); + + it('emits the same error that the retrieving process throws', function (done) { + this.clickGooglePayButton(result => { + result.on('error', err => { + assertDone(done, () => { + assert.ok(err); + assert.equal(err.code, 'google-pay-payment-failure'); + assert.equal(err.message, 'Google Pay could not get the Payment Data'); + }); }); - })); + }); }); - }); - context('when the user provide a
with custom billing address', function () { - beforeEach(function () { - this.googlePayOpts.form = { - first_name: 'Frank', - last_name: 'Isaac', - country: 'RF', - state: '', - city: '', - postal_code: '123', - address1: '', - address2: '', - }; + it('do not request any token to Recurly', function (done) { + this.clickGooglePayButton(result => { + result.on('token', () => done(new Error('expected to not emit the token'))); + result.on('error', () => assertDone(done, () => { + assert.equal(this.recurly.request.post.called, false); + })); + }); }); + }); - it('request to Recurly to create the token with the billing address from the ', function (done) { + context('when success retrieving the user Payment Data', function () { + it('request to Recurly to create the token with the billing address from the user Payment Data', function (done) { this.clickGooglePayButton(result => { result.on('token', () => assertDone(done, () => { assert.equal(this.recurly.request.post.called, true); - - assert.deepEqual(this.recurly.request.post.getCall(0).args[0], { + let expectedCall = { route: '/google_pay/token', data: { - first_name: 'Frank', - last_name: 'Isaac', - country: 'RF', - state: '', - city: '', - postal_code: '123', - address1: '', + first_name: 'John', + last_name: 'Smith', + country: 'US', + state: 'CA', + city: 'Mountain View', + postal_code: '94043', + address1: '1600 Amphitheatre Parkway', address2: '', paymentData: { paymentMethodData: { @@ -770,45 +819,88 @@ describe('Google Pay', function () { }, gateway_code: 'gateway_123', } - }); + }; + + if (isBraintreeIntegration) { + expectedCall.data.type = 'braintree'; + expectedCall.data.paymentData.paymentMethodData.gatewayToken = { + nonce: 'GATEWAY_TOKEN', + deviceData: 'DEVICE_DATA', + }; + } + + assert.deepEqual(this.recurly.request.post.getCall(0).args[0], expectedCall); })); }); }); - }); - context('when Recurly fails creating the token', function () { - beforeEach(function () { - this.stubRequestOpts.token = Promise.reject(recurlyError('api-error')); - }); + context('when the user provide a with custom billing address', function () { + beforeEach(function () { + this.googlePayOpts.form = { + first_name: 'Frank', + last_name: 'Isaac', + country: 'RF', + state: '', + city: '', + postal_code: '123', + address1: '', + address2: '', + }; + }); - it('emits an api-error', function (done) { - this.clickGooglePayButton(result => { - result.on('error', err => assertDone(done, () => { - assert.ok(err); - assert.equal(err.code, 'api-error'); - assert.equal(err.message, 'There was an error with your request.'); - })); + it('request to Recurly to create the token with the billing address from the ', function (done) { + this.clickGooglePayButton(result => { + result.on('token', () => assertDone(done, () => { + assert.equal(this.recurly.request.post.called, true); + + const callData = this.recurly.request.post.getCall(0).args[0].data; + assert.equal(callData.first_name, 'Frank'); + assert.equal(callData.last_name, 'Isaac'); + assert.equal(callData.country, 'RF'); + assert.equal(callData.state, ''); + assert.equal(callData.city, ''); + assert.equal(callData.postal_code, '123'); + assert.equal(callData.address1, ''); + assert.equal(callData.address2, ''); + })); + }); }); }); - it('do not emit any token', function (done) { - this.clickGooglePayButton(result => { - result.on('token', () => done(new Error('expected to not emit a token event'))); + context('when Recurly fails creating the token', function () { + beforeEach(function () { + this.stubRequestOpts.token = Promise.reject(recurlyError('api-error')); + }); - nextTick(done); + it('emits an api-error', function (done) { + this.clickGooglePayButton(result => { + result.on('error', err => assertDone(done, () => { + assert.ok(err); + assert.equal(err.code, 'api-error'); + assert.equal(err.message, 'There was an error with your request.'); + })); + }); + }); + + it('do not emit any token', function (done) { + this.clickGooglePayButton(result => { + result.on('token', () => done(new Error('expected to not emit a token event'))); + + nextTick(done); + }); }); }); - }); - context('when Recurly success creating the token', function () { - it('emits the token', function (done) { - this.clickGooglePayButton(result => { - result.on('token', (token) => assertDone(done, () => { - assert.ok(token); - assert.deepEqual(token, { - id: 'TOKEN_123', - }); - })); + context('when Recurly success creating the token', function () { + it('emits the token', function (done) { + this.clickGooglePayButton(result => { + result.on('token', (token) => assertDone(done, () => { + assert.ok(token); + assert.deepEqual(token, { + id: 'TOKEN_123', + }); + })); + }); }); }); }); @@ -816,4 +908,4 @@ describe('Google Pay', function () { }); }); }); -}); +} diff --git a/types/lib/google-pay/index.d.ts b/types/lib/google-pay/index.d.ts index 3d27f3c3..6f76a748 100644 --- a/types/lib/google-pay/index.d.ts +++ b/types/lib/google-pay/index.d.ts @@ -81,6 +81,13 @@ export type GooglePayOptions = { */ gatewayCode?: string; + /** + * If provided, will use Braintree to process the GooglePay transaction. + */ + braintree?: { + clientAuthorization: string; + }; + /** * Specify configuration for Google Pay API. */