Skip to content

Commit

Permalink
Feature/using checkoutanalytics mvp iDeal (#2549)
Browse files Browse the repository at this point in the history
* Adding selected info event for issuerList buttons

* IssuerList handles analytics for both dropdown & issuerList buttons

* Detect and send analytics event when issuerList's dropdown is expanded

* Detect and send analytics event (debounced) when issuerList search functionality is used

* Moved debounce function to own util file

* Fixes for unit tests

* Made debounce for search a constant

* Adding new unit tests

* Removing child comp's submitAnalytics function (since all it does is call super). Removing unused param.
  • Loading branch information
sponglord authored Feb 14, 2024
1 parent c2a0306 commit 077e23b
Show file tree
Hide file tree
Showing 19 changed files with 189 additions and 38 deletions.
4 changes: 2 additions & 2 deletions packages/lib/src/components/ApplePay/ApplePay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { preparePaymentRequest } from './payment-request';
import { resolveSupportedVersion, mapBrands } from './utils';
import { ApplePayElementProps, ApplePayElementData, ApplePaySessionRequest, OnAuthorizedCallback } from './types';
import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError';
import { ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants';
import { ANALYTICS_INSTANT_PAYMENT_BUTTON, ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants';

const latestSupportedVersion = 14;

Expand Down Expand Up @@ -56,7 +56,7 @@ class ApplePayElement extends UIElement<ApplePayElementProps> {
submit() {
// Analytics
if (this.props.isInstantPayment) {
this.submitAnalytics({ type: ANALYTICS_SELECTED_STR, target: 'instant_payment_button' });
this.submitAnalytics({ type: ANALYTICS_SELECTED_STR, target: ANALYTICS_INSTANT_PAYMENT_BUTTON });
}

return this.startSession(this.props.onAuthorized);
Expand Down
4 changes: 2 additions & 2 deletions packages/lib/src/components/GooglePay/GooglePay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { GooglePayProps } from './types';
import { mapBrands, getGooglePayLocale } from './utils';
import collectBrowserInfo from '../../utils/browserInfo';
import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError';
import { ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants';
import { ANALYTICS_INSTANT_PAYMENT_BUTTON, ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants';

class GooglePay extends UIElement<GooglePayProps> {
public static type = 'paywithgoogle';
Expand Down Expand Up @@ -48,7 +48,7 @@ class GooglePay extends UIElement<GooglePayProps> {
public submit = () => {
// Analytics
if (this.props.isInstantPayment) {
this.submitAnalytics({ type: ANALYTICS_SELECTED_STR, target: 'instant_payment_button' });
this.submitAnalytics({ type: ANALYTICS_SELECTED_STR, target: ANALYTICS_INSTANT_PAYMENT_BUTTON });
}

const { onAuthorized = () => {} } = this.props;
Expand Down
1 change: 1 addition & 0 deletions packages/lib/src/components/UIElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class UIElement<P extends UIElementProps = any> extends BaseElement<P> im
this.handleOrder = this.handleOrder.bind(this);
this.handleResponse = this.handleResponse.bind(this);
this.setElementStatus = this.setElementStatus.bind(this);
this.submitAnalytics = this.submitAnalytics.bind(this);

this.elementRef = (props && props.elementRef) || this;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ class IssuerListContainer extends UIElement<IssuerListContainerProps> {
onChange={this.setState}
onSubmit={this.submit}
payButton={this.payButton}
onSubmitAnalytics={this.submitAnalytics}
/>
</SRPanelProvider>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { AddressLookupItem } from '../types';
import { useCallback, useEffect, useState, useMemo } from 'preact/hooks';
import './AddressSearch.scss';
import useCoreContext from '../../../../core/Context/useCoreContext';
import { debounce } from '../utils';
import { debounce } from '../../../../utils/debounce';
import Select from '../../FormFields/Select';
import { AddressData } from '../../../../types';

Expand Down
7 changes: 4 additions & 3 deletions packages/lib/src/components/internal/Address/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Specifications from './Specifications';
import { ValidatorRules } from '../../../utils/Validator/types';
import { ValidationRuleResult } from '../../../utils/Validator/ValidationRuleResult';
import { OnAddressLookupType, OnAddressSelectedType } from './components/AddressSearch';
import { SelectTargetObject } from '../FormFields/Select/types';

// Describes an object with unknown keys whose value is always a string
export type StringObject = {
Expand Down Expand Up @@ -56,7 +57,7 @@ export interface FieldContainerProps {
valid?: object;
onInput?: (e: Event) => void;
onBlur?: (e: Event) => void;
onDropdownChange: (e: { target: { value: string | number; name: string } }) => void;
onDropdownChange: (e: { target: SelectTargetObject }) => void;
readOnly?: boolean;
specifications: Specifications;
maxLength?: number;
Expand All @@ -76,7 +77,7 @@ export interface CountryFieldProps {
classNameModifiers: string[];
label: string;
errorMessage: boolean | string;
onDropdownChange: (e: { target: { value: string | number; name: string } }) => void;
onDropdownChange: (e: { target: SelectTargetObject }) => void;
readOnly?: boolean;
value: string;
}
Expand All @@ -90,7 +91,7 @@ export interface StateFieldProps {
classNameModifiers: string[];
label: string;
errorMessage: boolean | string;
onDropdownChange: (e: { target: { value: string | number; name: string } }) => void;
onDropdownChange: (e: { target: SelectTargetObject }) => void;
readOnly?: boolean;
selectedCountry: string;
specifications: Specifications;
Expand Down
9 changes: 0 additions & 9 deletions packages/lib/src/components/internal/Address/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ import { ADDRESS_SCHEMA } from './constants';
import { AddressField } from '../../../types';
import { StringObject } from './types';

export const DEFAULT_DEBOUNCE_TIME_MS = 300;

/**
* Used by the SRPanel sorting function to tell it whether we need to prepend the field type to the SR panel message, and, if so, we retrieve the correct translation for the field type.
* (Whether we need to prepend the field type depends on whether we know that the error message correctly reflects the label of the field. Ultimately all error messages should do this
Expand All @@ -16,10 +14,3 @@ export const mapFieldKey = (key: string, i18n: Language, countrySpecificLabels:
}
return null;
};
export const debounce = (fn: Function, ms = DEFAULT_DEBOUNCE_TIME_MS) => {
let timeoutId: ReturnType<typeof setTimeout>;
return function (this: any, ...args: any[]) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), ms);
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ function Select({
disabled,
disableTextFilter,
clearOnSelect,
blurOnClose
blurOnClose,
onListToggle
}: SelectProps) {
const filterInputRef = useRef(null);
const selectContainerRef = useRef(null);
Expand Down Expand Up @@ -232,6 +233,7 @@ function Select({
if (showList && filterable && filterInputRef.current) {
filterInputRef.current.focus();
}
onListToggle?.(showList);
}, [showList]);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export interface SelectItem {
selectedOptionName?: string;
}

export interface SelectTargetObject {
value?: string | number;
name?: string;
}

export interface SelectProps {
className: string;
classNameModifiers: string[];
Expand All @@ -20,7 +25,7 @@ export interface SelectProps {
onChange: (
e:
| {
target: { value: string | number; name: string };
target: SelectTargetObject;
}
| Partial<h.JSX.TargetedKeyboardEvent<HTMLInputElement>>
) => void;
Expand All @@ -33,6 +38,7 @@ export interface SelectProps {
disableTextFilter?: boolean;
clearOnSelect?: boolean;
blurOnClose?: boolean;
onListToggle?: (isOpen: boolean) => void;
}

export interface SelectButtonProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { mount } from 'enzyme';
import { h } from 'preact';
import IssuerList from './IssuerList';
import PayButton from '../PayButton';
import { ANALYTICS_FEATURED_ISSUER, ANALYTICS_LIST, ANALYTICS_SELECTED_STR } from '../../../core/Analytics/constants';

describe('IssuerList', () => {
test('Accepts Items as props', () => {
Expand All @@ -16,6 +17,7 @@ describe('IssuerList', () => {
showPayButton={false}
onChange={jest.fn()}
payButton={props => <PayButton {...props} amount={{ value: 50, currency: 'USD' }} />}
onSubmitAnalytics={() => {}}
/>
);
expect(wrapper.props().items).toHaveLength(3);
Expand All @@ -37,6 +39,7 @@ describe('IssuerList', () => {
showPayButton={false}
onChange={jest.fn()}
payButton={props => <PayButton {...props} amount={{ value: 50, currency: 'USD' }} />}
onSubmitAnalytics={() => {}}
/>
);
expect(wrapper.props().highlightedIds).toHaveLength(2);
Expand All @@ -61,6 +64,7 @@ describe('IssuerList', () => {
showPayButton={false}
onChange={onChangeCb}
payButton={props => <PayButton {...props} amount={{ value: 50, currency: 'USD' }} />}
onSubmitAnalytics={() => {}}
/>
);

Expand Down Expand Up @@ -93,6 +97,7 @@ describe('IssuerList', () => {
showPayButton={false}
onChange={jest.fn()}
payButton={props => <PayButton {...props} amount={{ value: 50, currency: 'USD' }} />}
onSubmitAnalytics={() => {}}
/>
);

Expand All @@ -117,6 +122,7 @@ describe('IssuerList', () => {
showPayButton={false}
onChange={jest.fn()}
payButton={props => <PayButton {...props} amount={{ value: 50, currency: 'USD' }} />}
onSubmitAnalytics={() => {}}
/>
);

Expand All @@ -127,3 +133,73 @@ describe('IssuerList', () => {
expect(highlightedIssuerButton.prop('value')).toBe(highlightedIssuerDropdownItem.prop('data-value'));
});
});

describe('IssuerList: calls that generate analytics should produce objects with the expected shapes ', () => {
let onSubmitAnalytics;
beforeEach(() => {
console.log = jest.fn(() => {});

onSubmitAnalytics = jest.fn(obj => {
console.log('### IssuerList.test::callbacks.onSubmitAnalytics:: obj', obj);
});
});

test('Clicking on a highlighted issuer button triggers call to onSubmitAnalytics with expected analytics object', () => {
const items = [
{ name: 'Issuer 1', id: '1' },
{ name: 'Issuer 2', id: '2' },
{ name: 'Issuer 3', id: '3' }
];
const highlightedIds = ['2', '3'];

expect(onSubmitAnalytics).toBeCalledTimes(0);

const wrapper = mount(
<IssuerList
items={items}
highlightedIds={highlightedIds}
showPayButton={false}
onChange={() => {}}
payButton={props => <PayButton {...props} amount={{ value: 50, currency: 'USD' }} />}
onSubmitAnalytics={onSubmitAnalytics}
/>
);

wrapper.find('.adyen-checkout__issuer-button-group button').at(1).simulate('click');

expect(onSubmitAnalytics).toHaveBeenCalledWith({
type: ANALYTICS_SELECTED_STR,
target: ANALYTICS_FEATURED_ISSUER,
issuer: 'Issuer 3'
});
});

test('Clicking on a issuer in the dropdown triggers call to onSubmitAnalytics with expected analytics object', () => {
const items = [
{ name: 'Issuer 1', id: '1' },
{ name: 'Issuer 2', id: '2' },
{ name: 'Issuer 3', id: '3' }
];

expect(onSubmitAnalytics).toBeCalledTimes(0);

const wrapper = mount(
<IssuerList
items={items}
showPayButton={false}
onChange={() => {}}
payButton={props => <PayButton {...props} amount={{ value: 50, currency: 'USD' }} />}
onSubmitAnalytics={onSubmitAnalytics}
/>
);

const highlightedIssuerDropdownItem = wrapper.find('ul li').at(1);
highlightedIssuerDropdownItem.simulate('click');

expect(onSubmitAnalytics).toHaveBeenCalledWith({
type: ANALYTICS_SELECTED_STR,
target: ANALYTICS_LIST,
issuer: 'Issuer 2'
});
});
});
31 changes: 30 additions & 1 deletion packages/lib/src/components/internal/IssuerList/IssuerList.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Fragment, h } from 'preact';
import { useState, useEffect, useCallback } from 'preact/hooks';
import { useState, useEffect, useCallback, useRef } from 'preact/hooks';
import useForm from '../../../utils/useForm';
import Field from '../FormFields/Field';
import IssuerButtonGroup from './IssuerButtonGroup';
Expand All @@ -15,6 +15,17 @@ import { ERROR_ACTION_FOCUS_FIELD } from '../../../core/Errors/constants';
import { setFocusOnField } from '../../../utils/setFocus';
import DisclaimerMessage from '../DisclaimerMessage';
import Select from '../FormFields/Select';
import { SelectTargetObject } from '../FormFields/Select/types';
import {
ANALYTICS_DISPLAYED_STR,
ANALYTICS_FEATURED_ISSUER,
ANALYTICS_INPUT_STR,
ANALYTICS_LIST,
ANALYTICS_LIST_SEARCH,
ANALYTICS_SEARCH_DEBOUNCE_TIME,
ANALYTICS_SELECTED_STR
} from '../../../core/Analytics/constants';
import { debounce } from '../../../utils/debounce';

const payButtonLabel = ({ issuer, items }, i18n): string => {
const issuerName = items.find(i => i.id === issuer)?.name;
Expand Down Expand Up @@ -57,12 +68,28 @@ function IssuerList({ items, placeholder = 'idealIssuer.selectField.placeholder'

const handleInputChange = useCallback(
(type: IssuerListInputTypes) => (event: h.JSX.TargetedKeyboardEvent<HTMLInputElement>) => {
const target = type === IssuerListInputTypes.Dropdown ? ANALYTICS_LIST : ANALYTICS_FEATURED_ISSUER;
const issuerObj = items.find(issuer => issuer.id === (event.target as SelectTargetObject).value);
props.onSubmitAnalytics({ type: ANALYTICS_SELECTED_STR, target, issuer: issuerObj.name });

setInputType(type);
handleChangeFor('issuer')(event);
},
[handleChangeFor]
);

const handleListToggle = useCallback((isOpen: boolean) => {
if (isOpen) {
props.onSubmitAnalytics({ type: ANALYTICS_DISPLAYED_STR, target: ANALYTICS_LIST });
}
}, []);

const debounceSearchAnalytics = useRef(debounce(props.onSubmitAnalytics, ANALYTICS_SEARCH_DEBOUNCE_TIME));

const handleSearch = useCallback(() => {
debounceSearchAnalytics.current({ type: ANALYTICS_INPUT_STR, target: ANALYTICS_LIST_SEARCH });
}, []);

useEffect(() => {
props.onChange({ data, valid, errors, isValid });

Expand Down Expand Up @@ -106,6 +133,8 @@ function IssuerList({ items, placeholder = 'idealIssuer.selectField.placeholder'
name={'issuer'}
className={'adyen-checkout__issuer-list__dropdown'}
onChange={handleInputChange(IssuerListInputTypes.Dropdown)}
onListToggle={handleListToggle}
onInput={handleSearch}
/>
</Field>

Expand Down
2 changes: 2 additions & 0 deletions packages/lib/src/components/internal/IssuerList/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PayButtonProps } from '../PayButton/PayButton';
import { ComponentChildren } from 'preact';
import { SendAnalyticsObject } from '../../../core/Analytics/types';

export interface IssuerListProps {
items: IssuerItem[];
Expand All @@ -10,6 +11,7 @@ export interface IssuerListProps {
placeholder?: string;
issuer?: string;
termsAndConditions?: TermsAndConditions;
onSubmitAnalytics: (aObj: SendAnalyticsObject) => void;
}

export interface IssuerItem {
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/src/core/Analytics/Analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import logEvent from '../Services/analytics/log-event';
import { PaymentAmount } from '../../types';
import { createAnalyticsObject } from './utils';
import wait from '../../utils/wait';
import { DEFAULT_DEBOUNCE_TIME_MS } from '../../components/internal/Address/utils';
import { DEFAULT_DEBOUNCE_TIME_MS } from '../../utils/debounce';
import { ANALYTICS_EVENT } from './types';

jest.mock('../Services/analytics/collect-id');
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/src/core/Analytics/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import CollectId from '../Services/analytics/collect-id';
import EventsQueue, { EventsQueueModule } from './EventsQueue';
import { ANALYTICS_EVENT, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps, CreateAnalyticsEventObject } from './types';
import { ANALYTICS_EVENT_ERROR, ANALYTICS_EVENT_INFO, ANALYTICS_EVENT_LOG, ANALYTICS_INFO_TIMER_INTERVAL, ANALYTICS_PATH } from './constants';
import { debounce } from '../../components/internal/Address/utils';
import { debounce } from '../../utils/debounce';
import { AnalyticsModule } from '../../components/types';
import { createAnalyticsObject } from './utils';
import { analyticsPreProcessor } from './analyticsPreProcessor';
Expand Down
Loading

0 comments on commit 077e23b

Please sign in to comment.