Skip to content

Commit

Permalink
Add analytics to ascertain when certain "wallets" are explicitly used…
Browse files Browse the repository at this point in the history
… as express payment methods (#2616)

* Add analytics to ascertain when certain "wallets" are explicitly used as express payment methods

* Updating Playground filed
  • Loading branch information
sponglord authored Mar 25, 2024
1 parent db2e807 commit d14f791
Show file tree
Hide file tree
Showing 19 changed files with 255 additions and 42 deletions.
5 changes: 5 additions & 0 deletions .changeset/wise-tigers-sniff.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@adyen/adyen-web": patch
---

Add analytics to ascertain when certain "wallets" are explicitly used as express payment methods
14 changes: 14 additions & 0 deletions packages/lib/src/components/AmazonPay/AmazonPay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,25 @@ import { AmazonPayElementData, AmazonPayElementProps, CheckoutDetailsRequest } f
import defaultProps from './defaultProps';
import { getCheckoutDetails } from './services';
import './AmazonPay.scss';
import { SendAnalyticsObject } from '../../core/Analytics/types';
import { ANALYTICS_RENDERED_STR } from '../../core/Analytics/constants';

export class AmazonPayElement extends UIElement<AmazonPayElementProps> {
public static type = 'amazonpay';
protected static defaultProps = defaultProps;

protected submitAnalytics(analyticsObj: SendAnalyticsObject) {
let extraAnalyticsObject = {};
if (analyticsObj.type === ANALYTICS_RENDERED_STR) {
const isExpress = this.props.isExpress;
const expressPage = this.props.expressPage ?? null;
extraAnalyticsObject = {
isExpress,
...(isExpress && expressPage && { expressPage }) // We only care about the expressPage value if isExpress is true
};
}
super.submitAnalytics({ ...analyticsObj, ...extraAnalyticsObject });
}
formatProps(props) {
return {
...props,
Expand Down
3 changes: 2 additions & 1 deletion packages/lib/src/components/AmazonPay/defaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const defautProps: Partial<AmazonPayElementProps> = {
showSignOutButton: false,
showPayButton: true,
onClick: resolve => resolve(),
onSignOut: resolve => resolve()
onSignOut: resolve => resolve(),
isExpress: false
};

export default defautProps;
11 changes: 11 additions & 0 deletions packages/lib/src/components/AmazonPay/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,17 @@ export interface AmazonPayElementProps extends UIElementProps {
onClick: (resolve, reject) => Promise<void>;
onError: (error, component) => void;
onSignOut: (resolve, reject) => Promise<void>;

/**
* Used for analytics
*/
expressPage?: 'cart' | 'minicart' | 'pdp' | 'checkout';

/**
* Used for analytics
* @defaultValue false
*/
isExpress?: boolean;
}

export interface AmazonPayComponentProps extends AmazonPayElementProps {
Expand Down
16 changes: 15 additions & 1 deletion packages/lib/src/components/ApplePay/ApplePay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ 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_INSTANT_PAYMENT_BUTTON, ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants';
import { ANALYTICS_INSTANT_PAYMENT_BUTTON, ANALYTICS_RENDERED_STR, ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants';
import { DecodeObject } from '../types';
import { SendAnalyticsObject } from '../../core/Analytics/types';

const latestSupportedVersion = 14;

Expand All @@ -26,6 +27,19 @@ class ApplePayElement extends UIElement<ApplePayElementProps> {
this.validateMerchant = this.validateMerchant.bind(this);
}

protected submitAnalytics(analyticsObj: SendAnalyticsObject) {
let extraAnalyticsObject = {};
if (analyticsObj.type === ANALYTICS_RENDERED_STR) {
const isExpress = this.props.isExpress;
const expressPage = this.props.expressPage ?? null;
extraAnalyticsObject = {
isExpress,
...(isExpress && expressPage && { expressPage }) // We only care about the expressPage value if isExpress is true
};
}
super.submitAnalytics({ ...analyticsObj, ...extraAnalyticsObject });
}

/**
* Formats the component props
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/src/components/ApplePay/defaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const defaultProps = {

initiative: 'web',

isExpress: false,

/**
* https://developer.apple.com/documentation/apple_pay_on_the_web/applepaypaymentrequest/1916120-lineitems
* A set of line items that explain recurring payments and additional charges and discounts.
Expand Down
11 changes: 11 additions & 0 deletions packages/lib/src/components/ApplePay/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,17 @@ export type OnAuthorizedCallback = (
) => void;

export interface ApplePayElementProps extends UIElementProps {
/**
* Used for analytics
* @defaultValue false
*/
isExpress?: boolean;

/**
* Used for analytics
*/
expressPage?: 'cart' | 'minicart' | 'pdp' | 'checkout';

/**
* The Apple Pay version number your website supports.
* @default highest supported version by the shopper device
Expand Down
16 changes: 15 additions & 1 deletion packages/lib/src/components/GooglePay/GooglePay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,27 @@ import { GooglePayProps } from './types';
import { mapBrands, getGooglePayLocale } from './utils';
import collectBrowserInfo from '../../utils/browserInfo';
import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError';
import { ANALYTICS_INSTANT_PAYMENT_BUTTON, ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants';
import { ANALYTICS_INSTANT_PAYMENT_BUTTON, ANALYTICS_RENDERED_STR, ANALYTICS_SELECTED_STR } from '../../core/Analytics/constants';
import { SendAnalyticsObject } from '../../core/Analytics/types';

class GooglePay extends UIElement<GooglePayProps> {
public static type = 'paywithgoogle';
public static defaultProps = defaultProps;
protected googlePay = new GooglePayService(this.props);

protected submitAnalytics(analyticsObj: SendAnalyticsObject) {
let extraAnalyticsObject = {};
if (analyticsObj.type === ANALYTICS_RENDERED_STR) {
const isExpress = this.props.isExpress;
const expressPage = this.props.expressPage ?? null;
extraAnalyticsObject = {
isExpress,
...(isExpress && expressPage && { expressPage }) // We only care about the expressPage value if isExpress is true
};
}
super.submitAnalytics({ ...analyticsObj, ...extraAnalyticsObject });
}

/**
* Formats the component data input
* For legacy support - maps configuration.merchantIdentifier to configuration.merchantId
Expand Down
2 changes: 2 additions & 0 deletions packages/lib/src/components/GooglePay/defaultProps.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export default {
environment: 'TEST',

isExpress: false,

// isReadyToPayRequest
existingPaymentMethodRequired: false,

Expand Down
11 changes: 11 additions & 0 deletions packages/lib/src/components/GooglePay/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ export interface GooglePayProps extends UIElementProps {
*/
emailRequired?: boolean;

/**
* Used for analytics
*/
expressPage?: 'cart' | 'minicart' | 'pdp' | 'checkout';

/**
* Used for analytics
* @defaultValue false
*/
isExpress?: boolean;

/**
* Set to true to request a full shipping address.
* @defaultValue false
Expand Down
15 changes: 15 additions & 0 deletions packages/lib/src/components/PayPal/Paypal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import CoreProvider from '../../core/Context/CoreProvider';
import AdyenCheckoutError from '../../core/Errors/AdyenCheckoutError';
import { ERRORS } from './constants';
import { createShopperDetails } from './utils/create-shopper-details';
import { SendAnalyticsObject } from '../../core/Analytics/types';
import { ANALYTICS_RENDERED_STR } from '../../core/Analytics/constants';

class PaypalElement extends UIElement<PayPalElementProps> {
public static type = 'paypal';
Expand All @@ -28,6 +30,19 @@ class PaypalElement extends UIElement<PayPalElementProps> {
this.handleOnShippingOptionsChange = this.handleOnShippingOptionsChange.bind(this);
}

protected submitAnalytics(analyticsObj: SendAnalyticsObject) {
let extraAnalyticsObject = {};
if (analyticsObj.type === ANALYTICS_RENDERED_STR) {
const isExpress = this.props.isExpress;
const expressPage = this.props.expressPage ?? null;
extraAnalyticsObject = {
isExpress,
...(isExpress && expressPage && { expressPage }) // We only care about the expressPage value if isExpress is true
};
}
super.submitAnalytics({ ...analyticsObj, ...extraAnalyticsObject });
}

formatProps(props: PayPalElementProps): PayPalElementProps {
const { merchantId, intent: intentFromConfig } = props.configuration;
const isZeroAuth = props.amount?.value === 0;
Expand Down
7 changes: 6 additions & 1 deletion packages/lib/src/components/PayPal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,10 +165,15 @@ interface PayPalCommonProps {
onShippingOptionsChange?: (data: any, actions: { reject: (reason?: string) => Promise<void> }) => Promise<void>;

/**
* Identifies if the payment is Express.
* Identifies if the payment is Express. Also used for analytics.
* @defaultValue false
*/
isExpress?: boolean;

/**
* Used for analytics
*/
expressPage?: 'cart' | 'minicart' | 'pdp' | 'checkout';
}

export interface PayPalConfig {
Expand Down
47 changes: 39 additions & 8 deletions packages/lib/src/core/Analytics/Analytics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const amount: PaymentAmount = { value: 50000, currency: 'USD' };

const mockCheckoutAttemptId = '123456';

const event = {
const setUpEvent = {
containerWidth: 100,
component: 'card',
flavor: 'components'
Expand Down Expand Up @@ -61,26 +61,57 @@ describe('Analytics initialisation and event queue', () => {
test('Should not fire any calls if analytics is disabled', () => {
const analytics = Analytics({ analytics: { enabled: false }, loadingContext: '', locale: '', clientKey: '', amount });

analytics.setUp(event);
analytics.setUp(setUpEvent);
expect(collectIdPromiseMock).not.toHaveBeenCalled();
expect(logEventPromiseMock).not.toHaveBeenCalled();
});

test('Will not call the collectId endpoint if telemetry is disabled, but will call the logEvent (analytics pixel)', () => {
const analytics = Analytics({ analytics: { telemetry: false }, loadingContext: '', locale: '', clientKey: '', amount });
expect(collectIdPromiseMock).not.toHaveBeenCalled();
analytics.setUp(event);
analytics.setUp(setUpEvent);
expect(collectIdPromiseMock).not.toHaveBeenCalled();

expect(logEventPromiseMock).toHaveBeenCalledWith({ ...event });
expect(logEventPromiseMock).toHaveBeenCalledWith({ ...setUpEvent });
});

test('Calls the collectId endpoint by default, adding expected fields', async () => {
analytics.setUp(event);
test('Calls the collectId endpoint by default, adding expected fields, including sanitising the passed analyticsData object', async () => {
const applicationInfo = {
merchantApplication: {
name: 'merchant_application_name',
version: 'version'
},
externalPlatform: {
name: 'external_platform_name',
version: 'external_platform_version',
integrator: 'getSystemIntegratorName'
}
};

analytics = Analytics({
analytics: {
analyticsData: {
applicationInfo,
// @ts-ignore - this is one of the things we're testing
foo: {
bar: 'val'
}
}
},
loadingContext: '',
locale: '',
clientKey: '',
amount
});

analytics.setUp(setUpEvent);

expect(collectIdPromiseMock).toHaveBeenCalled();
await Promise.resolve(); // wait for the next tick
expect(collectIdPromiseMock).toHaveBeenCalledWith({ ...event });

const enhancedSetupEvent = { ...setUpEvent, applicationInfo };

expect(collectIdPromiseMock).toHaveBeenCalledWith({ ...enhancedSetupEvent });

expect(analytics.getCheckoutAttemptId()).toEqual(mockCheckoutAttemptId);
});
Expand All @@ -91,7 +122,7 @@ describe('Analytics initialisation and event queue', () => {
};
const analytics = Analytics({ analytics: { payload }, loadingContext: '', locale: '', clientKey: '', amount });

analytics.setUp(event);
analytics.setUp(setUpEvent);

expect(collectIdPromiseMock).toHaveLength(0);
});
Expand Down
13 changes: 9 additions & 4 deletions packages/lib/src/core/Analytics/Analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ANALYTICS_EVENT, AnalyticsInitialEvent, AnalyticsObject, AnalyticsProps
import { ANALYTICS_EVENT_ERROR, ANALYTICS_EVENT_INFO, ANALYTICS_EVENT_LOG, ANALYTICS_INFO_TIMER_INTERVAL, ANALYTICS_PATH } from './constants';
import { debounce } from '../../utils/debounce';
import { AnalyticsModule } from '../../components/types';
import { createAnalyticsObject } from './utils';
import { createAnalyticsObject, processAnalyticsData } from './utils';
import { analyticsPreProcessor } from './analyticsPreProcessor';

let capturedCheckoutAttemptId = null;
Expand All @@ -16,7 +16,8 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy
const defaultProps = {
enabled: true,
telemetry: true,
checkoutAttemptId: null
checkoutAttemptId: null,
analyticsData: {}
};

const props = { ...defaultProps, ...analytics };
Expand Down Expand Up @@ -71,13 +72,17 @@ const Analytics = ({ loadingContext, locale, clientKey, analytics, amount, analy
setUp: async (initialEvent: AnalyticsInitialEvent) => {
const { enabled, payload, telemetry } = props; // TODO what is payload, is it ever used?

// console.log('### Analytics::setUp:: initialEvent', initialEvent);
const analyticsData = processAnalyticsData(props.analyticsData);

if (enabled === true) {
if (telemetry === true && !capturedCheckoutAttemptId) {
try {
// fetch a new checkoutAttemptId if none is already available
const checkoutAttemptId = await collectId({ ...initialEvent, ...(payload && { ...payload }) });
const checkoutAttemptId = await collectId({
...initialEvent,
...(payload && { ...payload }),
...(Object.keys(analyticsData).length && { ...analyticsData })
});
capturedCheckoutAttemptId = checkoutAttemptId;
} catch (e) {
console.warn(`Fetching checkoutAttemptId failed.${e ? ` Error=${e}` : ''}`);
Expand Down
11 changes: 10 additions & 1 deletion packages/lib/src/core/Analytics/analyticsPreProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,16 @@ export const analyticsPreProcessor = (analyticsModule: AnalyticsModule) => {
*/
// Called from BaseElement (when component mounted) or, from DropinComponent (after mounting, when it has finished resolving all the PM promises)
// &/or, from DropinComponent when a PM is selected
case ANALYTICS_RENDERED_STR:
case ANALYTICS_RENDERED_STR: {
const { isStoredPaymentMethod, brand, isExpress, expressPage } = analyticsObj;
const data = { component, type, isStoredPaymentMethod, brand, isExpress, expressPage };

analyticsModule.createAnalyticsEvent({
event: ANALYTICS_EVENT_INFO,
data
});
break;
}
case ANALYTICS_CONFIGURED_STR: {
const { isStoredPaymentMethod, brand } = analyticsObj;
const data = { component, type, isStoredPaymentMethod, brand };
Expand Down
Loading

0 comments on commit d14f791

Please sign in to comment.