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

v6 - Adding isExpress flag for GooglePay and ApplePay #2597

Merged
merged 7 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 6 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
26 changes: 26 additions & 0 deletions packages/lib/src/components/ApplePay/ApplePay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,27 @@ beforeEach(() => {
};
});

const configurationMock = {
merchantName: 'TestMerchant',
merchantId: 'test-merchant'
};

describe('ApplePay', () => {
describe('isExpress flag', () => {
test('should add subtype: express when isExpress is configured', () => {
const applepay = new ApplePay(global.core, { isExpress: true });
expect(applepay.data.paymentMethod).toHaveProperty('subtype', 'express');
});
test('should not add subtype: express when isExpress is omitted', () => {
const applepay = new ApplePay(global.core);
expect(applepay.data.paymentMethod).not.toHaveProperty('subtype', 'express');
});

test('should throw error when express callbacks are passed but isExpress flag is not set', () => {
expect(() => new ApplePay(global.core, { onShippingContactSelected: jest.fn() })).toThrow();
});
});

describe('submit()', () => {
test('should forward apple pay error (if available) to ApplePay if payment fails', async () => {
const onPaymentFailedMock = jest.fn();
Expand All @@ -32,6 +52,7 @@ describe('ApplePay', () => {
});

const applepay = new ApplePay(global.core, {
configuration: configurationMock,
amount: { currency: 'EUR', value: 2000 },
onPaymentFailed: onPaymentFailedMock,
onSubmit(state, component, actions) {
Expand Down Expand Up @@ -77,6 +98,7 @@ describe('ApplePay', () => {
});

const applepay = new ApplePay(global.core, {
configuration: configurationMock,
amount: { currency: 'EUR', value: 2000 },
onPaymentFailed: onPaymentFailedMock,
onSubmit(state, component, actions) {
Expand Down Expand Up @@ -128,6 +150,7 @@ describe('ApplePay', () => {
});

const applepay = new ApplePay(global.core, {
configuration: configurationMock,
amount: { currency: 'EUR', value: 2000 },
onOrderTrackingRequest: onOrderTrackingRequestMock,
onPaymentCompleted: onPaymentCompletedMock
Expand Down Expand Up @@ -178,6 +201,7 @@ describe('ApplePay', () => {
});

const applepay = new ApplePay(global.core, {
configuration: configurationMock,
amount: { currency: 'EUR', value: 2000 },
onOrderTrackingRequest: onOrderTrackingRequestMock,
onPaymentCompleted: onPaymentCompletedMock
Expand Down Expand Up @@ -222,6 +246,7 @@ describe('ApplePay', () => {
});

const applepay = new ApplePay(global.core, {
configuration: configurationMock,
amount: { currency: 'EUR', value: 2000 },
onOrderTrackingRequest: onOrderTrackingRequestMock,
onPaymentCompleted: onPaymentCompletedMock
Expand Down Expand Up @@ -298,6 +323,7 @@ describe('ApplePay', () => {
});

const applepay = new ApplePay(global.core, {
configuration: configurationMock,
amount: { currency: 'EUR', value: 2000 },
onAuthorized: onAuthorizedMock,
onChange: onChangeMock,
Expand Down
21 changes: 17 additions & 4 deletions packages/lib/src/components/ApplePay/ApplePay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { DecodeObject } from '../../types/global-types';
import { TxVariants } from '../tx-variants';
import { sanitizeResponse, verifyPaymentDidNotFail } from '../internal/UIElement/utils';
import { ANALYTICS_INSTANT_PAYMENT_BUTTON, ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants';

import type { ApplePayConfiguration, ApplePayElementData, ApplePayPaymentOrderDetails, ApplePaySessionRequest } from './types';
import type { ICore } from '../../core/types';
import type { PaymentResponseData, RawPaymentResponse } from '../../types/global-types';
Expand All @@ -25,6 +26,16 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> {

constructor(checkout: ICore, props?: ApplePayConfiguration) {
super(checkout, props);

const { isExpress, onShippingContactSelected, onShippingMethodSelected } = this.props;

if (isExpress === false && (onShippingContactSelected || onShippingMethodSelected)) {
throw new AdyenCheckoutError(
'IMPLEMENTATION_ERROR',
'ApplePay - You must set "isExpress" flag to "true" in order to use "onShippingContactSelected" and/or "onShippingMethodSelected" callbacks'
);
}

this.startSession = this.startSession.bind(this);
this.submit = this.submit.bind(this);
this.validateMerchant = this.validateMerchant.bind(this);
Expand All @@ -35,7 +46,7 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> {
/**
* Formats the component props
*/
protected formatProps(props) {
protected override formatProps(props) {
ribeiroguilherme marked this conversation as resolved.
Show resolved Hide resolved
const version = props.version || resolveSupportedVersion(latestSupportedVersion);
const supportedNetworks = props.brands?.length ? mapBrands(props.brands) : props.supportedNetworks;

Expand All @@ -51,20 +62,22 @@ class ApplePayElement extends UIElement<ApplePayConfiguration> {
/**
* Formats the component data output
*/
protected formatData(): ApplePayElementData {
protected override formatData(): ApplePayElementData {
const { applePayToken, billingAddress, deliveryAddress } = this.state;
const { isExpress } = this.props;

return {
paymentMethod: {
type: ApplePayElement.type,
applePayToken
applePayToken,
...(isExpress && { subtype: 'express' })
},
...(billingAddress && { billingAddress }),
...(deliveryAddress && { deliveryAddress })
};
}

public submit = (): void => {
public override submit = (): void => {
// Analytics
if (this.props.isInstantPayment) {
this.submitAnalytics({ type: ANALYTICS_SELECTED_STR, target: ANALYTICS_INSTANT_PAYMENT_BUTTON });
Expand Down
84 changes: 5 additions & 79 deletions packages/lib/src/components/ApplePay/defaultProps.ts
Original file line number Diff line number Diff line change
@@ -1,89 +1,15 @@
const defaultProps = {
// Transaction Information
amount: { currency: 'USD', value: 0 },

/**
* The merchant’s two-letter ISO 3166 country code.
*/
countryCode: 'US',
import { ApplePayConfiguration } from './types';

const defaultProps: ApplePayConfiguration = {
isExpress: false,
amount: { currency: 'USD', value: 0 },
totalPriceStatus: 'final',
totalPriceLabel: undefined,

configuration: {
merchantName: '',
merchantId: ''
},

initiative: 'web',

/**
* https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentrequest/1916120-lineitems
* A set of line items that explain recurring payments and additional charges and discounts.
*/
lineItems: undefined,

/**
* https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentrequest/1916123-merchantcapabilities
* The payment capabilities supported by the merchant.
*/
merchantCapabilities: ['supports3DS'],

/**
* https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentrequest/1916121-shippingmethods
* A list of available methods for shipping physical goods.
*/
shippingMethods: undefined,

/**
* https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentrequest/1916128-shippingtype
* An optional value that indicates how purchased items are to be shipped.
*/
shippingType: undefined,

/**
* https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentrequest/2928612-supportedcountries
* A list of two-character country codes you provide, used to limit payments to cards from specific countries.
*/
supportedCountries: undefined,

/**
* https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentrequest/1916122-supportednetworks
* The payment networks supported by the merchant.
*/
supportedNetworks: ['amex', 'discover', 'masterCard', 'visa'],

// Requested Billing and Shipping Contact Information

/**
* The fields of billing information that you require from the user to process the transaction.
*/
requiredBillingContactFields: undefined,

/**
* The fields of shipping information that you require from the user to fulfill the order.
*/
requiredShippingContactFields: undefined,

// Known Contact Information

billingContact: undefined, // Billing contact information for the user.
shippingContact: undefined, // Shipping contact information for the user.

// Custom Data

applicationData: undefined, // A Base64-encoded string used to contain your application-specific data.

// Events
onClick: resolve => resolve(),
onPaymentMethodSelected: null,
onShippingContactSelected: null,
onShippingMethodSelected: null,

// ButtonOptions
buttonType: 'plain',
buttonColor: 'black',
showPayButton: true // show or hide the Apple Pay button
onClick: resolve => resolve()
};

export default defaultProps;
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('preparePaymentRequest', () => {
test('works with defaultProps', () => {
const paymentRequest = preparePaymentRequest({
...defaultProps,
countryCode: 'US',
companyName: 'Test'
});

Expand Down
16 changes: 9 additions & 7 deletions packages/lib/src/components/ApplePay/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ export type ApplePayButtonType =
| 'top-up';

export interface ApplePayConfiguration extends UIElementProps {
/**
* Enables the ApplePay Express Flow
* @defaultValue false
*/
isExpress?: boolean;

/**
* The Apple Pay version number your website supports.
* @default highest supported version by the shopper device
Expand Down Expand Up @@ -196,20 +202,16 @@ export interface ApplePayConfiguration extends UIElementProps {
// ButtonOptions
buttonColor?: 'black' | 'white' | 'white-with-line';
buttonType?: ApplePayButtonType;

/**
* Show or hide the Apple Pay button
*/
showPayButton?: boolean;
}

export interface ApplePayElementData {
paymentMethod: {
type: string;
applePayToken: string;
billingAddress?: AddressData;
deliveryAddress?: AddressData;
isExpress?: boolean;
};
billingAddress?: AddressData;
deliveryAddress?: AddressData;
}

export interface ApplePaySessionRequest {
Expand Down
31 changes: 23 additions & 8 deletions packages/lib/src/components/GooglePay/GooglePay.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,21 @@ beforeEach(() => {
});

describe('GooglePay', () => {
describe('isExpress flag', () => {
test('should add subtype: express when isExpress is configured', () => {
const googlepay = new GooglePay(global.core, { isExpress: true });
expect(googlepay.data.paymentMethod).toHaveProperty('subtype', 'express');
});
test('should not add subtype: express when isExpress is omitted', () => {
const googlepay = new GooglePay(global.core);
expect(googlepay.data.paymentMethod).not.toHaveProperty('subtype', 'express');
});

test('should throw error when express callbacks are passed but isExpress flag is not set', () => {
expect(() => new GooglePay(global.core, { paymentDataCallbacks: { onPaymentDataChanged: jest.fn() } })).toThrow();
});
Comment on lines +68 to +70

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be great to see this documented somewhere. There's no mention about it on your documentation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @asumaran - we do mention in the release notes, but indeed we are not mentioning it in the payment method page itself. I shared the feedback with the docs team. Thanks for flagging it

});

describe('submit()', () => {
test('should make the payments call passing deliveryAddress and billingAddress', async () => {
const onSubmitMock = jest.fn().mockImplementation((data, component, actions) => {
Expand All @@ -70,7 +85,7 @@ describe('GooglePay', () => {
});

// @ts-ignore GooglePayService is mocked
const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized;
const onPaymentAuthorized = GooglePayService.mock.calls[0][1].onPaymentAuthorized;
const promise = onPaymentAuthorized(googlePaymentData);

await new Promise(process.nextTick);
Expand Down Expand Up @@ -133,7 +148,7 @@ describe('GooglePay', () => {
});

// @ts-ignore GooglePayService is mocked
const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized;
const onPaymentAuthorized = GooglePayService.mock.calls[0][1].onPaymentAuthorized;
const googlePaymentDataWithoutAddresses = { ...googlePaymentData };
delete googlePaymentDataWithoutAddresses.shippingAddress;
delete googlePaymentDataWithoutAddresses.paymentMethodData.info.billingAddress;
Expand Down Expand Up @@ -173,7 +188,7 @@ describe('GooglePay', () => {
});

// @ts-ignore GooglePayService is mocked
const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized;
const onPaymentAuthorized = GooglePayService.mock.calls[0][1].onPaymentAuthorized;
const promise = onPaymentAuthorized(googlePaymentData);

await new Promise(process.nextTick);
Expand Down Expand Up @@ -215,7 +230,7 @@ describe('GooglePay', () => {
});

// @ts-ignore GooglePayService is mocked
const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized;
const onPaymentAuthorized = GooglePayService.mock.calls[0][1].onPaymentAuthorized;
const promise = onPaymentAuthorized(googlePaymentData);

await new Promise(process.nextTick);
Expand Down Expand Up @@ -294,7 +309,7 @@ describe('GooglePay', () => {
new GooglePay(global.core, { onAuthorized: onAuthorizedMock });

// @ts-ignore GooglePayService is mocked
const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized;
const onPaymentAuthorized = GooglePayService.mock.calls[0][1].onPaymentAuthorized;
onPaymentAuthorized(googlePaymentData);

expect(onAuthorizedMock.mock.calls[0][0]).toStrictEqual(event);
Expand All @@ -313,7 +328,7 @@ describe('GooglePay', () => {
});

// @ts-ignore GooglePayService is mocked
const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized;
const onPaymentAuthorized = GooglePayService.mock.calls[0][1].onPaymentAuthorized;
const promise = onPaymentAuthorized(googlePaymentData);

expect(promise).resolves.toEqual({
Expand Down Expand Up @@ -344,7 +359,7 @@ describe('GooglePay', () => {
const paymentCall = jest.spyOn(gpay as any, 'makePaymentsCall');

// @ts-ignore GooglePayService is mocked
const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized;
const onPaymentAuthorized = GooglePayService.mock.calls[0][1].onPaymentAuthorized;
onPaymentAuthorized(googlePaymentData);

await new Promise(process.nextTick);
Expand All @@ -357,7 +372,7 @@ describe('GooglePay', () => {
const paymentCall = jest.spyOn(gpay as any, 'makePaymentsCall');

// @ts-ignore GooglePayService is mocked
const onPaymentAuthorized = GooglePayService.mock.calls[0][0].paymentDataCallbacks.onPaymentAuthorized;
const onPaymentAuthorized = GooglePayService.mock.calls[0][1].onPaymentAuthorized;
onPaymentAuthorized(googlePaymentData);

await new Promise(process.nextTick);
Expand Down
Loading
Loading