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

Fastlane Card Component signup #3043

Merged
merged 18 commits into from
Jan 9, 2025
Merged
Show file tree
Hide file tree
Changes from 13 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
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
}
];
5 changes: 4 additions & 1 deletion packages/lib/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ export class CardElement extends UIElement<CardConfiguration> {
*/
const cardBrand = this.state.selectedBrandValue;

console.log('fastlane data', this.state.fastlaneData);

return {
paymentMethod: {
type: CardElement.type,
Expand All @@ -154,7 +156,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
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { CbObjOnBrand, CbObjOnFocus } from '../../../internal/SecuredFields/lib/
import { FieldErrorAnalyticsObject } from '../../../../core/Analytics/types';
import { PREFIX } from '../../../internal/Icon/constants';
import useSRPanelForCardInputErrors from './useSRPanelForCardInputErrors';
import FastlaneSignup from '../Fastlane/FastlaneSignup';

const CardInput = (props: CardInputProps) => {
const sfp = useRef(null);
Expand Down Expand Up @@ -98,7 +99,6 @@ const CardInput = (props: CardInputProps) => {
* if the PAN length drops below the /binLookup digit threshold.
* Default value, 'card', indicates no brand detected
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [internallyDetectedBrand, setInternallyDetectedBrand] = useState('card');

/**
Expand Down Expand Up @@ -480,6 +480,11 @@ const CardInput = (props: CardInputProps) => {
</div>
)}
/>

{props.fastlaneConfiguration && (
<FastlaneSignup {...props.fastlaneConfiguration} currentDetectedBrand={internallyDetectedBrand} onChange={props.onChange} />
)}

{props.showPayButton &&
props.payButton({
status,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { ComponentMethodsRef } from '../../../internal/UIElement/types';
import { AddressData, PaymentAmount } from '../../../../types/global-types';
import { AnalyticsModule } from '../../../../types/global-types';
import { FieldErrorAnalyticsObject } from '../../../../core/Analytics/types';
import type { FastlaneSignupConfiguration } from '../../../PayPalFastlane/types';

export interface CardInputValidState {
holderName?: boolean;
Expand Down Expand Up @@ -92,6 +93,7 @@ export interface CardInputProps {
enableStoreDetails?: boolean;
expiryMonth?: string;
expiryYear?: string;
fastlaneConfiguration?: FastlaneSignupConfiguration;
forceCompat?: boolean;
fundingSource?: 'debit' | 'credit';
hasCVC?: boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
@use 'styles/mixins';
@import 'styles/variable-generator';

.adyen-checkout-card__fastlane {
background: token(color-background-primary);
border: token(border-width-s) solid token(color-outline-primary);
border-radius: token(border-radius-m);
padding: token(spacer-060) token(spacer-070);
margin-top: token(spacer-070);
align-items: center;

[dir='rtl'] & {
padding: token(spacer-060) token(spacer-070);
}

&-consent-toggle {
display: flex;
gap: token(spacer-040);

&--active {
margin-bottom: token(spacer-070);
}
}

&-consent-text {
margin-bottom: token(spacer-070);

@include mixins.adyen-checkout-text-caption;
}

&-brand {
width: 168px;
height: 23px;
}
}

.adyen-checkout__button.adyen-checkout__button--fastlane-info-modal {
width: 20px;
height: 20px;
line-height: 0;
padding: 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { h } from 'preact';
import { render, screen } from '@testing-library/preact';
import { CoreProvider } from '../../../../core/Context/CoreProvider';
import FastlaneSignup from './FastlaneSignup';
import type { FastlaneSignupConfiguration } from '../../../PayPalFastlane/types';
import userEvent from '@testing-library/user-event';

const customRender = ui => {
return render(
<CoreProvider i18n={global.i18n} loadingContext="test" resources={global.resources}>
{ui}
</CoreProvider>
);
};

test('should trigger onChange even if the consent UI is not allowed to be shown', () => {
const fastlaneConfiguration: FastlaneSignupConfiguration = {
showConsent: false,
defaultToggleState: false,
termsAndConditionsLink: 'https://adyen.com',
termsAndConditionsVersion: 'v1',
privacyPolicyLink: 'https://adyen.com',
fastlaneSessionId: 'xxx-bbb'
};

const onChangeMock = jest.fn();

customRender(<FastlaneSignup {...fastlaneConfiguration} onChange={onChangeMock} currentDetectedBrand="card" />);

expect(onChangeMock).toHaveBeenCalledTimes(1);
expect(onChangeMock.mock.calls[0][0]).toEqual({
fastlaneData: {
consentGiven: false,
consentShown: false,
consentVersion: 'v1',
fastlaneSessionId: 'xxx-bbb'
}
});
});

test('should send "consentShown:true" flag if the shopper saw the consent UI at least once', async () => {
const user = userEvent.setup();

const fastlaneConfiguration: FastlaneSignupConfiguration = {
showConsent: true,
defaultToggleState: false,
termsAndConditionsLink: 'https://adyen.com',
termsAndConditionsVersion: 'v1',
privacyPolicyLink: 'https://adyen.com',
fastlaneSessionId: 'xxx-bbb'
};

const onChangeMock = jest.fn();

customRender(<FastlaneSignup {...fastlaneConfiguration} onChange={onChangeMock} currentDetectedBrand="mc" />);

// Show the UI
await user.click(screen.getByRole('switch'));
expect(screen.getByLabelText('Mobile number')).toBeVisible();

// Hide the UI
await user.click(screen.getByRole('switch'));
expect(screen.queryByText('Mobile number')).toBeNull();

expect(onChangeMock).lastCalledWith({
fastlaneData: {
consentGiven: false,
consentShown: true,
consentVersion: 'v1',
fastlaneSessionId: 'xxx-bbb'
}
});
});

test('should return phone number formatted (without spaces and without prefix)', async () => {
const user = userEvent.setup();

const fastlaneConfiguration: FastlaneSignupConfiguration = {
showConsent: true,
defaultToggleState: true,
termsAndConditionsLink: 'https://adyen.com',
termsAndConditionsVersion: 'v1',
privacyPolicyLink: 'https://adyen.com',
fastlaneSessionId: 'xxx-bbb'
};

const onChangeMock = jest.fn();

customRender(<FastlaneSignup {...fastlaneConfiguration} onChange={onChangeMock} currentDetectedBrand="visa" />);

const input = screen.getByLabelText('Mobile number');

await user.click(input);
await user.keyboard('8005550199');

expect(onChangeMock).lastCalledWith({
fastlaneData: {
consentGiven: true,
consentShown: true,
consentVersion: 'v1',
fastlaneSessionId: 'xxx-bbb',
telephoneNumber: '8005550199'
}
});
});

test('should display terms and privacy statement links', () => {
const fastlaneConfiguration: FastlaneSignupConfiguration = {
showConsent: true,
defaultToggleState: true,
termsAndConditionsLink: 'https://fastlane.com/terms',
termsAndConditionsVersion: 'v1',
privacyPolicyLink: 'https://fastlane.com/privacy-policy',
fastlaneSessionId: 'xxx-bbb'
};

const onChangeMock = jest.fn();

customRender(<FastlaneSignup {...fastlaneConfiguration} onChange={onChangeMock} currentDetectedBrand="visa" />);

expect(screen.getByRole('link', { name: 'terms' })).toHaveAttribute('href', 'https://fastlane.com/terms');
expect(screen.getByRole('link', { name: 'privacy statement' })).toHaveAttribute('href', 'https://fastlane.com/privacy-policy');
});

test('should open Fastlane info dialog and close it', async () => {
const user = userEvent.setup();

const fastlaneConfiguration: FastlaneSignupConfiguration = {
showConsent: true,
defaultToggleState: true,
termsAndConditionsLink: 'https://fastlane.com/terms',
termsAndConditionsVersion: 'v1',
privacyPolicyLink: 'https://fastlane.com/privacy-policy',
fastlaneSessionId: 'xxx-bbb'
};

const onChangeMock = jest.fn();

customRender(<FastlaneSignup {...fastlaneConfiguration} onChange={onChangeMock} currentDetectedBrand="visa" />);

screen.getByRole('dialog', { hidden: true });

const dialogButton = screen.getByRole('button', { name: /read more/i });
await user.click(dialogButton);
screen.getByRole('dialog', { hidden: false });

const closeDialogButton = screen.getByRole('button', { name: /close dialog/i });
await user.click(closeDialogButton);
screen.getByRole('dialog', { hidden: true });
});

test('should not render the UI if there are missing configuration fields', () => {
// @ts-ignore Testing misconfigured component
const fastlaneConfiguration: FastlaneSignupConfiguration = {
showConsent: true,
defaultToggleState: true,
termsAndConditionsLink: 'http://invalidlink.com',
privacyPolicyLink: 'https://fastlane.com/privacy-policy'
};

const onChangeMock = jest.fn();

const consoleMock = jest.fn();
jest.spyOn(console, 'warn').mockImplementation(consoleMock);

const { container } = customRender(<FastlaneSignup {...fastlaneConfiguration} onChange={onChangeMock} currentDetectedBrand="visa" />);

expect(consoleMock).toHaveBeenCalledTimes(1);
expect(consoleMock).toHaveBeenCalledWith('Fastlane: Component configuration is not valid. Fastlane will not be displayed');

expect(container).toBeEmptyDOMElement();
});
Loading
Loading