Skip to content

Commit

Permalink
v6 - Adding isExpress flag for GooglePay and ApplePay (#2597)
Browse files Browse the repository at this point in the history
* feat: adding flag isExpress

* using isexpress flag on googlepay

* adding check for applepay

* fixed playground

* fixed tests
  • Loading branch information
ribeiroguilherme authored Mar 18, 2024
1 parent e49e137 commit 34d5c4d
Show file tree
Hide file tree
Showing 14 changed files with 163 additions and 157 deletions.
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) {
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();
});
});

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

0 comments on commit 34d5c4d

Please sign in to comment.