Skip to content

Commit

Permalink
Feature/v5 card adds analytics config data (#2724)
Browse files Browse the repository at this point in the history
* Card passes this.props to submitAnalytics

* Added util that analyticsPreProcessor.ts can call to generate configData for the (Custom)Card comp

* Passing on configData to endpoint

* Fixed accidental overwrite of analytics "type" value in rendered event

* Added unit test for billingAddressMode

* Fix for edge case where both addressLookup and billingAddressMode:'partial' are set

* Adding more configData values, with corresponding unit tests

* Added ConfigData type

* Adding 5 more props & tests

* Adding another 5 props & tests

* Playground Card has all config options listed

* minimumExpiryDate is sent as a boolean

* default value of showInstallmentAmounts is null, but it is alway represented in configData as a boolean

* billingAddressMode value is always passed on (rather than being forced to "none")

* billingAddressAllowedCountries has default value in CardInput.defaultProps

* isStylesConfigured ==> hasStylesConfigured. booleans added to show if callback functions have been defined

* Resolved some types. All expected configData for the Card component is now ready to be sent in

* Updated and added to unit tests

* Clarifying types

* allowing configData to be sent to endpoint. Adding props related to srPanel & riskModule

* Skipping tests until endpoint is ready

* Adding changeset file

* Updating e2e test to reflect third party changes to markup

* Limiting configData length until endpoint is ready

* Adjusting unit tests to allow for presence of configData

* Prefixing booleans, indicating presence of callbacks, with "has"

* Card component merges CardInput's default props into its own

* Cleaner way to detect if default callbacks have been overridden

* Card shouldn't merge all of cardInputs default props

* Also send configData for Bcmc. Minor reformatting
  • Loading branch information
sponglord authored Jul 19, 2024
1 parent e69d807 commit 2f38b88
Show file tree
Hide file tree
Showing 12 changed files with 1,114 additions and 57 deletions.
5 changes: 5 additions & 0 deletions .changeset/forty-bulldogs-fly.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@adyen/adyen-web": minor
---

Card component adds configData for analytics (added to "rendered" Info Event)
25 changes: 21 additions & 4 deletions packages/lib/src/components/Card/Card.Analytics.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Analytics from '../../core/Analytics';
const analyticsModule = Analytics({ analytics: {}, loadingContext: '', locale: '', clientKey: '' });

let card;
let analyticsEventObject;

import {
ANALYTICS_CONFIGURED_STR,
Expand All @@ -23,18 +24,29 @@ describe('Card: calls that generate "info" analytics should produce objects with
card = new CardElement({
modules: {
analytics: analyticsModule
}
},
type: 'card'
});

analyticsModule.createAnalyticsEvent = jest.fn(() => null);
analyticsEventObject = null;

// @ts-ignore it's a test
analyticsModule.createAnalyticsEvent = jest.fn(aObj => {
analyticsEventObject = aObj;
});
});

