Skip to content

Commit

Permalink
Support Venmo Pay on Braintree
Browse files Browse the repository at this point in the history
  • Loading branch information
calebbarde committed Feb 19, 2021
1 parent 98ca564 commit f9806fe
Show file tree
Hide file tree
Showing 9 changed files with 6,540 additions and 8,084 deletions.
2 changes: 2 additions & 0 deletions lib/recurly.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { factory as ApplePay } from './recurly/apple-pay';
import { factory as Elements } from './recurly/elements';
import { factory as Frame } from './recurly/frame';
import { factory as PayPal } from './recurly/paypal';
import { factory as Venmo } from './recurly/venmo';
import { factory as Risk } from './recurly/risk';
import { deprecated as deprecatedPaypal } from './recurly/paypal/strategy/direct';
import { Bus } from './recurly/bus';
Expand Down Expand Up @@ -106,6 +107,7 @@ export class Recurly extends Emitter {
giftcard = giftCard; // DEPRECATED
item = item;
PayPal = PayPal;
Venmo = Venmo;
paypal = deprecatedPaypal;
plan = plan;
Risk = Risk;
Expand Down
20 changes: 20 additions & 0 deletions lib/recurly/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,11 @@ const ERRORS = [
message: 'PayPal must be initialized by calling recurly.PayPal',
classification: 'merchant'
},
{
code: 'venmo-factory-only',
message: 'Venmo must be initialized by calling recurly.Venmo',
classification: 'merchant'
},
{
code: 'paypal-config-missing',
message: c => `Missing PayPal configuration option: '${c.opt}'`,
Expand Down Expand Up @@ -201,6 +206,21 @@ const ERRORS = [
message: 'Braintree API experienced an error',
classification: 'internal'
},
{
code: 'venmo-braintree-api-error',
message: 'Braintree API experienced an error',
classification: 'internal'
},
{
code: 'venmo-braintree-tokenize-braintree-error',
message: 'An error occurred while attempting to generate the Braintree token',
classification: 'internal'
},
{
code: 'venmo-braintree-tokenize-recurly-error',
message: 'An error occurred while attempting to generate the Braintree token within Recurly',
classification: 'internal'
},
{
code: 'paypal-braintree-tokenize-braintree-error',
message: 'An error occurred while attempting to generate the Braintree token',
Expand Down
64 changes: 64 additions & 0 deletions lib/recurly/venmo/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import Emitter from 'component-emitter';
import { BraintreeStrategy } from './strategy/braintree';

/**
* Venmo instantiation factory
*
* @param {Object} options
* @return {Venmo}
*/
export function factory (options) {
options = Object.assign({}, options, { recurly: this });
return new Venmo(options);
}

const DEFERRED_EVENTS = [
'ready',
'token',
'error',
'cancel'
];

/**
* Venmo strategy interface
*/
class Venmo extends Emitter {
constructor (options) {
super();
this.isReady = false;
this.options = options;

this.once('ready', () => this.isReady = true);

this.strategy = new BraintreeStrategy(options);

this.bindStrategy();
}

ready (done) {
if (this.isReady) done();
else this.once('ready', done);
}

start (...args) {
return this.strategy.start(...args);
}

destroy () {
const { strategy } = this;
if (strategy) {
strategy.destroy();
delete this.strategy;
}
this.off();
}

/**
* Binds external interface events to those on the strategy
*
* @private
*/
bindStrategy () {
DEFERRED_EVENTS.forEach(ev => this.strategy.on(ev, this.emit.bind(this, ev)));
}
}
122 changes: 122 additions & 0 deletions lib/recurly/venmo/strategy/braintree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import loadScript from 'load-script';
import after from '../../../util/after';
import { VenmoStrategy } from './index';
import { normalize } from '../../../util/normalize';

export const BRAINTREE_CLIENT_VERSION = '3.50.0';

const debug = require('debug')('recurly:paypal:strategy:braintree');

/**
* Braintree-specific Venmo handler
*/

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;
}

/**
* Loads Braintree client and modules
*
* @todo semver client detection
*/
load ({ form }) {
debug('loading Braintree libraries');
this.form = 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);
}

/**
* Initializes a Braintree client, device data collection module, and venmo client
*/
initialize () {
debug('Initializing Braintree client');

const authorization = this.config.clientAuthorization;
const braintree = window.braintree;

braintree.client.create({ authorization }, (error, client) => {
if (error) return this.fail('venmo-braintree-api-error', { cause: error });
debug('Braintree client created');
braintree.venmo.create({ client }, (error, venmo) => {
if (error) return this.fail('venmo-braintree-api-error', { cause: error });
debug('Venmo client created');
this.venmo = venmo;
this.emit('ready');
});
});
}

handleVenmoError (err) {
if (err.code === 'VENMO_CANCELED') {
console.log('App is not available or user aborted payment flow');
} else if (err.code === 'VENMO_APP_CANCELED') {
if (err.code === 'VENMO_POPUP_CLOSED')
console.log('User canceled payment flow');
} else {
console.error('An error occurred:', err.message);
}

this.emit('cancel');
return this.error('venmo-braintree-tokenize-braintree-error', { cause: err });
}

handleVenmoSuccess (payload) {
const nameData = normalize(this.form, ['first_name', 'last_name']);
this.recurly.request.post({
route: '/venmo/token',
data: { type: 'braintree', payload: { ...payload, ...nameData } },
done: (error, token) => {
if (error) return this.error('venmo-braintree-tokenize-recurly-error', { cause: error });
this.emit('token', token);
}
});
}

start () {
// Tokenize with Braintree
this.venmo.tokenize()
.then(this.handleVenmoSuccess.bind(this))
.catch(this.handleVenmoError.bind(this));
}

destroy () {
if (this.close) {
this.close();
}
this.off();
}

braintreeClientAvailable (module) {
const bt = window.braintree;
return bt && bt.client && bt.client.VERSION === BRAINTREE_CLIENT_VERSION && (module ? module in bt : true);
}
}
96 changes: 96 additions & 0 deletions lib/recurly/venmo/strategy/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import Emitter from 'component-emitter';
import { Recurly } from '../../../recurly';
import errors from '../../errors';

