Skip to content

Commit

Permalink
feat(apple pay): populate billing/shippingContact with form address f…
Browse files Browse the repository at this point in the history
…ields

Apple Pay on the Web supports supplying a `billingContact` and
`shippingContact` when creating the session. If provided, it would
populate over the customer's default billing contact on the payment
sheet.

If the `form` option is supplied, use the address fields that are
populated to populate the `billingContact` and the shipping fields
that are populated to populate the `shippingContact`.

This also cleans up the interaction between the `form` and the token.
Currently, if any of the fields on the `form` are present, we prefer
that address instead of what is presented and modified on the payment
card. This changes that functionality to always prefer the
`billingContact` from the payment card when tokenizing the card as
that is what the customer sees when authorizing the payment.
  • Loading branch information
cbarton committed Mar 29, 2023
1 parent dd36259 commit edbd256
Show file tree
Hide file tree
Showing 8 changed files with 371 additions and 168 deletions.
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'] }),
};
}
}
75 changes: 75 additions & 0 deletions lib/recurly/apple-pay/util/transform-address.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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 {};

const target = {};

for (const [addressField, contactField] of Object.entries(map)) {
const sourceField = to === 'contact' ? addressField : contactField;
const targetField = to === 'address' ? addressField : contactField;

if (except.includes(sourceField)) continue;

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

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

0 comments on commit edbd256

Please sign in to comment.