Skip to content

Commit

Permalink
Merge pull request #892 from recurly/google-pay-class
Browse files Browse the repository at this point in the history
chore: convert Google Pay element to a class from function
  • Loading branch information
chrissrogers authored Aug 15, 2024
2 parents e0f595f + 1a96a82 commit bca8e4f
Show file tree
Hide file tree
Showing 8 changed files with 147 additions and 107 deletions.
5 changes: 5 additions & 0 deletions lib/recurly/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import squish from 'string-squish';

const BASE_URL = 'https://dev.recurly.com/docs/recurly-js-';
const GOOGLE_PAY_ERRORS = [
{
code: 'google-pay-factory-only',
message: 'Google Pay must be initialized by calling recurly.GooglePay',
classification: 'merchant'
},
{
code: 'google-pay-not-available',
message: 'Google Pay is not available',
Expand Down
166 changes: 103 additions & 63 deletions lib/recurly/google-pay/google-pay.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ import Emitter from 'component-emitter';
import Promise from 'promise';
import { normalize } from '../../util/normalize';
import { FIELDS as TOKEN_FIELDS } from '../token';
import recurlyError from '../errors';
import errors from '../errors';
import { payWithGoogle } from './pay-with-google';

const debug = require('debug')('recurly:google-pay');

const API_VERSION = { apiVersion: 2, apiVersionMinor: 0 };

const getRecurlyInputsFromHtmlForm = ({ $form, inputNames }) => $form ? normalize($form, inputNames, { parseCard: false }).values : {};

const getBillingAddressFromGoogle = ({ paymentData }) => {
const transformAddress = ({ paymentData }) => {
const googleBillingAddress = paymentData?.paymentMethodData?.info?.billingAddress || {};
const {
name,
Expand Down Expand Up @@ -39,24 +37,9 @@ const getBillingAddressFromGoogle = ({ paymentData }) => {
};
};

const createRecurlyToken = ({ recurly, $form, paymentData, gatewayCodeSelected }) => {
const userInputs = getRecurlyInputsFromHtmlForm({ $form, inputNames: TOKEN_FIELDS });
const userBillingAddress = getBillingAddressFromGoogle({ paymentData });
const userInputsOverrideBillingAddress = Object.keys(userInputs).some(k => k in userBillingAddress);

const data = {
gateway_code: gatewayCodeSelected,
...userInputs,
...(!userInputsOverrideBillingAddress && userBillingAddress),
paymentData,
};

return recurly.request.post({ route: '/google_pay/token', data });
};

const validateRecurlyMerchantInfo = ({ recurlyMerchantInfo }) => {
const validateRecurlyMerchantInfo = (recurlyMerchantInfo) => {
if (recurlyMerchantInfo.paymentMethods.length === 0) {
throw recurlyError('google-pay-not-configured');
throw errors('google-pay-not-configured');
}

return recurlyMerchantInfo;
Expand Down Expand Up @@ -91,7 +74,7 @@ const buildIsReadyToPayRequest = (paymentMethods, { billingAddressRequired }) =>
};
};

const getGoogleInfoFromMerchantInfo = ({ recurlyMerchantInfo, options }) => {
const getGoogleInfoFromMerchantInfo = (recurlyMerchantInfo, options) => {
const { siteMode, paymentMethods } = recurlyMerchantInfo;
const { environment: envOpt, } = options;
const environment = envOpt || (siteMode === 'production' ? 'PRODUCTION' : 'TEST');
Expand Down Expand Up @@ -137,67 +120,124 @@ const getGoogleInfoFromMerchantInfo = ({ recurlyMerchantInfo, options }) => {
};
};

const buildPaymentDataRequest = ({ recurly, options }) => {
const buildPaymentDataRequest = (googlePay, options) => {
const { recurly } = googlePay;
return new Promise((resolve, reject) => {
const data = {
gateway_code: options.gatewayCode,
currency: options.currency ?? options.paymentDataRequest?.transactionInfo.currencyCode,
country: options.country ?? options.paymentDataRequest?.transactionInfo.countryCode,
};

if (!data.currency) return reject(recurlyError('google-pay-config-missing', { opt: 'currency' }));
if (!data.country) return reject(recurlyError('google-pay-config-missing', { opt: 'country' }));
if (!data.currency) return reject(errors('google-pay-config-missing', { opt: 'currency' }));
if (!data.country) return reject(errors('google-pay-config-missing', { opt: 'country' }));

resolve(data);
}).then(data => recurly.request.get({ route: '/google_pay/info', data }))
.then(recurlyMerchantInfo => validateRecurlyMerchantInfo({ recurlyMerchantInfo, options }))
.then(recurlyMerchantInfo => getGoogleInfoFromMerchantInfo({ recurlyMerchantInfo, options }));
.then(recurlyMerchantInfo => validateRecurlyMerchantInfo(recurlyMerchantInfo))
.then(recurlyMerchantInfo => getGoogleInfoFromMerchantInfo(recurlyMerchantInfo, options));
};

const googlePay = (recurly, options) => {
const emitter = new Emitter();
const handleErr = err => emitter.emit('error', err);
let gatewayCodeSelected;
export class GooglePay extends Emitter {
constructor (options) {
super();

this._ready = false;
this.config = {};
this.options = options;
this.once('ready', () => this._ready = true);

const onPaymentAuthorized = (paymentData) => {
return createRecurlyToken({ recurly, paymentData, gatewayCodeSelected, $form: options.form })
this.configure({ ...options });
}

/**
* Initialized state callback registry
*
* @param {Function} cb callback
* @public
*/
ready (cb) {
if (this._ready) cb();
else this.once('ready', cb);
}

/**
* Configures a new instance
*
* @param {Object} options
* @emit 'ready'
* @private
*/
configure (options) {
if (options.recurly) this.recurly = options.recurly;
else throw errors('google-pay-factory-only');

if (options.form) this.config.form = options.form;
if (options.callbacks) this.config.callbacks = options.callbacks;

return buildPaymentDataRequest(this, options)
.then(({ gatewayCode, environment, isReadyToPayRequest, paymentDataRequest }) => {
this.config.gatewayCode = gatewayCode;

return this.createButton({
paymentOptions: {
environment,
merchantInfo: paymentDataRequest.merchantInfo,
paymentDataCallbacks: this.config.callbacks,
},
isReadyToPayRequest,
paymentDataRequest,
buttonOptions: {
...options.buttonOptions,
onClick: this.token.bind(this),
onError: (err) => this.emit('error', errors('google-pay-payment-failure', err)),
},
});
})
.then(button => this.emit('ready', button))
.catch(err => this.emit('error', err));
}

/**
* Creates the Google Pay button that will trigger the payment flow
*
* @public
*/
createButton (payWithGoogleRequest) {
return payWithGoogle(payWithGoogleRequest);
}

token (paymentData) {
const data = this.mapPaymentData(paymentData);
debug('paymentData received', paymentData);

return this.recurly.request.post({ route: '/google_pay/token', data })
.catch(err => {
handleErr(recurlyError('google-pay-payment-failure', err));
debug('tokenization error', err);
throw err;
})
.then(token => {
emitter.emit('token', token, paymentData);
debug('Token received', token);
this.emit('token', token, paymentData);

paymentData.recurlyToken = token;
debug('GooglePay.onPaymentAuthorized', paymentData);
emitter.emit('paymentAuthorized', paymentData);
return paymentData;
});
};
debug('onPaymentAuthorized', paymentData);
this.emit('paymentAuthorized', paymentData);

buildPaymentDataRequest({ recurly, options })
.then(({ gatewayCode, environment, isReadyToPayRequest, paymentDataRequest }) => {
gatewayCodeSelected = gatewayCode;

return payWithGoogle({
paymentOptions: {
environment,
merchantInfo: paymentDataRequest.merchantInfo,
paymentDataCallbacks: options.callbacks,
},
isReadyToPayRequest,
paymentDataRequest,
buttonOptions: {
...options.buttonOptions,
onClick: onPaymentAuthorized,
onError: (err) => handleErr(recurlyError('google-pay-payment-failure', err)),
},
return paymentData;
});
})
.then(button => emitter.emit('ready', button))
.catch(handleErr);
}

return emitter;
};
mapPaymentData (paymentData) {
const formAddress = this.config.form ? normalize(this.config.form, TOKEN_FIELDS, { parseCard: false }).values : {};
const googleBillingAddress = transformAddress({ paymentData });
const useGoogleAddress = Object.keys(formAddress).some(k => k in googleBillingAddress);

export { googlePay };
return {
gateway_code: this.config.gatewayCode,
...formAddress,
...(!useGoogleAddress && googleBillingAddress),
paymentData,
};
}
}
6 changes: 3 additions & 3 deletions lib/recurly/google-pay/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { googlePay } from './google-pay';
import { GooglePay } from './google-pay';

/**
* Returns a GooglePay instance.
Expand All @@ -7,7 +7,7 @@ import { googlePay } from './google-pay';
* @return {GooglePay}
*/
export function factory (options) {
const recurly = this;
const factoryClass = GooglePay;

return googlePay(recurly, options);
return new factoryClass(Object.assign({}, options, { recurly: this }));
}
6 changes: 3 additions & 3 deletions lib/recurly/google-pay/pay-with-google.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ function createButton (
},
});

debug('GooglePay.createButton', paymentDataRequest, buttonOptions);
debug('createButton', paymentDataRequest, buttonOptions);
return googlePayClient.createButton({
...buttonOptions,
allowedPaymentMethods,
Expand Down Expand Up @@ -68,10 +68,10 @@ const payWithGoogle = ({ paymentOptions, isReadyToPayRequest, paymentDataRequest
let googlePayClient;
return loadGooglePayLib()
.then(() => {
debug('GooglePay.newPaymentsClient', paymentOptions);
debug('newPaymentsClient', paymentOptions);
googlePayClient = new window.google.payments.api.PaymentsClient(paymentOptions);

debug('GooglePay.isReadyToPay', isReadyToPayRequest);
debug('isReadyToPay', isReadyToPayRequest);
return googlePayClient.isReadyToPay(isReadyToPayRequest);
})
.catch(err => {
Expand Down
2 changes: 0 additions & 2 deletions test/e2e/support/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,8 +168,6 @@ function init ({ fixture = '', opts = {} } = {}) {
* @return {Promise}
*/
async function configureRecurly (opts = {}) {
console.log('configureRecurly', opts);

return await browser.executeAsync(function (opts, done) {
recurly.configure(opts);
recurly.ready(function () {
Expand Down
1 change: 0 additions & 1 deletion test/unit/apple-pay.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ 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';

const infoFixture = require('@recurly/public-api-test-server/fixtures/apple_pay/info');
Expand Down
Loading

0 comments on commit bca8e4f

Please sign in to comment.