diff --git a/README.md b/README.md index c43a3413cf..ff782c5da9 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![npm](https://img.shields.io/npm/v/@adyen/adyen-web.svg) +[![npm](https://img.shields.io/npm/v/@adyen/adyen-web.svg)](https://www.npmjs.com/package/@adyen/adyen-web) ![Web](https://user-images.githubusercontent.com/7724351/198588741-f522c3ed-ff3c-4f70-b8cb-8ff9e6d41cfa.png) diff --git a/packages/e2e-playwright/package.json b/packages/e2e-playwright/package.json index 3e3d5ba758..0fa2607adf 100644 --- a/packages/e2e-playwright/package.json +++ b/packages/e2e-playwright/package.json @@ -26,6 +26,6 @@ "webpack-dev-server": "4.15.1" }, "dependencies": { - "@adyen/adyen-web": "5.56.1" + "@adyen/adyen-web": "5.57.0" } } diff --git a/packages/e2e/package.json b/packages/e2e/package.json index fd8e400616..fabe040e11 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -41,6 +41,6 @@ "whatwg-fetch": "^3.6.2" }, "dependencies": { - "@adyen/adyen-web": "5.56.1" + "@adyen/adyen-web": "5.57.0" } } diff --git a/packages/lib/CHANGELOG.md b/packages/lib/CHANGELOG.md index 89ddd73b08..7d67fee6a1 100644 --- a/packages/lib/CHANGELOG.md +++ b/packages/lib/CHANGELOG.md @@ -1,5 +1,11 @@ # @adyen/adyen-web +## 5.57.0 + +### Minor Changes + +- Use the label provided by the backend in stored blik payments ([#2522](https://github.com/Adyen/adyen-web/pull/2522)) + ## 5.56.1 ### Patch Changes diff --git a/packages/lib/package.json b/packages/lib/package.json index 8e820a9331..20d294ea30 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -25,7 +25,7 @@ "./dist/es/adyen.css": "./dist/es/adyen.css", "./package.json": "./package.json" }, - "version": "5.56.1", + "version": "5.57.0", "license": "MIT", "homepage": "https://docs.adyen.com/checkout", "repository": "github:Adyen/adyen-web", @@ -117,7 +117,7 @@ "stylelint-config-standard-scss": "7.0.1", "tslib": "2.6.2", "typescript": "4.9.5", - "vite": "4.5.1", + "vite": "4.5.2", "vite-plugin-stylelint": "^4.3.0", "whatwg-fetch": "^3.6.2" }, diff --git a/packages/lib/src/components/Blik/Blik.test.tsx b/packages/lib/src/components/Blik/Blik.test.tsx new file mode 100644 index 0000000000..5854234d3b --- /dev/null +++ b/packages/lib/src/components/Blik/Blik.test.tsx @@ -0,0 +1,47 @@ +import { render, screen } from '@testing-library/preact'; +import AdyenCheckout from '../../index'; + +describe('Blik', () => { + const createDropin = async paymentMethodsResponse => { + const checkout = await AdyenCheckout({ + environment: 'test', + clientKey: 'test_123456', + analytics: { enabled: false }, + paymentMethodsResponse: paymentMethodsResponse + }); + return checkout.create('dropin'); + }; + + describe('in Dropin display correct payment method name', () => { + test('display only blik if it is not stored', async () => { + const blik = await createDropin({ paymentMethods: [{ type: 'blik', name: 'Blik' }] }); + render(blik.render()); + + const blikText = await screen.findByText('Blik'); + + expect(blikText).toBeInTheDocument(); + }); + + test('display blik payment method name and label', async () => { + const blik = await createDropin({ + storedPaymentMethods: [ + { + id: 'X8CN3VMB6XXZTX43', + label: 'mBank PMM', + name: 'Blik', + supportedRecurringProcessingModels: ['CardOnFile'], + supportedShopperInteractions: ['Ecommerce'], + type: 'blik' + } + ] + }); + render(blik.render()); + + const blikText = await screen.findByText('Blik'); + const storedPaymentMethodLabel = await screen.findByText('mBank PMM'); + + expect(blikText).toBeInTheDocument(); + expect(storedPaymentMethodLabel).toBeInTheDocument(); + }); + }); +}); diff --git a/packages/lib/src/components/Blik/Blik.tsx b/packages/lib/src/components/Blik/Blik.tsx index 6fdb003680..12ac7aac51 100644 --- a/packages/lib/src/components/Blik/Blik.tsx +++ b/packages/lib/src/components/Blik/Blik.tsx @@ -37,6 +37,20 @@ class BlikElement extends UIElement { return !!this.state.isValid; } + get displayName() { + if (this.props.storedPaymentMethodId && this.props.label) { + return this.props.label; + } + return this.props.name; + } + + get additionalInfo() { + if (this.props.storedPaymentMethodId && this.props.label) { + return this.props.name; + } + return null; + } + /** * NOTE: for future reference: * this.props.onComplete (which is called from this.onComplete) equates to the merchant defined onAdditionalDetails callback diff --git a/packages/lib/src/components/Dropin/components/PaymentMethod/PaymentMethodName.tsx b/packages/lib/src/components/Dropin/components/PaymentMethod/PaymentMethodName.tsx index fbce97a783..3689caeda0 100644 --- a/packages/lib/src/components/Dropin/components/PaymentMethod/PaymentMethodName.tsx +++ b/packages/lib/src/components/Dropin/components/PaymentMethod/PaymentMethodName.tsx @@ -13,14 +13,16 @@ const PaymentMethodName = ({ displayName, additionalInfo, isSelected }) => ( {displayName} - - {additionalInfo} - + {additionalInfo && ( + + {additionalInfo} + + )} ); diff --git a/packages/lib/src/components/Giftcard/Giftcard.test.tsx b/packages/lib/src/components/Giftcard/Giftcard.test.tsx index 2f99146c8a..7387b3f9ee 100644 --- a/packages/lib/src/components/Giftcard/Giftcard.test.tsx +++ b/packages/lib/src/components/Giftcard/Giftcard.test.tsx @@ -1,8 +1,24 @@ import Giftcard from './Giftcard'; +import { render, screen } from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; + const flushPromises = () => new Promise(process.nextTick); describe('Giftcard', () => { - const baseProps = { amount: { value: 1000, currency: 'EUR' }, name: 'My Test Gift Card', type: 'giftcard', brand: 'genericgiftcard' }; + const resources = global.resources; + const i18n = global.i18n; + const user = userEvent.setup(); + + const baseProps = { + modules: { resources }, + amount: { value: 1000, currency: 'EUR' }, + name: 'My Test Gift Card', + type: 'giftcard', + brand: 'genericgiftcard', + showPayButton: true, + i18n, + loadingContext: 'mock' + }; describe('onBalanceCheck', () => { test('If onBalanceCheck is not provided, step is skipped and calls onSubmit', async () => { @@ -24,4 +40,206 @@ describe('Giftcard', () => { expect(onBalanceCheck).not.toHaveBeenCalled(); }); }); + + describe('icon getters', () => { + test('should default to loading from resources', async () => { + const giftcard = new Giftcard({ ...baseProps }); + + expect(giftcard.icon).toBe('MOCK'); + }); + + test('should use the prop .icon as 2. priority', async () => { + const giftcard = new Giftcard({ ...baseProps, icon: 'PROP_ICON_MOCK' }); + + expect(giftcard.icon).toBe('PROP_ICON_MOCK'); + }); + + test('should use brandsConfiguration as 1. priority', async () => { + const giftcard = new Giftcard({ + ...baseProps, + icon: 'PROP_ICON_MOCK', + brandsConfiguration: { + genericgiftcard: { icon: 'genericgiftcard_MOCK' }, + givex: { icon: 'givex_MOCK' } + } + }); + + expect(giftcard.icon).toBe('genericgiftcard_MOCK'); + expect(giftcard.icon).not.toBe('PROP_ICON_MOCK'); + }); + }); + + describe('displayName getters', () => { + test('should default to props.name', async () => { + const giftcard = new Giftcard({ ...baseProps }); + + expect(giftcard.displayName).toBe('My Test Gift Card'); + }); + + test('should use brandsConfiguration as 1. priority', async () => { + const giftcard = new Giftcard({ + ...baseProps, + brandsConfiguration: { + genericgiftcard: { name: 'genericgiftcard brand name' }, + givex: { name: 'givex brand name' } + } + }); + + expect(giftcard.displayName).toBe('genericgiftcard brand name'); + expect(giftcard.displayName).not.toBe('givex brand name'); + }); + }); + + describe('onBalanceCheck handling', () => { + test('onBalanceCheck should be called on pay button click', async () => { + const onBalanceCheck = jest.fn(); + + // mounting and clicking pay button + const giftcard = new Giftcard({ + ...baseProps, + onBalanceCheck + }); + render(giftcard.render()); + // skip feeling in fields + giftcard.setState({ isValid: true }); + const payButton = await screen.findByRole('button'); + await user.click(payButton); + + expect(onBalanceCheck).toHaveBeenCalled(); + }); + + test('after balance check we should call onOrderRequest if not enough balance for checkout', async () => { + const onBalanceCheck = jest.fn(resolve => + resolve({ + balance: { value: 500, currency: 'EUR' } + }) + ); + const onOrderRequest = jest.fn(); + + // mounting and clicking pay button + const giftcard = new Giftcard({ + ...baseProps, + onBalanceCheck, + onOrderRequest + }); + render(giftcard.render()); + giftcard.setState({ isValid: true }); + const payButton = await screen.findByRole('button'); + await user.click(payButton); + + // since the balance is not enough for a full we should create an order + expect(onOrderRequest).toHaveBeenCalled(); + }); + + test('if there is enough balance for checkout we should require confirmation', async () => { + const onBalanceCheck = jest.fn(resolve => + resolve({ + balance: { value: 2000, currency: 'EUR' } + }) + ); + const onRequiringConfirmation = jest.fn(); + + // mounting and clicking pay button + const giftcard = new Giftcard({ + ...baseProps, + onBalanceCheck, + onRequiringConfirmation + }); + render(giftcard.render()); + giftcard.setState({ isValid: true }); + const payButton = await screen.findByRole('button'); + await user.click(payButton); + + // since there is enough balance we should inform merchant + // to confirm using the giftcard funds + expect(onRequiringConfirmation).toHaveBeenCalled(); + }); + + test('if theres 0 balance we should trigger and error', async () => { + const onBalanceCheck = jest.fn(resolve => + resolve({ + balance: { value: 0, currency: 'EUR' } + }) + ); + + // mounting and clicking pay button + const onError = jest.fn(); + const giftcard = new Giftcard({ + ...baseProps, + onBalanceCheck, + onError + }); + render(giftcard.render()); + giftcard.setState({ isValid: true }); + const payButton = await screen.findByRole('button'); + await user.click(payButton); + + expect(onBalanceCheck).toHaveBeenCalled(); + expect(onError).toHaveBeenCalled(); + }); + }); + + describe('onOrderRequest handling', () => { + test('after creating an order we should call submit / payments endpoint', async () => { + const onBalanceCheck = jest.fn(resolve => + resolve({ + balance: { value: 500, currency: 'EUR' } + }) + ); + const onOrderRequest = jest.fn(resolve => + resolve({ + orderData: 'mock', + pspReference: 'mock' + }) + ); + const onSubmit = jest.fn(); + + // mounting and clicking pay button + const giftcard = new Giftcard({ + ...baseProps, + onBalanceCheck, + onOrderRequest, + onSubmit + }); + render(giftcard.render()); + giftcard.setState({ isValid: true }); + const payButton = await screen.findByRole('button'); + await user.click(payButton); + + expect(onSubmit).toHaveBeenCalled(); + }); + + test('should not create new order if one already exists', async () => { + const onBalanceCheck = jest.fn(resolve => + resolve({ + balance: { value: 500, currency: 'EUR' } + }) + ); + const onOrderRequest = jest.fn(resolve => + resolve({ + orderData: 'mock', + pspReference: 'mock' + }) + ); + const onSubmit = jest.fn(); + + // mounting and clicking pay button + const giftcard = new Giftcard({ + ...baseProps, + order: { + orderData: 'mock' + }, + onBalanceCheck, + onOrderRequest, + onSubmit + }); + render(giftcard.render()); + giftcard.setState({ isValid: true }); + const payButton = await screen.findByRole('button'); + await user.click(payButton); + + expect(onOrderRequest).not.toHaveBeenCalled(); + expect(onSubmit).toHaveBeenCalled(); + }); + }); }); diff --git a/packages/lib/src/components/MealVoucherFR/MealVoucherFR.test.tsx b/packages/lib/src/components/MealVoucherFR/MealVoucherFR.test.tsx new file mode 100644 index 0000000000..308f35eb98 --- /dev/null +++ b/packages/lib/src/components/MealVoucherFR/MealVoucherFR.test.tsx @@ -0,0 +1,94 @@ +import userEvent from '@testing-library/user-event'; +import MealVoucherFR from './MealVoucherFR'; +import { render, screen } from '@testing-library/preact'; + +describe('MealVoucherFR', () => { + const resources = global.resources; + const i18n = global.i18n; + const user = userEvent.setup(); + + const baseProps = { + modules: { resources }, + amount: { value: 1000, currency: 'EUR' }, + name: 'MealVoucher', + showPayButton: true, + i18n, + loadingContext: 'mock' + }; + + // test only the layout of MealVoucher since the logic comes from Giftcard + describe('layout of mealvoucher', () => { + test('should also display expiry date', async () => { + const onBalanceCheck = jest.fn(); + + // mounting and clicking pay button + const mealVoucherFR = new MealVoucherFR({ + ...baseProps, + onBalanceCheck + }); + render(mealVoucherFR.render()); + // skip feeling in fields + mealVoucherFR.setState({ isValid: true }); + const payButton = await screen.findByRole('button'); + await user.click(payButton); + + const card = await screen.findByText('Card number'); + const expiryDate = await screen.findByText('Expiry date'); + const cvc = await screen.findByText('Security code'); + + // expiry date is not available in the regular gift card + expect(card).toBeInTheDocument(); + expect(expiryDate).toBeInTheDocument(); + expect(cvc).toBeInTheDocument(); + }); + }); + + // this tests are useful so it's obvious changes on giftcard will also affect behaviour on MealVoucher + describe('basic gift card tests', () => { + test('onBalanceCheck should be called on pay button click', async () => { + const onBalanceCheck = jest.fn(); + + // mounting and clicking pay button + const mealVoucherFR = new MealVoucherFR({ + ...baseProps, + onBalanceCheck + }); + render(mealVoucherFR.render()); + // skip feeling in fields + mealVoucherFR.setState({ isValid: true }); + const payButton = await screen.findByRole('button'); + await user.click(payButton); + + expect(onBalanceCheck).toHaveBeenCalled(); + }); + + test('after creating an order we should call submit / payments endpoint', async () => { + const onBalanceCheck = jest.fn(resolve => + resolve({ + balance: { value: 500, currency: 'EUR' } + }) + ); + const onOrderRequest = jest.fn(resolve => + resolve({ + orderData: 'mock', + pspReference: 'mock' + }) + ); + const onSubmit = jest.fn(); + + // mounting and clicking pay button + const mealVoucherFR = new MealVoucherFR({ + ...baseProps, + onBalanceCheck, + onOrderRequest, + onSubmit + }); + render(mealVoucherFR.render()); + mealVoucherFR.setState({ isValid: true }); + const payButton = await screen.findByRole('button'); + await user.click(payButton); + + expect(onSubmit).toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/lib/src/components/UIElement.tsx b/packages/lib/src/components/UIElement.tsx index 7d4c5ee799..ebac1b8c5d 100644 --- a/packages/lib/src/components/UIElement.tsx +++ b/packages/lib/src/components/UIElement.tsx @@ -294,6 +294,13 @@ export class UIElement

extends BaseElement

im return this.props.name || this.constructor['type']; } + /** + * Used to display the second line of a payment method item + */ + get additionalInfo(): string { + return null; + } + /** * Get the element accessible name, used in the aria-label of the button that controls selected payment method */ diff --git a/packages/playground/package.json b/packages/playground/package.json index 4b30b4ca15..1052aea13d 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -41,6 +41,6 @@ "whatwg-fetch": "^3.6.2" }, "dependencies": { - "@adyen/adyen-web": "5.56.1" + "@adyen/adyen-web": "5.57.0" } } diff --git a/yarn.lock b/yarn.lock index a3c0e94a88..f901fc260c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15865,10 +15865,10 @@ vite-plugin-stylelint@^4.3.0: "@rollup/pluginutils" "^5.0.2" chokidar "^3.5.3" -vite@4.5.1: - version "4.5.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.1.tgz#3370986e1ed5dbabbf35a6c2e1fb1e18555b968a" - integrity sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA== +vite@4.5.2: + version "4.5.2" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.2.tgz#d6ea8610e099851dad8c7371599969e0f8b97e82" + integrity sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w== dependencies: esbuild "^0.18.10" postcss "^8.4.27"