diff --git a/lib/const/gateway-constants.js b/lib/const/gateway-constants.js deleted file mode 100644 index eb2dc1e3a..000000000 --- a/lib/const/gateway-constants.js +++ /dev/null @@ -1 +0,0 @@ -export const BRAINTREE_CLIENT_VERSION = '3.101.0'; diff --git a/lib/recurly/apple-pay/apple-pay.braintree.js b/lib/recurly/apple-pay/apple-pay.braintree.js index 9125f9bd2..6fdab46b8 100644 --- a/lib/recurly/apple-pay/apple-pay.braintree.js +++ b/lib/recurly/apple-pay/apple-pay.braintree.js @@ -1,36 +1,11 @@ import Promise from 'promise'; import { ApplePay } from './apple-pay'; -import loadScriptPromise from '../../util/load-script-promise'; import Debug from 'debug'; -import { BRAINTREE_CLIENT_VERSION } from '../../const/gateway-constants'; +import BraintreeLoader from '../../util/braintree-loader'; const debug = Debug('recurly:apple-pay:braintree'); -const LIBS = { - client: 'client', - applePay: 'apple-pay', - dataCollector: 'data-collector', -}; - -const loadBraintree = (...libs) => { - const loadLib = lib => { - const isLibPresent = window.braintree?.client?.VERSION === BRAINTREE_CLIENT_VERSION && - lib in window.braintree; - - return isLibPresent - ? Promise.resolve() - : loadScriptPromise(ApplePayBraintree.libUrl(lib)); - }; - - return loadLib('client') - .then(() => Promise.all(libs.map(loadLib))); -}; - export class ApplePayBraintree extends ApplePay { - static libUrl (lib) { - return `https://js.braintreegateway.com/web/${BRAINTREE_CLIENT_VERSION}/js/${LIBS[lib]}.min.js`; - } - configure (options) { debug('Initializing client'); @@ -38,7 +13,7 @@ export class ApplePayBraintree extends ApplePay { if (options.braintree.displayName) this.displayName = options.braintree.displayName; else this.displayName = 'My Store'; - loadBraintree('applePay', 'dataCollector') + BraintreeLoader.loadModules('applePay', 'dataCollector') .then(() => window.braintree.client.create({ authorization })) .then(client => Promise.all([ window.braintree.dataCollector.create({ client }), diff --git a/lib/recurly/paypal/strategy/braintree.js b/lib/recurly/paypal/strategy/braintree.js index eff619bcf..8ae83a115 100644 --- a/lib/recurly/paypal/strategy/braintree.js +++ b/lib/recurly/paypal/strategy/braintree.js @@ -1,7 +1,5 @@ -import loadScript from 'load-script'; -import after from '../../../util/after'; +import BraintreeLoader from '../../../util/braintree-loader'; import { PayPalStrategy } from './index'; -import { BRAINTREE_CLIENT_VERSION } from '../../../const/gateway-constants'; const debug = require('debug')('recurly:paypal:strategy:braintree'); @@ -10,45 +8,16 @@ const debug = require('debug')('recurly:paypal:strategy:braintree'); */ export class BraintreeStrategy extends PayPalStrategy { - constructor (...args) { - super(...args); - this.load(); - } - configure (options) { super.configure(options); if (!options.braintree || !options.braintree.clientAuthorization) { throw this.error('paypal-config-missing', { opt: 'braintree.clientAuthorization' }); } this.config.clientAuthorization = options.braintree.clientAuthorization; - } - - /** - * Loads Braintree client and modules - * - * @todo semver client detection - */ - load () { - debug('loading Braintree libraries'); - const part = after(2, () => this.initialize()); - const get = (lib, done = () => {}) => { - const uri = `https://js.braintreegateway.com/web/${BRAINTREE_CLIENT_VERSION}/js/${lib}.min.js`; - loadScript(uri, error => { - if (error) this.error('paypal-load-error', { cause: error }); - else done(); - }); - }; - - const modules = () => { - if (this.braintreeClientAvailable('paypal')) part(); - else get('paypal', part); - if (this.braintreeClientAvailable('dataCollector')) part(); - else get('data-collector', part); - }; - - if (this.braintreeClientAvailable()) modules(); - else get('client', modules); + BraintreeLoader.loadModules('paypal', 'dataCollector') + .catch(cause => this.error('paypal-load-error', { cause })) + .then(() => this.initialize()); } /** @@ -113,9 +82,4 @@ export class BraintreeStrategy extends PayPalStrategy { } this.off(); } - - braintreeClientAvailable (module) { - const bt = window.braintree; - return bt && bt.client && bt.client.VERSION === BRAINTREE_CLIENT_VERSION && (module ? module in bt : true); - } } diff --git a/lib/recurly/risk/three-d-secure/strategy/braintree.js b/lib/recurly/risk/three-d-secure/strategy/braintree.js index 8a653cd31..d7327de11 100644 --- a/lib/recurly/risk/three-d-secure/strategy/braintree.js +++ b/lib/recurly/risk/three-d-secure/strategy/braintree.js @@ -1,7 +1,5 @@ -import loadScript from 'load-script'; -import Promise from 'promise'; +import BraintreeLoader from '../../../../util/braintree-loader'; import ThreeDSecureStrategy from './strategy'; -import { BRAINTREE_CLIENT_VERSION } from '../../../../const/gateway-constants'; const debug = require('debug')('recurly:risk:three-d-secure:braintree'); @@ -9,15 +7,17 @@ export default class BraintreeStrategy extends ThreeDSecureStrategy { static strategyName = 'braintree_blue'; + loadBraintreeLibraries () { + return BraintreeLoader.loadModules('threeDSecure'); + } + constructor (...args) { super(...args); - debug('loading braintree libraries'); this.loadBraintreeLibraries() .catch(cause => this.threeDSecure.error('3ds-vendor-load-error', { vendor: 'Braintree', cause })) .then(() => { this.braintree = window.braintree; - debug('Braintree checkout instance created', this.braintree); this.markReady(); }); } @@ -97,24 +97,4 @@ export default class BraintreeStrategy extends ThreeDSecureStrategy { .catch(cause => this.threeDSecure.error('3ds-auth-error', { cause })); }); } - - urlForResource (type) { - return `https://js.braintreegateway.com/web/${BRAINTREE_CLIENT_VERSION}/js/${type}.min.js`; - } - - /** - * Loads Braintree library dependency - */ - loadBraintreeLibraries () { - return new Promise((resolve, reject) => { - if (window.braintree && window.braintree.client && window.braintree.threeDSecure) return resolve(); - loadScript(this.urlForResource('client'), error => { - if (error) reject(error); - else loadScript(this.urlForResource('three-d-secure'), error => { - if (error) reject(error); - else resolve(); - }); - }); - }); - } } diff --git a/lib/recurly/venmo/strategy/braintree.js b/lib/recurly/venmo/strategy/braintree.js index 8ce4c91b2..c68eb9182 100644 --- a/lib/recurly/venmo/strategy/braintree.js +++ b/lib/recurly/venmo/strategy/braintree.js @@ -1,8 +1,6 @@ -import loadScript from 'load-script'; -import after from '../../../util/after'; +import BraintreeLoader from '../../../util/braintree-loader'; import { VenmoStrategy } from './index'; import { normalize } from '../../../util/normalize'; -import { BRAINTREE_CLIENT_VERSION } from '../../../const/gateway-constants'; const debug = require('debug')('recurly:venmo:strategy:braintree'); @@ -11,47 +9,20 @@ const debug = require('debug')('recurly:venmo:strategy:braintree'); */ export class BraintreeStrategy extends VenmoStrategy { - constructor (...args) { - super(args); - this.load(args[0]); - } - configure (options) { super.configure(options); + if (!options.braintree || !options.braintree.clientAuthorization) { throw this.error('venmo-config-missing', { opt: 'braintree.clientAuthorization' }); } this.config.clientAuthorization = options.braintree.clientAuthorization; this.config.allowDesktopWebLogin = options.braintree.webAuthentication ? options.braintree.webAuthentication : false; - } - /** - * Loads Braintree client and modules - * - * @todo semver client detection - */ - load ({ form }) { - debug('loading Braintree libraries'); - this.form = form; + this.form = options.form; - const part = after(2, () => this.initialize()); - const get = (lib, done = () => {}) => { - const uri = `https://js.braintreegateway.com/web/${BRAINTREE_CLIENT_VERSION}/js/${lib}.min.js`; - loadScript(uri, error => { - if (error) this.error('venmo-load-error', { cause: error }); - else done(); - }); - }; - - const modules = () => { - if (this.braintreeClientAvailable('venmo')) part(); - else get('venmo', part); - if (this.braintreeClientAvailable('dataCollector')) part(); - else get('data-collector', part); - }; - - if (this.braintreeClientAvailable()) modules(); - else get('client', modules); + BraintreeLoader.loadModules('venmo', 'dataCollector') + .catch(cause => this.error('venmo-load-error', { cause })) + .then(() => this.initialize()); } /** @@ -124,9 +95,4 @@ export class BraintreeStrategy extends VenmoStrategy { } this.off(); } - - braintreeClientAvailable (module) { - const bt = window.braintree; - return bt && bt.client && bt.client.VERSION === BRAINTREE_CLIENT_VERSION && (module ? module in bt : true); - } } diff --git a/lib/recurly/venmo/strategy/index.js b/lib/recurly/venmo/strategy/index.js index 7c64fc0ad..12084dd66 100644 --- a/lib/recurly/venmo/strategy/index.js +++ b/lib/recurly/venmo/strategy/index.js @@ -17,7 +17,7 @@ export class VenmoStrategy extends Emitter { this.once('ready', () => this.isReady = true); - this.configure(options[0]); + this.configure(options); } ready (done) { diff --git a/lib/util/after.js b/lib/util/after.js deleted file mode 100644 index bcc9c690e..000000000 --- a/lib/util/after.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Returns a function which calls the callback after x calls - * - * @param {Number} count - * @param {Function} done - * @return {Function} - */ -export default function after (count = 0, done) { - let i = 0; - return () => i++ && i === count && done(); -} diff --git a/lib/util/braintree-loader.js b/lib/util/braintree-loader.js new file mode 100644 index 000000000..e78dffc0d --- /dev/null +++ b/lib/util/braintree-loader.js @@ -0,0 +1,37 @@ +import loadScriptPromise from './load-script-promise'; +import Debug from 'debug'; + +const debug = Debug('recurly:braintree'); + +const BRAINTREE_CLIENT_VERSION = '3.101.0'; + +const MOD_TO_LIB = { + 'dataCollector': 'data-collector', + 'applePay': 'apple-pay', + 'googlePayment': 'google-payment', + 'threeDSecure': 'three-d-secure', +}; + +const libUrl = (mod) => { + const btMod = MOD_TO_LIB[mod] || mod; + return `https://js.braintreegateway.com/web/${BRAINTREE_CLIENT_VERSION}/js/${btMod}.min.js`; +}; + +const loadModuleScript = (mod) => { + const isModulePresent = window.braintree?.client?.VERSION === BRAINTREE_CLIENT_VERSION && mod in window.braintree; + + return isModulePresent + ? Promise.resolve() + : loadScriptPromise(libUrl(mod)); +}; + +export default { + BRAINTREE_CLIENT_VERSION, + + loadModules: (...modules) => { + debug('loading Braintree client modules', modules); + + return loadModuleScript('client') + .then(() => Promise.all(modules.map(loadModuleScript))); + }, +}; diff --git a/test/unit/apple-pay.test.js b/test/unit/apple-pay.test.js index d5c922c9f..38414a2cd 100644 --- a/test/unit/apple-pay.test.js +++ b/test/unit/apple-pay.test.js @@ -6,6 +6,7 @@ import omit from 'lodash.omit'; import Emitter from 'component-emitter'; import Promise from 'promise'; import { initRecurly, nextTick } from './support/helpers'; +import BraintreeLoader from '../../lib/util/braintree-loader'; import { ApplePayBraintree } from '../../lib/recurly/apple-pay/apple-pay.braintree'; import filterSupportedNetworks from '../../lib/recurly/apple-pay/util/filter-supported-networks'; @@ -78,9 +79,7 @@ const getBraintreeStub = () => ({ }, }); -const maybeDescribe = 'ApplePaySession' in window ? describe : describe.skip; - -maybeDescribe('ApplePay', function () { +describe('ApplePay', function () { beforeEach(function () { this.sandbox = sinon.createSandbox(); window.ApplePaySession = ApplePaySessionStub; @@ -719,58 +718,19 @@ function applePayTest (integrationType) { describe('when the libs are not loaded', function () { beforeEach(function () { delete window.braintree; - this.sandbox.stub(ApplePayBraintree, 'libUrl').returns('/api/mock-200'); + this.sandbox.stub(BraintreeLoader, 'loadModules').rejects('boom'); }); it('load the libs', function (done) { const applePay = this.recurly.ApplePay(validOpts); - applePay.on('error', ensureDone(done, () => { - assert.equal(ApplePayBraintree.libUrl.callCount, 3); - assert.equal(ApplePayBraintree.libUrl.getCall(0).args[0], 'client'); - assert.equal(ApplePayBraintree.libUrl.getCall(1).args[0], 'applePay'); - assert.equal(ApplePayBraintree.libUrl.getCall(2).args[0], 'dataCollector'); + applePay.on('error', ensureDone(done, (err) => { + assert(BraintreeLoader.loadModules.calledWith('applePay', 'dataCollector')); + assert.equal(err, applePay.initError); + assertInitError(applePay, 'apple-pay-init-error'); })); }); }); - const requiredBraintreeLibs = ['client', 'dataCollector', 'applePay']; - requiredBraintreeLibs.forEach(requiredLib => { - describe(`when failed to load the braintree ${requiredLib} lib`, function () { - beforeEach(function () { - delete window.braintree; - this.sandbox.stub(ApplePayBraintree, 'libUrl').withArgs(requiredLib).returns('/api/mock-404'); - }); - - it('register an initialization error', function (done) { - const applePay = this.recurly.ApplePay(validOpts); - - applePay.on('error', (err) => { - nextTick(ensureDone(done, () => { - assert.equal(err, applePay.initError); - assertInitError(applePay, 'apple-pay-init-error'); - })); - }); - }); - }); - - describe(`when failed to create the ${requiredLib} instance`, function () { - beforeEach(function () { - window.braintree[requiredLib].create = sinon.stub().rejects('error'); - }); - - it('register an initialization error', function (done) { - const applePay = this.recurly.ApplePay(validOpts); - - applePay.on('error', (err) => { - nextTick(ensureDone(done, () => { - assert.equal(err, applePay.initError); - assertInitError(applePay, 'apple-pay-init-error'); - })); - }); - }); - }); - }); - it('assigns the braintree configuration', function (done) { const applePay = this.recurly.ApplePay(validOpts); diff --git a/test/unit/paypal/strategy/braintree.test.js b/test/unit/paypal/strategy/braintree.test.js index 4a263f393..d6d5c765c 100644 --- a/test/unit/paypal/strategy/braintree.test.js +++ b/test/unit/paypal/strategy/braintree.test.js @@ -1,23 +1,21 @@ import assert from 'assert'; -import each from 'component-each'; -import merge from 'lodash.merge'; -import { Recurly } from '../../../../lib/recurly'; import { initRecurly, stubBraintree, stubWindowOpen } from '../../support/helpers'; -describe(`BraintreeStrategy`, function () { +describe('BraintreeStrategy', function () { const validOpts = { braintree: { clientAuthorization: 'valid' } }; stubWindowOpen(); stubBraintree(); - beforeEach(function () { + beforeEach(function (done) { this.sandbox = sinon.createSandbox(); this.recurly = initRecurly(); this.paypal = this.recurly.PayPal(validOpts); + this.paypal.on('ready', done); }); describe('start', function () { @@ -26,7 +24,7 @@ describe(`BraintreeStrategy`, function () { this.paypal.start(); assert(this.paypal.strategy.paypal.tokenize.calledOnce); }); - }) + }); describe('destroy', function () { it('closes the window and removes listeners', function () { diff --git a/test/unit/risk/three-d-secure/strategy/braintree.test.js b/test/unit/risk/three-d-secure/strategy/braintree.test.js index 4f83a1574..a4c3afd2c 100644 --- a/test/unit/risk/three-d-secure/strategy/braintree.test.js +++ b/test/unit/risk/three-d-secure/strategy/braintree.test.js @@ -2,8 +2,8 @@ import assert from 'assert'; import { applyFixtures } from '../../../support/fixtures'; import { initRecurly, testBed } from '../../../support/helpers'; import BraintreeStrategy from '../../../../../lib/recurly/risk/three-d-secure/strategy/braintree'; +import BraintreeLoader from '../../../../../lib/util/braintree-loader'; import actionToken from '@recurly/public-api-test-server/fixtures/tokens/action-token-braintree.json'; -import Promise from 'promise'; describe('BraintreeStrategy', function () { this.ctx.fixture = 'threeDSecure'; @@ -25,6 +25,7 @@ describe('BraintreeStrategy', function () { }; this.braintree = { client: { + VERSION: BraintreeLoader.BRAINTREE_CLIENT_VERSION, create: sinon.stub().resolves() }, threeDSecure: { @@ -46,7 +47,7 @@ describe('BraintreeStrategy', function () { describe('when the braintree.js library encounters a load error', function () { beforeEach(function () { const { sandbox, threeDSecure } = this; - sandbox.replace(BraintreeStrategy.prototype, 'urlForResource', (f) => '/api/mock-404'); + sandbox.stub(BraintreeLoader, 'loadModules').rejects(); delete window.braintree; this.strategy = new BraintreeStrategy({ threeDSecure, actionToken }); }); @@ -57,7 +58,7 @@ describe('BraintreeStrategy', function () { assert.strictEqual(error.code, '3ds-vendor-load-error'); assert.strictEqual(error.vendor, 'Braintree'); done(); - }) + }); }); }); diff --git a/test/unit/support/helpers.js b/test/unit/support/helpers.js index db1d2ec44..3123a2b96 100644 --- a/test/unit/support/helpers.js +++ b/test/unit/support/helpers.js @@ -1,7 +1,7 @@ import bowser from 'bowser'; import merge from 'lodash.merge'; import { Recurly } from '../../../lib/recurly'; -import { BRAINTREE_CLIENT_VERSION } from '../../../lib/const/gateway-constants'; +import BraintreeLoader from '../../../lib/util/braintree-loader'; import Promise from 'promise'; @@ -62,7 +62,7 @@ export function stubBraintree () { window.braintree = { client: { - VERSION: BRAINTREE_CLIENT_VERSION, + VERSION: BraintreeLoader.BRAINTREE_CLIENT_VERSION, create }, venmo: { diff --git a/test/unit/venmo/strategy/braintree.test.js b/test/unit/venmo/strategy/braintree.test.js index a40efba6c..e9f02beec 100644 --- a/test/unit/venmo/strategy/braintree.test.js +++ b/test/unit/venmo/strategy/braintree.test.js @@ -5,16 +5,17 @@ import { stubWindowOpen } from '../../support/helpers'; -describe(`BraintreeStrategy`, function () { +describe('BraintreeStrategy', function () { const validOpts = { braintree: { clientAuthorization: 'valid' } }; stubWindowOpen(); stubBraintree(); - beforeEach(function () { + beforeEach(function (done) { this.sandbox = sinon.createSandbox(); this.recurly = initRecurly(); this.venmo = this.recurly.Venmo(validOpts); + this.venmo.on('ready', done); }); describe('start', function () { @@ -23,7 +24,7 @@ describe(`BraintreeStrategy`, function () { this.venmo.start(); assert(this.venmo.strategy.venmo.tokenize.calledOnce); }); - }) + }); describe('destroy', function () { it('closes the window and removes listeners', function () {