Skip to content

Commit

Permalink
Fastlane Card Component signup (#3043)
Browse files Browse the repository at this point in the history
* sign up component ui

* a11y icons

* fixed button label type. renamed components. fixed modal describeby

* using tokens

* showing signup based on the brand. added to card state fastlaneData

* mobile input working with static prefix

* css variables. fixed small issue mobile input

* fixed issue with input field focus

* unit tests

* increasing size limit

* fixed size-limit. link css. storybook

* fixed css import

* e2e tests

* adjusted fastlane images

* more e2e tests

* adjusted stories

* adjusted css variables
  • Loading branch information
ribeiroguilherme authored Jan 9, 2025
1 parent 773db10 commit 1014bbc
Show file tree
Hide file tree
Showing 33 changed files with 1,168 additions and 43 deletions.
1 change: 1 addition & 0 deletions packages/e2e-playwright/fixtures/URL_MAP.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const URL_MAP = {
cardWithInstallments: '/iframe.html?args=&id=cards-card--with-installments&viewMode=story',
cardWithKcp: '/iframe.html?args=&globals=&id=cards-card--with-kcp&viewMode=story',
cardWithClickToPay: '/iframe.html?args=&id=cards-card--with-click-to-pay&viewMode=story',
cardWithFastlane: '/iframe.html?args=&globals=&id=cards-card--with-mocked-fastlane&viewMode=story',
fullAvsWithoutPrefilledDataUrl: '/iframe.html?args=componentConfiguration.data:!undefined&globals=&id=cards-card--with-avs&viewMode=story',
fullAvsWithPrefilledDataUrl: '/iframe.html?globals=&args=&id=cards-card--with-avs&viewMode=story',
addressLookupUrl: '/iframe.html?id=cards-card--with-avs-address-lookup&viewMode=story',
Expand Down
8 changes: 7 additions & 1 deletion packages/e2e-playwright/fixtures/card.fixture.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { test as base, expect } from '@playwright/test';
import { URL_MAP } from './URL_MAP';
import { Card } from '../models/card';
import { BCMC } from '../models/bcmc';
import { URL_MAP } from './URL_MAP';
import { CardWithAvs } from '../models/card-avs';
import { CardWithKCP } from '../models/card-kcp';
import { CardWithSSN } from '../models/card-ssn';
import { CardWithFastlane } from '../models/card-fastlane';

type Fixture = {
card: Card;
cardWithAvs: CardWithAvs;
cardWithKCP: CardWithKCP;
cardWithSSN: CardWithSSN;
cardWithFastlane: CardWithFastlane;
bcmc: BCMC;
};

Expand All @@ -19,6 +21,10 @@ const test = base.extend<Fixture>({
const cardPage = new Card(page);
await use(cardPage);
},
cardWithFastlane: async ({ page }, use) => {
const cardPage = new CardWithFastlane(page);
await use(cardPage);
},
cardWithAvs: async ({ page }, use) => {
const cardPage = new CardWithAvs(page);
await use(cardPage);
Expand Down
27 changes: 27 additions & 0 deletions packages/e2e-playwright/models/card-fastlane.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Page } from '@playwright/test';
import { Card } from './card';
import { USER_TYPE_DELAY } from '../tests/utils/constants';

class CardWithFastlane extends Card {
constructor(page: Page) {
super(page);
}

get fastlaneElement() {
return this.page.getByTestId('fastlane-signup-component');
}

get fastlaneSignupToggle() {
return this.fastlaneElement.getByRole('switch');
}

get mobileNumberInput() {
return this.fastlaneElement.getByLabel('Mobile number');
}

async typeMobileNumber(number: string) {
return this.mobileNumberInput.pressSequentially(number, { delay: USER_TYPE_DELAY });
}
}

export { CardWithFastlane };
260 changes: 260 additions & 0 deletions packages/e2e-playwright/tests/e2e/card/fastlane.signup.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { test, expect } from '../../../fixtures/card.fixture';
import { MAESTRO_CARD, PAYMENT_RESULT, REGULAR_TEST_CARD, TEST_CVC_VALUE, TEST_DATE_VALUE, VISA_CARD } from '../../utils/constants';
import { URL_MAP } from '../../../fixtures/URL_MAP';
import { paymentSuccessfulMock } from '../../../mocks/payments/payments.mock';
import { getStoryUrl } from '../../utils/getStoryUrl';

test.describe('Card - Fastlane Sign up', () => {
test.describe('when Fastlane SDK returns "showConsent: true"', () => {
test('#1 should shown consent UI only when Mastercard or Visa number is entered', async ({ cardWithFastlane, page }) => {
await paymentSuccessfulMock(page);
const paymentsRequestPromise = page.waitForRequest(request => request.url().includes('/payments') && request.method() === 'POST');

await cardWithFastlane.goto(URL_MAP.cardWithFastlane);
await expect(cardWithFastlane.fastlaneElement).not.toBeVisible();

// Start typing VISA card
await cardWithFastlane.typeCardNumber('4111');
await expect(cardWithFastlane.fastlaneElement).toBeVisible();

await cardWithFastlane.deleteCardNumber();
await expect(cardWithFastlane.fastlaneElement).not.toBeVisible();

// Start typing MC card
await cardWithFastlane.typeCardNumber('5454');
await expect(cardWithFastlane.fastlaneElement).toBeVisible();

await cardWithFastlane.deleteCardNumber();
await expect(cardWithFastlane.fastlaneElement).not.toBeVisible();

// Enter brand not supported by fastlame (MAESTRO)
await cardWithFastlane.typeCardNumber(MAESTRO_CARD);
await cardWithFastlane.typeCvc(TEST_CVC_VALUE);
await cardWithFastlane.typeExpiryDate(TEST_DATE_VALUE);
await expect(cardWithFastlane.fastlaneElement).not.toBeVisible();

await cardWithFastlane.pay();

const request = await paymentsRequestPromise;
const paymentMethod = await request.postDataJSON().paymentMethod;
expect(paymentMethod.fastlaneData).toBeDefined();

expect(JSON.parse(atob(paymentMethod.fastlaneData))).toEqual({
consentShown: true,
consentVersion: 'v1',
consentGiven: false,
fastlaneSessionId: 'ABC-123'
});

await expect(cardWithFastlane.paymentResult).toContainText(PAYMENT_RESULT.authorised);
});

test('#2 should send consentGiven:true even if the mobile number input is empty', async ({ cardWithFastlane, page }) => {
await paymentSuccessfulMock(page);
const paymentsRequestPromise = page.waitForRequest(request => request.url().includes('/payments') && request.method() === 'POST');

await cardWithFastlane.goto(URL_MAP.cardWithFastlane);
await cardWithFastlane.typeCardNumber(REGULAR_TEST_CARD);
await cardWithFastlane.typeCvc(TEST_CVC_VALUE);
await cardWithFastlane.typeExpiryDate(TEST_DATE_VALUE);
await expect(cardWithFastlane.fastlaneElement).toBeVisible();

await cardWithFastlane.pay();

const request = await paymentsRequestPromise;
const paymentMethod = await request.postDataJSON().paymentMethod;
expect(paymentMethod.fastlaneData).toBeDefined();

expect(JSON.parse(atob(paymentMethod.fastlaneData))).toEqual({
consentShown: true,
consentGiven: true,
consentVersion: 'v1',
fastlaneSessionId: 'ABC-123'
});

await expect(cardWithFastlane.paymentResult).toContainText(PAYMENT_RESULT.authorised);
});

test('#3 should send fastlane data even if the consent UI is not displayed at all', async ({ cardWithFastlane, page }) => {
await paymentSuccessfulMock(page);
const paymentsRequestPromise = page.waitForRequest(request => request.url().includes('/payments') && request.method() === 'POST');

await cardWithFastlane.goto(URL_MAP.cardWithFastlane);
await cardWithFastlane.typeCardNumber(MAESTRO_CARD);
await cardWithFastlane.typeCvc(TEST_CVC_VALUE);
await cardWithFastlane.typeExpiryDate(TEST_DATE_VALUE);
await expect(cardWithFastlane.fastlaneElement).not.toBeVisible();

await cardWithFastlane.pay();

const request = await paymentsRequestPromise;
const paymentMethod = await request.postDataJSON().paymentMethod;
expect(paymentMethod.fastlaneData).toBeDefined();

expect(JSON.parse(atob(paymentMethod.fastlaneData))).toEqual({
consentShown: false,
consentGiven: false,
consentVersion: 'v1',
fastlaneSessionId: 'ABC-123'
});

await expect(cardWithFastlane.paymentResult).toContainText(PAYMENT_RESULT.authorised);
});

test('#4 should sign up passing the mobile number', async ({ cardWithFastlane, page }) => {
await paymentSuccessfulMock(page);
const paymentsRequestPromise = page.waitForRequest(request => request.url().includes('/payments') && request.method() === 'POST');

await cardWithFastlane.goto(URL_MAP.cardWithFastlane);
await cardWithFastlane.typeCardNumber(REGULAR_TEST_CARD);
await cardWithFastlane.typeCvc(TEST_CVC_VALUE);
await cardWithFastlane.typeExpiryDate(TEST_DATE_VALUE);

const telephoneNumber = '8001005000';
await cardWithFastlane.typeMobileNumber(telephoneNumber);

await cardWithFastlane.pay();

const request = await paymentsRequestPromise;
const paymentMethod = await request.postDataJSON().paymentMethod;
expect(paymentMethod.fastlaneData).toBeDefined();

expect(JSON.parse(atob(paymentMethod.fastlaneData))).toEqual({
consentShown: true,
consentGiven: true,
consentVersion: 'v1',
fastlaneSessionId: 'ABC-123',
telephoneNumber
});

await expect(cardWithFastlane.paymentResult).toContainText(PAYMENT_RESULT.authorised);
});
});

test.describe('when Fastlane SDK returns "showConsent: false"', () => {
test('#1 should send fastlaneData even though the sign up UI is not displayed', async ({ cardWithFastlane, page }) => {
await paymentSuccessfulMock(page);
const paymentsRequestPromise = page.waitForRequest(request => request.url().includes('/payments') && request.method() === 'POST');

await cardWithFastlane.goto(
getStoryUrl({
baseUrl: URL_MAP.cardWithFastlane,
componentConfig: {
fastlaneConfiguration: {
showConsent: false,
defaultToggleState: false,
termsAndConditionsLink: 'https://adyen.com',
privacyPolicyLink: 'https://adyen.com',
termsAndConditionsVersion: 'v1',
fastlaneSessionId: 'ABC-123'
}
}
})
);

await cardWithFastlane.typeCardNumber(REGULAR_TEST_CARD);
await cardWithFastlane.typeCvc(TEST_CVC_VALUE);
await cardWithFastlane.typeExpiryDate(TEST_DATE_VALUE);

await expect(cardWithFastlane.fastlaneElement).not.toBeVisible();

await cardWithFastlane.pay();

const request = await paymentsRequestPromise;
const paymentMethod = await request.postDataJSON().paymentMethod;
expect(paymentMethod.fastlaneData).toBeDefined();

expect(JSON.parse(atob(paymentMethod.fastlaneData))).toEqual({
consentShown: false,
consentGiven: false,
consentVersion: 'v1',
fastlaneSessionId: 'ABC-123'
});

await expect(cardWithFastlane.paymentResult).toContainText(PAYMENT_RESULT.authorised);
});
});

test.describe('when Fastlane configuration is not passed to the Card component', () => {
test('#1 should not add fastlaneData to the payments request', async ({ cardWithFastlane, page }) => {
await paymentSuccessfulMock(page);
const paymentsRequestPromise = page.waitForRequest(request => request.url().includes('/payments') && request.method() === 'POST');

await cardWithFastlane.goto(
getStoryUrl({
baseUrl: URL_MAP.cardWithFastlane,
componentConfig: {}
})
);

await cardWithFastlane.typeCardNumber(REGULAR_TEST_CARD);
await cardWithFastlane.typeCvc(TEST_CVC_VALUE);
await cardWithFastlane.typeExpiryDate(TEST_DATE_VALUE);

await expect(cardWithFastlane.fastlaneElement).not.toBeVisible();

await cardWithFastlane.pay();

const request = await paymentsRequestPromise;
const paymentMethod = await request.postDataJSON().paymentMethod;
expect(paymentMethod.fastlaneData).toBeUndefined();

await expect(cardWithFastlane.paymentResult).toContainText(PAYMENT_RESULT.authorised);
});

test('#2 should not show Fastlane signup interface for the supported brands', async ({ cardWithFastlane, page }) => {
await cardWithFastlane.goto(
getStoryUrl({
baseUrl: URL_MAP.cardWithFastlane,
componentConfig: {}
})
);

await cardWithFastlane.typeCardNumber(REGULAR_TEST_CARD);
await cardWithFastlane.typeCvc(TEST_CVC_VALUE);
await cardWithFastlane.typeExpiryDate(TEST_DATE_VALUE);
await expect(cardWithFastlane.fastlaneElement).not.toBeVisible();

await cardWithFastlane.deleteCardNumber();

await cardWithFastlane.typeCardNumber(VISA_CARD);
await expect(cardWithFastlane.fastlaneElement).not.toBeVisible();
});
});

test.describe('when Fastlane configuration object is not valid', () => {
test('#1 should not add fastlaneData to the payments request', async ({ cardWithFastlane, page }) => {
await paymentSuccessfulMock(page);
const paymentsRequestPromise = page.waitForRequest(request => request.url().includes('/payments') && request.method() === 'POST');

// Omitted 'showConsent' and 'defaultToggleState'
await cardWithFastlane.goto(
getStoryUrl({
baseUrl: URL_MAP.cardWithFastlane,
componentConfig: {
fastlaneConfiguration: {
termsAndConditionsLink: 'https://adyen.com',
privacyPolicyLink: 'https://adyen.com',
termsAndConditionsVersion: 'v1',
fastlaneSessionId: 'ABC-123'
}
}
})
);

await cardWithFastlane.typeCardNumber(REGULAR_TEST_CARD);
await cardWithFastlane.typeCvc(TEST_CVC_VALUE);
await cardWithFastlane.typeExpiryDate(TEST_DATE_VALUE);

await expect(cardWithFastlane.fastlaneElement).not.toBeVisible();

await cardWithFastlane.pay();

const request = await paymentsRequestPromise;
const paymentMethod = await request.postDataJSON().paymentMethod;
expect(paymentMethod.fastlaneData).toBeUndefined();

await expect(cardWithFastlane.paymentResult).toContainText(PAYMENT_RESULT.authorised);
});
});
});
24 changes: 12 additions & 12 deletions packages/lib/.size-limit.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,40 +6,40 @@ module.exports = [
name: 'UMD',
path: 'dist/umd/adyen.js',
limit: '110 KB',
running: false,
running: false
},
/**
* 'auto' bundle with all Components included, excluding Languages
*/
{
name: 'Auto',
path: 'auto/auto.js',
import: "{ AdyenCheckout, Dropin }",
limit: '110 KB',
running: false,
import: '{ AdyenCheckout, Dropin }',
limit: '115 KB',
running: false
},
/**
* ES modules (tree-shake)
*/
{
name: 'ESM - Core',
path: 'dist/es/index.js',
import: "{ AdyenCheckout }",
import: '{ AdyenCheckout }',
limit: '30 KB',
running: false,
running: false
},
{
name: 'ESM - Core + Card',
path: 'dist/es/index.js',
import: "{ AdyenCheckout, Card }",
import: '{ AdyenCheckout, Card }',
limit: '65 KB',
running: false,
running: false
},
{
name: 'ESM - Core + Dropin with Card',
path: 'dist/es/index.js',
import: "{ AdyenCheckout, Dropin, Card }",
import: '{ AdyenCheckout, Dropin, Card }',
limit: '70 KB',
running: false,
},
]
running: false
}
];
3 changes: 2 additions & 1 deletion packages/lib/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,8 @@ export class CardElement extends UIElement<CardConfiguration> {
holderName: this.props.holderName ?? ''
}),
...(cardBrand && { brand: cardBrand }),
...(this.props.fundingSource && { fundingSource: this.props.fundingSource })
...(this.props.fundingSource && { fundingSource: this.props.fundingSource }),
...(this.state.fastlaneData && { fastlaneData: btoa(JSON.stringify(this.state.fastlaneData)) })
},
...(this.state.billingAddress && { billingAddress: this.state.billingAddress }),
...(this.state.socialSecurityNumber && { socialSecurityNumber: this.state.socialSecurityNumber }),
Expand Down
Loading

0 comments on commit 1014bbc

Please sign in to comment.