test('Analytics should produce an "info" event, of type "rendered", for a card PM', () => {
card.submitAnalytics({
type: ANALYTICS_RENDERED_STR
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
// configData is too complex an object to fully inspect - but expect it to be there
expect(analyticsEventObject.data.configData).toBeDefined();
delete analyticsEventObject.data.configData;

// With configData removed inspect what's left
expect(analyticsEventObject).toEqual({
event: ANALYTICS_EVENT_INFO,
data: { component: card.constructor['type'], type: ANALYTICS_RENDERED_STR }
});
Expand All @@ -47,7 +59,12 @@ describe('Card: calls that generate "info" analytics should produce objects with
brand: 'mc'
});

expect(analyticsModule.createAnalyticsEvent).toHaveBeenCalledWith({
// configData is too complex an object to fully inspect - but expect it to be there
expect(analyticsEventObject.data.configData).toBeDefined();
delete analyticsEventObject.data.configData;

// With configData removed inspect what's left
expect(analyticsEventObject).toEqual({
event: ANALYTICS_EVENT_INFO,
data: { component: card.constructor['type'], type: ANALYTICS_RENDERED_STR, isStoredPaymentMethod: true, brand: 'mc' }
});
Expand Down
12 changes: 8 additions & 4 deletions packages/lib/src/components/Card/Card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { FieldErrorAnalyticsObject, SendAnalyticsObject } from '../../core/Analy
import { hasOwnProperty } from '../../utils/hasOwnProperty';
import { ERROR_CODES } from '../../core/Errors/constants';
import { getErrorMessageFromCode } from '../../core/Errors/utils';
import CardInputDefaultProps from './components/CardInput/defaultProps';

export class CardElement extends UIElement<CardElementProps> {
public static type = 'scheme';
Expand All @@ -47,10 +48,11 @@ export class CardElement extends UIElement<CardElementProps> {
}

protected static defaultProps = {
onBinLookup: () => {},
showBrandsUnderCardNumber: true,
showFormInstruction: true,
_disableClickToPay: false
_disableClickToPay: false,
doBinLookup: true,
// Merge most of CardInput's defaultProps
...reject(['type', 'setComponentRef']).from(CardInputDefaultProps)
};

public setStatus(status: UIElementStatus, props?): this {
Expand Down Expand Up @@ -82,6 +84,8 @@ export class CardElement extends UIElement<CardElementProps> {
hasCVC: !((props.brand && props.brand === 'bcmc') || props.hideCVC),
// billingAddressRequired only available for non-stored cards
billingAddressRequired: props.storedPaymentMethodId ? false : props.billingAddressRequired,
// edge case where merchant has defined both an onAddressLookup callback AND set billingAddressMode: 'partial' - which leads to some strange behaviour in the address UI
billingAddressMode: props.onAddressLookup ? CardInputDefaultProps.billingAddressMode : props.billingAddressMode,
// ...(props.brands && !props.groupTypes && { groupTypes: props.brands }),
type: props.type === 'scheme' ? 'card' : props.type,
countryCode: props.countryCode ? props.countryCode.toLowerCase() : null,
Expand Down Expand Up @@ -190,7 +194,7 @@ export class CardElement extends UIElement<CardElementProps> {
}
}

super.submitAnalytics(analyticsObj);
super.submitAnalytics(analyticsObj, this.props);
}

private onConfigSuccess = (obj: CbObjOnConfigSuccess) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,38 @@ export default {
setComponentRef: () => {},

// Settings
autoFocus: true,
billingAddressAllowedCountries: [],
billingAddressMode: AddressModeOptions.full,
billingAddressRequired: false,
billingAddressRequiredFields: ['street', 'houseNumberOrName', 'postalCode', 'city', 'stateOrProvince', 'country'],

configuration: { koreanAuthenticationRequired: false, socialSecurityNumberMode: 'auto' as SocialSecurityMode },
data: {
billingAddress: {}
},
disableIOSArrowKeys: true,
enableStoreDetails: false,
exposeExpiryDate: false,
forceCompat: false,
hasHolderName: false,
holderNameRequired: false,
enableStoreDetails: false,
hasCVC: true,
hideCVC: false,
installmentOptions: {},
keypadFix: true,
legacyInputMode: false,
maskSecurityCode: false,
minimumExpiryDate: null,
name: null, // Affects Dropin only, the name displayed in the PMList item
placeholders: {},
positionHolderNameOnTop: false,
showBrandIcon: true,
showBrandsUnderCardNumber: true,
positionHolderNameOnTop: false,
billingAddressRequired: false,
billingAddressMode: AddressModeOptions.full,
billingAddressRequiredFields: ['street', 'houseNumberOrName', 'postalCode', 'city', 'stateOrProvince', 'country'],
installmentOptions: {},
configuration: { koreanAuthenticationRequired: false, socialSecurityNumberMode: 'auto' as SocialSecurityMode },
autoFocus: true,
showInstallmentAmounts: null,
styles: {},

isPayButtonPrimaryVariant: true,
disableIOSArrowKeys: true,
exposeExpiryDate: false,

// Events
onLoad: (): any => {},
Expand All @@ -36,12 +52,5 @@ export default {
onFocus: (): any => {},
onChange: (): any => {},

// Values
data: {
billingAddress: {}
},

// Customization
styles: {},
placeholders: {}
onBinLookup: () => {} // Strictly speaking a Card level prop, but needed here for analytics.configData
};
9 changes: 8 additions & 1 deletion packages/lib/src/components/Card/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import {
CbObjOnLoad,
CbObjOnBinLookup,
CVCPolicyType,
DatePolicyType
DatePolicyType,
CbObjOnAllValid
} from '../internal/SecuredFields/lib/types';
import { ClickToPayConfiguration } from '../internal/ClickToPay/types';

Expand Down Expand Up @@ -109,6 +110,12 @@ export interface CardElementProps extends UIElementProps {
*/
onConfigSuccess?: (event: CbObjOnConfigSuccess) => void;

/**
* Called when *all* the securedFields becomes valid
* Also called again if one of the fields moves out of validity.
*/
onAllValid?: (event: CbObjOnAllValid) => void;

/**
* Called when a field becomes valid and also if a valid field changes and becomes invalid.
* For the card number field, it returns the last 4 digits of the card number.
Expand Down
5 changes: 5 additions & 0 deletions packages/lib/src/components/SecuredFields/SecuredFields.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CbObjOnBinLookup, CbObjOnFocus } from '../internal/SecuredFields/lib/ty
import { BrandObject } from '../Card/types';
import { fieldTypeToSnakeCase, getCardImageUrl } from '../internal/SecuredFields/utils';
import { ANALYTICS_FOCUS_STR, ANALYTICS_UNFOCUS_STR } from '../../core/Analytics/constants';
import { SendAnalyticsObject } from '../../core/Analytics/types';

export class SecuredFieldsElement extends UIElement {
public static type = 'scheme';
Expand Down Expand Up @@ -40,6 +41,10 @@ export class SecuredFieldsElement extends UIElement {
};
}

protected submitAnalytics(analyticsObj: SendAnalyticsObject) {
super.submitAnalytics(analyticsObj, this.props);
}

updateStyles(stylesObj) {
if (this.componentRef?.updateStyles) this.componentRef.updateStyles(stylesObj);
return this;
Expand Down
Loading

0 comments on commit 2f38b88

Please sign in to comment.