const debug = require('debug')('recurly:paypal:strategy');

/**
* Venmo base interface strategy
*
* @abstract
*/
export class VenmoStrategy extends Emitter {
constructor (options) {
super();
this.isReady = false;
this.config = {};

this.once('ready', () => this.isReady = true);

this.configure(options[0]);
}

ready (done) {
if (this.isReady) done();
else this.once('ready', done);
}

configure (options) {
if (!(options.recurly instanceof Recurly)) throw this.error('venmo-factory-only');
this.recurly = options.recurly;
}

initialize () {
debug("Method 'initialize' not implemented");
}

/**
* Starts the PayPal flow
* > must be on the call chain with a user interaction (click, touch) on it
*/
start () {
debug("Method 'start' not implemented");
}

cancel () {
this.emit('cancel');
}

/**
* Registers or immediately invokes a failure handler
*
* @param {Function} done Failure handler
*/
onFail (done) {
if (this.failure) done();
else this.once('fail', done);
}

/**
* Logs and announces a failure to initialize a strategy
*
* @private
* @param {String} reason
* @param {Object} [options]
*/
fail (reason, options) {
if (this.failure) return;
debug('Failure scenario encountered', reason, options);
const failure = this.failure = this.error(reason, options);
this.emit('fail', failure);
}

/**
* Creates and emits a RecurlyError
*
* @protected
* @param {...Mixed} params to be passed to the Recurlyerror factory
* @return {RecurlyError}
* @emit 'error'
*/
error (...params) {
let err = params[0] instanceof Error ? params[0] : errors(...params);
this.emit('error', err);
return err;
}

/**
* Updates price information from a Pricing instance
*
* @private
*/
updatePriceFromPricing () {
this.config.display.amount = this.pricing.totalNow;
this.config.display.currency = this.pricing.currencyCode;
}
}
Loading

0 comments on commit f9806fe

Please sign in to comment.