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): populate billingContact with form address fields #797

Merged
merged 1 commit into from
Mar 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
25 changes: 16 additions & 9 deletions lib/recurly/apple-pay/apple-pay.braintree.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,22 +59,29 @@ export class ApplePayBraintree extends ApplePay {
.catch(err => this.error(err));
}

token (event, applePayPayment) {
token (event) {
debug('Creating token');

this.braintree.applePay
.tokenize({ token: event.payment.token })
.then(tokenizePayload => super.token(event, {
type: 'braintree',
payload: {
deviceData: this.braintree.dataCollector.deviceData,
applePayPayment,
tokenizePayload
}
}))
.then(braintreeToken => {
event.payment.gatewayToken = braintreeToken;
return super.token(event);
})
.catch(err => this.error('apple-pay-payment-failure', err));
}

mapPaymentData (event) {
return {
type: 'braintree',
payload: {
deviceData: this.braintree.dataCollector.deviceData,
tokenizePayload: event.payment.gatewayToken,
applePayPayment: super.mapPaymentData(event),
},
};
}

onCancel (event) {
debug('Teardown payment', event);

Expand Down
88 changes: 41 additions & 47 deletions lib/recurly/apple-pay/apple-pay.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,14 @@
import Emitter from 'component-emitter';
import errors from '../errors';
import { Pricing } from '../pricing';
import { FIELDS } from '../token';
import PricingPromise from '../pricing/promise';
import { normalize } from '../../util/normalize';
import buildApplePayPaymentRequest from './util/build-apple-pay-payment-request';
import { lineItem } from './util/apple-pay-line-item';
import buildApplePayPaymentRequest from './util/build-apple-pay-payment-request';
import { transformAddress, normalizeForm } from './util/transform-address';

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

const MINIMUM_SUPPORTED_VERSION = 4;
const APPLE_PAY_ADDRESS_MAP = {
first_name: 'givenName',
last_name: 'familyName',
address1: 'addressLines',
address2: 'addressLines',
city: 'locality',
state: 'administrativeArea',
postal_code: 'postalCode',
country: 'countryCode'
};

const I18N = {
subtotalLineItemLabel: 'Subtotal',
Expand Down Expand Up @@ -72,10 +61,26 @@ export class ApplePay extends Emitter {
*/
get session () {
if (this._session) return this._session;
let paymentRequest = this._paymentRequest;

if (this.config.form) {
const { billingContact, shippingContact } = paymentRequest;
const {
billingContact: formBillingContact,
shippingContact: formShippingContact, tokenData,
} = normalizeForm(this.config.form);
this.tokenData = tokenData;

paymentRequest = {
...paymentRequest,
...(!billingContact && formBillingContact && { billingContact: formBillingContact }),
...(!shippingContact && formShippingContact && { shippingContact: formShippingContact }),
};
}

debug('Creating new Apple Pay session', this._paymentRequest);
debug('Creating new Apple Pay session', paymentRequest);

const session = new window.ApplePaySession(MINIMUM_SUPPORTED_VERSION, this._paymentRequest);
const session = new window.ApplePaySession(MINIMUM_SUPPORTED_VERSION, paymentRequest);
session.onvalidatemerchant = this.onValidateMerchant.bind(this);
session.onshippingcontactselected = this.onShippingContactSelected.bind(this);
session.onshippingmethodselected = this.onShippingMethodSelected.bind(this);
Expand Down Expand Up @@ -247,6 +252,8 @@ export class ApplePay extends Emitter {
onPaymentMethodSelected (event) {
debug('Payment method selected', event);

this.emit('paymentMethodSelected', event);

this.session.completePaymentMethodSelection({
newTotal: this.finalTotalLineItem,
newLineItems: this.lineItems,
Expand Down Expand Up @@ -297,20 +304,14 @@ export class ApplePay extends Emitter {
onPaymentAuthorized (event) {
debug('Payment authorization received', event);

let data = {};

if (this.config.form) {
data = normalize(this.config.form, FIELDS, { parseCard: false }).values;
}

this.emit('paymentAuthorized', event);

this.mapPaymentData(data, event.payment);

return this.token(event, data);
return this.token(event);
}

token (event, data) {
token (event) {
const data = this.mapPaymentData(event);

this.recurly.request.post({
route: '/apple_pay/token',
data,
Expand Down Expand Up @@ -356,30 +357,23 @@ export class ApplePay extends Emitter {
return err;
}


/**
* Maps data from the Apple Pay token into the inputs
* object that is sent to RA for tokenization
/*
* Maps data from the Apple Pay token into the token input
* object that is sent to RA for toenization
*
* @private
*/
mapPaymentData (inputs, data) {
inputs.paymentData = data.token.paymentData;
inputs.paymentMethod = data.token.paymentMethod;

if (!data.billingContact) return;
if (Object.keys(APPLE_PAY_ADDRESS_MAP).some(field => inputs[field])) return;

FIELDS.forEach(field => {
if (!APPLE_PAY_ADDRESS_MAP[field]) return;

let tokenData = data.billingContact[APPLE_PAY_ADDRESS_MAP[field]];

// address lines are an array from Apple Pay
if (field === 'address1') tokenData = tokenData[0];
else if (field === 'address2') tokenData = tokenData[1];

inputs[field] = tokenData;
});
mapPaymentData (event) {
const {
billingContact,
token: { paymentData, paymentMethod },
} = event.payment;

return {
paymentData,
paymentMethod,
...(this.tokenData && this.tokenData),
...transformAddress(billingContact, { to: 'address', except: ['emailAddress'] }),
};
}
}
74 changes: 74 additions & 0 deletions lib/recurly/apple-pay/util/transform-address.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import isEmpty from 'lodash.isempty';
import { normalize } from '../../../util/normalize';
import { ADDRESS_FIELDS, NON_ADDRESS_FIELDS } from '../../token';

const BILLING_CONTACT_MAP = {
first_name: 'givenName',
last_name: 'familyName',
address1: { field: 'addressLines', index: 0 },
address2: { field: 'addressLines', index: 1 },
city: 'locality',
state: 'administrativeArea',
postal_code: 'postalCode',
country: 'countryCode',
};

const SHIPPING_CONTACT_MAP = {
email: 'emailAddress',
phone: 'phoneNumber',
};

const CONTACT_MAP = {
...BILLING_CONTACT_MAP,
...SHIPPING_CONTACT_MAP,
};

/**
* Transforms a source address type to another address type.
* @param {Object} source either an Address or ApplePayPaymentContact
* @param {Object} options transform options
* @param {string} options.to either 'contact' or 'address'
* @param {string} options.except properties to exclude
* @return {Object} the transform result
*/
export function transformAddress (source, { to = 'contact', except = [], map = CONTACT_MAP }) {
if (isEmpty(source)) return {};

return Object.keys(map).reduce((target, addressField) => {
const contactField = map[addressField];
const sourceField = to === 'contact' ? addressField : contactField;
const targetField = to === 'address' ? addressField : contactField;

if (~except.indexOf(sourceField)) return target;

const sourceValue = typeof sourceField === 'object'
? source[sourceField.field]?.[sourceField.index]
: source[sourceField];
if (!sourceValue) return target;

if (typeof targetField === 'object') {
const { field, index } = targetField;
target[field] = target[field] || [];
target[field][index] = sourceValue;
} else {
target[targetField] = sourceValue;
}

return target;
}, {});
}

export function normalizeForm (form) {
if (!form) return {};

const address = normalize(form, ADDRESS_FIELDS, { parseCard: false }).values;
const billingContact = transformAddress(address, { map: BILLING_CONTACT_MAP });
const shippingContact = transformAddress(address, { map: SHIPPING_CONTACT_MAP });
const tokenData = normalize(form, NON_ADDRESS_FIELDS, { parseCard: false }).values;

return {
billingContact,
shippingContact,
tokenData,
};
}
26 changes: 17 additions & 9 deletions lib/recurly/token.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,7 @@ import { validateCardInputs } from './validate';

const debug = require('debug')('recurly:token');

/**
* Fields that are sent to API.
*
* @type {Array}
* @private
*/

export const FIELDS = [
export const ADDRESS_FIELDS = [
'first_name',
'last_name',
'address1',
Expand All @@ -26,11 +19,26 @@ export const FIELDS = [
'state',
'postal_code',
'phone',
];

export const NON_ADDRESS_FIELDS = [
'vat_number',
'tax_identifier',
'tax_identifier_type',
'fraud_session_id',
'token'
'token',
];

/**
* Fields that are sent to API.
*
* @type {Array}
* @private
*/

export const FIELDS = [
...ADDRESS_FIELDS,
...NON_ADDRESS_FIELDS,
];

/**
Expand Down
13 changes: 13 additions & 0 deletions test/types/apple-pay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@ export default function applePay() {
total: { label: 'My Subscription', amount: '29.00' },
lineItems: [{ label: 'Subtotal', amount: '1.00' }],
requiredShippingContactFields: ['email', 'phone'],
billingContact: {
givenName: 'Emmet',
familyName: 'Brown',
addressLines: ['1640 Riverside Drive', 'Suite 1'],
locality: 'Hill Valley',
administrativeArea: 'CA',
postalCode: '91103',
countryCode: 'US'
},
shippingContact: {
phoneNumber: '1231231234',
emailAddress: 'ebrown@example.com'
},
pricing: window.recurly.Pricing.Checkout()
});

Expand Down
Loading