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

Click to Pay - 3rd party cookies #2409

Merged
merged 16 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from 8 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
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { loginValidationRules } from './validate';
import useCoreContext from '../../../../../core/Context/useCoreContext';
import useForm from '../../../../../utils/useForm';
import Field from '../../../FormFields/Field';
import InputText from '../../../FormFields/InputText';
import InputEmail from '../../../FormFields/InputEmail';

type OnChangeProps = { data: CtPLoginInputDataState; valid; errors; isValid: boolean };

Expand Down Expand Up @@ -73,7 +73,7 @@ const CtPLoginInput = (props: CtPLoginInputProps): h.JSX.Element => {
errorMessage={isLoginInputDirty ? props.errorMessage || !!errors.shopperLogin : null}
classNameModifiers={['shopperLogin']}
>
<InputText
<InputEmail
name={'shopperLogin'}
autocorrect={'off'}
spellcheck={false}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { h } from 'preact';
import { render, screen } from '@testing-library/preact';
import CoreProvider from '../../../../../core/Context/CoreProvider';
import ClickToPayProvider from '../../context/ClickToPayProvider';
import { IClickToPayService } from '../../services/types';
import { mock } from 'jest-mock-extended';
import { Resources } from '../../../../../core/Context/Resources';
import Language from '../../../../../language';
import userEvent from '@testing-library/user-event';
import CtPOneTimePassword from './CtPOneTimePassword';

const customRender = (ui, { clickToPayService = mock<IClickToPayService>(), configuration = {} } = {}) => {
return render(
<CoreProvider i18n={new Language()} loadingContext="test" resources={new Resources()}>
<ClickToPayProvider
clickToPayService={clickToPayService}
isStandaloneComponent={true}
amount={{ value: 5000, currency: 'USD' }}
onSetStatus={jest.fn()}
configuration={configuration}
onError={jest.fn()}
onSubmit={jest.fn()}
setClickToPayRef={jest.fn()}
>
{ui}
</ClickToPayProvider>
</CoreProvider>
);
};

describe('Click to Pay - CtPOneTimePassword', () => {
test('should set to store the cookie if shopper ticks the checkbox', async () => {
const user = userEvent.setup();
const ctpServiceMock = mock<IClickToPayService>();
ctpServiceMock.schemes = ['visa', 'mc'];
// const onResendCodeMock = jest.fn();

customRender(<CtPOneTimePassword />, { clickToPayService: ctpServiceMock });

// Default false
const checkbox = (await screen.findByLabelText('Skip verification next time')) as HTMLInputElement;
expect(checkbox.checked).toBe(false);

// Checked
await user.click(checkbox);
expect(checkbox.checked).toBe(true);
expect(ctpServiceMock.updateStoreCookiesConsent).toHaveBeenCalledWith(true);
expect(ctpServiceMock.updateStoreCookiesConsent).toHaveBeenCalledTimes(1);

// Unchecked
await user.click(checkbox);
expect(checkbox.checked).toBe(false);
expect(ctpServiceMock.updateStoreCookiesConsent).toHaveBeenCalledWith(false);
expect(ctpServiceMock.updateStoreCookiesConsent).toHaveBeenCalledTimes(2);
});

test('should pass OTP to ClickToPay service', async () => {
const user = userEvent.setup({ delay: 100 });
const ctpServiceMock = mock<IClickToPayService>();
ctpServiceMock.schemes = ['visa', 'mc'];

customRender(<CtPOneTimePassword />, { clickToPayService: ctpServiceMock });

await screen.findByLabelText('One time code', { exact: false });

await user.keyboard('654321');
await user.keyboard('[Enter]');

expect(ctpServiceMock.finishIdentityValidation).toHaveBeenCalledWith('654321');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CtPInfo } from '../CtPInfo';
import CtPSection from '../CtPSection';
import useCoreContext from '../../../../../core/Context/useCoreContext';
import './CtPOneTimePassword.scss';
import CtPSaveCookiesCheckbox from './CtPSaveCookiesCheckbox';

type CtPOneTimePasswordProps = {
onDisplayCardComponent?(): void;
Expand Down Expand Up @@ -80,6 +81,9 @@ const CtPOneTimePassword = ({ onDisplayCardComponent }: CtPOneTimePasswordProps)
onResendCode={onResendCode}
isValidatingOtp={isValidatingOtp}
/>

<CtPSaveCookiesCheckbox />

<Button
disabled={isAccountLocked}
label={i18n.get('continue')}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,5 @@
}

.adyen-checkout-ctp__section > .adyen-checkout__field.adyen-checkout__field--otp {
margin-bottom: 20px;
margin-bottom: 12px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@import "../../../../../../style/index";

.adyen-checkout-ctp__otp-checkbox-container {
padding: 12px;
background-color: $color-gray-lighter;
border-radius: $border-radius-medium;
margin-bottom: 16px;
}

.adyen-checkout-ctp__otp-checkbox-container--checked {
background-color: #F2F8FF;
ribeiroguilherme marked this conversation as resolved.
Show resolved Hide resolved
}

.adyen-checkout-ctp__otp-checkbox-container label {
cursor: pointer;
}

.adyen-checkout-ctp__section .adyen-checkout__field.adyen-checkout-ctp__otp-checkbox-container {
margin-bottom: 4px;
}

.adyen-checkout-ctp__otp-checkbox-info {
color: $color-new-gray-darker;
font-size: 13px;
line-height: 19px;
font-weight: 400;
margin: 4px 0 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { h } from 'preact';
import classnames from 'classnames';
import Field from '../../../../FormFields/Field';
import Checkbox from '../../../../FormFields/Checkbox';
import useCoreContext from '../../../../../../core/Context/useCoreContext';
import { useState, useCallback } from 'preact/hooks';
import useClickToPayContext from '../../../context/useClickToPayContext';
import './CtPSaveCookiesCheckbox.scss';

function CtPSaveCookiesCheckbox() {
const { i18n } = useCoreContext();
const { updateStoreCookiesConsent, isStoringCookies } = useClickToPayContext();
const [checked, setIsChecked] = useState(isStoringCookies);

const handleOnChange = useCallback(() => {
const newChecked = !checked;
setIsChecked(newChecked);
updateStoreCookiesConsent(newChecked);
}, [updateStoreCookiesConsent, setIsChecked, checked]);

return (
<div
className={classnames('adyen-checkout-ctp__otp-checkbox-container', {
'adyen-checkout-ctp__otp-checkbox-container--checked': checked
})}
>
<Field name={'clickToPayCookiesCheckbox'} addContextualElement={false} useLabelElement={false} i18n={i18n}>
<Checkbox
name={'clickToPayCookiesCheckbox'}
onInput={handleOnChange}
label={i18n.get('ctp.otp.saveCookiesCheckbox.label')}
checked={checked}
aria-describedby={'adyen-ctp-cookies-info'}
/>
</Field>

<p className="adyen-checkout-ctp__otp-checkbox-info" id="adyen-ctp-cookies-info">
{i18n.get('ctp.otp.saveCookiesCheckbox.information')}
</p>
</div>
);
}

export default CtPSaveCookiesCheckbox;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from './CtPSaveCookiesCheckbox';
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
display: flex;
align-items: center;
height: 18px;
margin-bottom: 14px;
margin-bottom: 16px;
}

&--standalone {
Expand All @@ -31,8 +31,8 @@
font-size: 17px;
font-weight: 600;
line-height: 22px;
margin: 0 0 4px;
padding: 0;
margin: 0;
width: auto;

@media screen and (max-width: 400px) {
Expand All @@ -49,7 +49,7 @@
font-size: 13px;
font-weight: 400;
line-height: 19px;
color: $color-gray-darker;
color: $color-new-gray-darker;
margin: 0 0 16px;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ export interface IClickToPayContext
extends Pick<IClickToPayService, 'checkout' | 'startIdentityValidation' | 'finishIdentityValidation' | 'verifyIfShopperIsEnrolled'> {
isStandaloneComponent: boolean;
isCtpPrimaryPaymentMethod: boolean;
isStoringCookies: boolean;
setIsCtpPrimaryPaymentMethod(isPrimary: boolean): void;
logoutShopper(): Promise<void>;
updateStoreCookiesConsent(shouldStore: boolean): void;
ctpState: CtpState;
cards: ShopperCard[];
schemes: string[];
Expand All @@ -37,8 +39,10 @@ const ClickToPayContext = createContext<IClickToPayContext>({
configuration: null,
isStandaloneComponent: null,
isCtpPrimaryPaymentMethod: null,
isStoringCookies: null,
ribeiroguilherme marked this conversation as resolved.
Show resolved Hide resolved
setIsCtpPrimaryPaymentMethod: null,
logoutShopper: null,
updateStoreCookiesConsent: null,
ctpState: null,
cards: [],
schemes: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ const ClickToPayProvider = ({
await ctpService?.logout();
}, [ctpService]);

const updateStoreCookiesConsent = useCallback(
(shouldStore: boolean) => {
ctpService.updateStoreCookiesConsent(shouldStore);
},
[ctpService]
);

return (
<ClickToPayContext.Provider
value={{
Expand All @@ -99,6 +106,7 @@ const ClickToPayProvider = ({
onSetStatus,
amount,
configuration,
isStoringCookies: ctpService?.storeCookies,
isStandaloneComponent,
isCtpPrimaryPaymentMethod,
setIsCtpPrimaryPaymentMethod,
Expand All @@ -112,6 +120,7 @@ const ClickToPayProvider = ({
logoutShopper,
startIdentityValidation,
finishIdentityValidation,
updateStoreCookiesConsent,
onReady
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,94 @@ import { ISrcSdkLoader } from './sdks/SrcSdkLoader';
import VisaSdk from './sdks/VisaSdk';
import MastercardSdk from './sdks/MastercardSdk';
import { IdentityLookupParams, SchemesConfiguration } from './types';
import { SrciIdentityLookupResponse, SrcProfile } from './sdks/types';
import { SrciCheckoutResponse, SrciIdentityLookupResponse, SrcProfile } from './sdks/types';
import SrciError from './sdks/SrciError';
import ShopperCard from '../models/ShopperCard';

test('should be able to tweak the configuration to store the cookie', () => {
const visa = mock<VisaSdk>();
const schemesConfig = mock<SchemesConfiguration>();
const sdkLoader = mock<ISrcSdkLoader>();
sdkLoader.load.mockResolvedValue([visa]);

const service = new ClickToPayService(schemesConfig, sdkLoader, 'test');
expect(service.storeCookies).toBe(false);

service.updateStoreCookiesConsent(true);
expect(service.storeCookies).toBe(true);

service.updateStoreCookiesConsent(false);
expect(service.storeCookies).toBe(false);
});

test('should pass the complianceSettings if the cookie is set to be stored', async () => {
const checkoutResponseMock = mock<SrciCheckoutResponse>();
checkoutResponseMock.dcfActionCode = 'COMPLETE';

const profileFromVisaSrcSystem: SrcProfile = {
srcCorrelationId: '123456',
profiles: [
{
maskedCards: [
{
srcDigitalCardId: 'xxxx',
panLastFour: '8902',
dateOfCardLastUsed: '2019-09-28T08:10:02.312Z',
paymentCardDescriptor: 'visa',
panExpirationMonth: '12',
panExpirationYear: '2020',
digitalCardData: {
descriptorName: 'Visa',
artUri: 'https://image.com/visa'
},
tokenId: '9w8e8e'
}
]
}
]
};

const visa = mock<VisaSdk>();
// @ts-ignore Mocking readonly property
visa.schemeName = 'visa';
visa.checkout.mockResolvedValue(checkoutResponseMock);
visa.init.mockResolvedValue();
visa.isRecognized.mockResolvedValue({ recognized: true, idTokens: ['id-token'] });
visa.getSrcProfile.mockResolvedValue(profileFromVisaSrcSystem);

const sdkLoader = mock<ISrcSdkLoader>();
const schemesConfig = mock<SchemesConfiguration>();

const shopperCard = mock<ShopperCard>();
shopperCard.srcDigitalCardId = 'xxxx';
shopperCard.srcCorrelationId = 'zzzz';
shopperCard.scheme = 'visa';
Object.defineProperty(shopperCard, 'isDcfPopupEmbedded', {
get: jest.fn(() => false)
});

sdkLoader.load.mockResolvedValue([visa]);

const service = new ClickToPayService(schemesConfig, sdkLoader, 'test');
service.updateStoreCookiesConsent(true);

await service.initialize();
await service.checkout(shopperCard);

expect(visa.checkout).toHaveBeenCalledTimes(1);
expect(visa.checkout).toHaveBeenCalledWith({
complianceSettings: {
complianceResources: [
{
complianceType: 'REMEMBER_ME',
uri: ''
}
]
},
srcCorrelationId: 'zzzz',
srcDigitalCardId: 'xxxx'
});
});

test('should pass the correct configuration to the respective scheme SDKs', async () => {
const visa = mock<VisaSdk>();
Expand Down
Loading
Loading