diff --git a/.eslintrc.js b/.eslintrc.js index ca4447fd..3d029d74 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -149,7 +149,6 @@ module.exports = { 'no-useless-call': 'error', 'no-useless-concat': 'error', 'no-void': 'error', - 'no-warning-comments': 'error', 'no-with': 'error', 'radix': 'error', 'vars-on-top': 'off', @@ -420,7 +419,7 @@ module.exports = { 'import/exports-last': 'off', 'import/group-exports': 'off', 'import/no-cycle': 'error', - 'import/no-default-export': 'error', + 'import/no-default-export': 'off', 'import/no-self-import': 'error', 'import/no-useless-path-segments': 'error', diff --git a/.flowconfig b/.flowconfig index fefe0d1d..d9ccc79f 100644 --- a/.flowconfig +++ b/.flowconfig @@ -3,6 +3,7 @@ .*/node_modules/npm .*/node_modules/jsonlint .*/dist/module +.*/test/tracker/set-user.test.js [untyped] .*/src/lib/jetlore [include] diff --git a/src/declarations.js b/src/declarations.js index d08e326f..6ac1f2bf 100644 --- a/src/declarations.js +++ b/src/declarations.js @@ -1,5 +1,5 @@ /* @flow */ -import type { MuseGlobalType } from './types'; +import type { MuseGlobalType } from './lib/types'; declare var __muse__ : MuseGlobalType; diff --git a/src/lib/compose-cart.js b/src/lib/compose-cart.js index d2ccbb5e..cd803408 100644 --- a/src/lib/compose-cart.js +++ b/src/lib/compose-cart.js @@ -1,4 +1,11 @@ /* @flow */ +import { getCookie } from './cookie-utils'; +import constants from './constants'; + +const { + storage, + sevenDays +} = constants; // $FlowFixMe export const removeFromCart = (items, currentItems = []) => { return items.reduce((accumulator, item) => { @@ -26,3 +33,39 @@ export const removeFromCart = (items, currentItems = []) => { export const addToCart = (items, currentItems = []) => { return [ ...currentItems, ...items ]; }; +// $FlowFixMe +export const composeCart = (type, data) => { + // Copy the data so we don't modify it outside the scope of this method. + let _data = { ...data }; + + // Devnote: Checking for cookie for backwards compatibility (the cookie check can be removed + // a couple weeks after deploy because any cart cookie storage will be moved to localStorage + // in this function). + const storedCart = window.localStorage.getItem(storage.paypalCrCart) || getCookie(storage.paypalCrCart) || '{}'; + const expiry = window.localStorage.getItem(storage.paypalCrCartExpiry); + const cart = JSON.parse(storedCart); + const currentItems = cart ? cart.items : []; + + if (!expiry) { + window.localStorage.setItem(storage.paypalCrCartExpiry, Date.now() + sevenDays); + } + + switch (type) { + case 'add': + _data.items = addToCart(data.items, currentItems); + break; + case 'set': + _data.items = data.items; + break; + case 'remove': + _data = { ...cart, ...data }; + _data.items = removeFromCart(data.items, currentItems); + break; + default: + throw new Error('invalid cart action'); + } + + window.localStorage.setItem(storage.paypalCrCart, JSON.stringify(_data)); + + return _data; +}; diff --git a/src/lib/constants.js b/src/lib/constants.js new file mode 100644 index 00000000..6af09fe7 --- /dev/null +++ b/src/lib/constants.js @@ -0,0 +1,16 @@ +/* @flow */ +export default { + 'sevenDays': 6.048e+8, + 'accessTokenUrl': 'https://www.paypal.com/muse/api/partner-token', + 'storage': { + 'paypalCrCart': 'paypal-cr-cart', + 'paypalCrCartExpiry': 'paypal-cr-cart-expiry' + }, + 'defaultTrackerConfig': { + 'user': { + 'id': null, + 'email': null, + 'name': null + } + } +}; diff --git a/src/lib/cookie-utils.js b/src/lib/cookie-utils.js index 836410c2..02104feb 100644 --- a/src/lib/cookie-utils.js +++ b/src/lib/cookie-utils.js @@ -1,4 +1,5 @@ /* @flow */ +import generate from './generate-id'; export const getCookie = (cookieName : string) : string => { const name = `${ cookieName }=`; @@ -21,3 +22,12 @@ export const setCookie = (cookieName : string, cookieValue : string, expirationM const expires = `expires=${ d.toUTCString() }`; document.cookie = `${ cookieName }=${ cookieValue }; Path=/; ${ expires }`; }; + +export const getUserIdCookie = () : ?string => { + return getCookie('paypal-user-id') || null; +}; + +export const setRandomUserIdCookie = () : void => { + const ONE_MONTH_IN_MILLISECONDS = 30 * 24 * 60 * 60 * 1000; + setCookie('paypal-user-id', generate.generateId(), ONE_MONTH_IN_MILLISECONDS); +}; diff --git a/src/generate-id.js b/src/lib/generate-id.js similarity index 85% rename from src/generate-id.js rename to src/lib/generate-id.js index 6359016b..b7fe3c09 100644 --- a/src/generate-id.js +++ b/src/lib/generate-id.js @@ -7,7 +7,6 @@ import { uniqueID } from 'belter/src'; ** - flow and eslint have to be disabled on whatever line this is used. */ -// eslint-disable-next-line import/no-default-export export default { generateId: uniqueID }; diff --git a/src/lib/get-property-id.js b/src/lib/get-property-id.js new file mode 100644 index 00000000..8811c5c0 --- /dev/null +++ b/src/lib/get-property-id.js @@ -0,0 +1,40 @@ +/* @flow */ +import { getClientID, getMerchantID } from '@paypal/sdk-client/src'; + +import type { + Config +} from './types'; + +export const getPropertyId = ({ paramsToPropertyIdUrl } : Config) => { + // $FlowFixMe + return new Promise(resolve => { + const clientId = getClientID(); + const merchantId = getMerchantID()[0]; + const propertyIdKey = `property-id-${ clientId }-${ merchantId }`; + const savedPropertyId = window.localStorage.getItem(propertyIdKey); + const currentUrl = `${ window.location.protocol }//${ window.location.host }`; + if (savedPropertyId) { + return resolve(savedPropertyId); + } + let url; + // $FlowFixMe + if (paramsToPropertyIdUrl) { + url = paramsToPropertyIdUrl(); + } else { + url = 'https://paypal.com/tagmanager/containers/xo'; + } + return window.fetch(`${ url }?mrid=${ merchantId }&url=${ encodeURIComponent(currentUrl) }`) + .then(res => { + if (res.status === 200) { + return res; + } + }) + .then(r => r.json()).then(container => { + window.localStorage.setItem(propertyIdKey, container.id); + resolve(container.id); + }) + .catch(() => { + // doing nothing for now since there's no logging + }); + }); +}; diff --git a/src/lib/track.js b/src/lib/track.js new file mode 100644 index 00000000..f6cb412b --- /dev/null +++ b/src/lib/track.js @@ -0,0 +1,49 @@ +/* @flow */ +import { getClientID, getMerchantID } from '@paypal/sdk-client/src'; + +import { getUserIdCookie, setRandomUserIdCookie } from './cookie-utils'; +import { getDeviceInfo } from './get-device-info'; +import type { + Config, + TrackingType +} from './types'; + +export const track = (config : Config, trackingType : TrackingType, trackingData : T) => { + const encodeData = data => encodeURIComponent(btoa(JSON.stringify(data))); + + const img = document.createElement('img'); + img.style.display = 'none'; + if (!getUserIdCookie()) { + setRandomUserIdCookie(); + } + const user = { + ...config.user, + id: getUserIdCookie() + }; + + const deviceInfo = getDeviceInfo(); + const data = { + ...trackingData, + user, + propertyId: config.propertyId, + trackingType, + clientId: getClientID(), + merchantId: getMerchantID().join(','), + deviceInfo + }; + + // paramsToBeaconUrl is a function that gives you the ability to override the beacon url + // to whatever you want it to be based on the trackingType string and data object. + // This can be useful for testing purposes, this feature won't be used by merchants. + if (config.paramsToBeaconUrl) { + img.src = config.paramsToBeaconUrl({ trackingType, data }); + } else { + img.src = `https://www.paypal.com/targeting/track/${ trackingType }?data=${ encodeData(data) }`; + } + + // TODO: this will add a new image EVERY time the 'track' method is called. There's no reason + // to clutter the DOM like this. We should replace the old image. + if (document.body) { + document.body.appendChild(img); + } +}; diff --git a/src/lib/types.js b/src/lib/types.js new file mode 100644 index 00000000..88038d26 --- /dev/null +++ b/src/lib/types.js @@ -0,0 +1,99 @@ +/* @flow */ + +export const TYPES = true; + +export type MuseServerConfigType = {| + assetsUrl : string +|}; + +export type MuseGlobalType = {| + serverConfig : MuseServerConfigType +|}; + +export type TrackingType = 'view' | 'cartEvent' | 'purchase' | 'setUser' | 'cancelCart'; + +export type CartEventType = 'addToCart' | 'setCart' | 'removeFromCart'; + +export type Product = {| + id : string, + title? : string, + url? : string, + description? : string, + imgUrl? : string, + otherImages? : $ReadOnlyArray, + keywords? : $ReadOnlyArray, + price? : string, + quantity? : string +|}; + +export type ViewData = {| page : string, title? : string |}; + +export type CartData = {| + cartId? : string, + items : $ReadOnlyArray, + emailCampaignId? : string, + total? : string, + currencyCode? : string +|}; + +export type CancelCartData = {| + cartId? : string +|}; + +export type RemoveCartData = {| + cartId? : string, + items : $ReadOnlyArray<{ id : string }> +|}; + +export type PurchaseData = {| cartId : string |}; + +export type UserData = {| + user : {| + id? : string, + email? : string, + name? : string + |} +|}; + +export type IdentityData = {| + mrid : string, + onIdentification : Function, + onError? : Function +|}; + +export type ParamsToBeaconUrl = ({ + trackingType : TrackingType, + data : ViewData | CartData | RemoveCartData | PurchaseData | CancelCartData +}) => string; + +export type ParamsToTokenUrl = () => string; + +export type ParamsToPropertyIdUrl = () => string; + +export type JetloreConfig = {| + user_id : string, + cid : string, + feed_id : string, + div? : string, + lang? : string +|}; + +export type Config = {| + user? : {| + id? : string, + email? : string, // mandatory if unbranded cart recovery + name? : string + |}, + propertyId? : string, + paramsToBeaconUrl? : ParamsToBeaconUrl, + paramsToTokenUrl? : ParamsToTokenUrl, + jetlore? : {| + user_id : string, + access_token : string, + feed_id : string, + div? : string, + lang? : string + |}, + paramsToPropertyIdUrl? : ParamsToPropertyIdUrl +|}; + diff --git a/src/tracker-component.js b/src/tracker-component.js index 664ecba4..901e9131 100644 --- a/src/tracker-component.js +++ b/src/tracker-component.js @@ -4,150 +4,31 @@ import 'whatwg-fetch'; // eslint-disable-line import/no-unassigned-import import { getClientID, getMerchantID } from '@paypal/sdk-client/src'; // $FlowFixMe -import generate from './generate-id'; -import { getCookie, setCookie } from './lib/cookie-utils'; +import { getUserIdCookie } from './lib/cookie-utils'; +import { getPropertyId } from './lib/get-property-id'; import getJetlore from './lib/jetlore'; -import { getDeviceInfo } from './lib/get-device-info'; -import { removeFromCart, addToCart } from './lib/compose-cart'; - -type TrackingType = 'view' | 'cartEvent' | 'purchase' | 'setUser' | 'cancelCart'; - -type CartEventType = 'addToCart' | 'setCart' | 'removeFromCart'; - -type Product = {| - id : string, - title? : string, - url? : string, - description? : string, - imgUrl? : string, - otherImages? : $ReadOnlyArray, - keywords? : $ReadOnlyArray, - price? : string, - quantity? : string -|}; - -type ViewData = {| page : string, title? : string |}; - -type CartData = {| - cartId? : string, - items : $ReadOnlyArray, - emailCampaignId? : string, - total? : string, - currencyCode? : string -|}; - -type CancelCartData = {| - cartId? : string -|}; - -type RemoveCartData = {| - cartId? : string, - items : $ReadOnlyArray<{ id : string }> -|}; - -type PurchaseData = {| cartId : string |}; - -type UserData = {| - user : {| - email : string, - name? : string - |} -|}; - -type IdentityData = {| - mrid : string, - onIdentification : Function, - onError? : Function -|}; - -type ParamsToBeaconUrl = ({ - trackingType : TrackingType, - data : ViewData | CartData | RemoveCartData | PurchaseData | CancelCartData -}) => string; - -type ParamsToTokenUrl = () => string; - -type ParamsToPropertyIdUrl = () => string; - -type JetloreConfig = {| - user_id : string, - cid : string, - feed_id : string, - div? : string, - lang? : string -|}; - -type Config = {| - user? : {| - email? : string, // mandatory if unbranded cart recovery - name? : string - |}, - propertyId? : string, - paramsToBeaconUrl? : ParamsToBeaconUrl, - paramsToTokenUrl? : ParamsToTokenUrl, - jetlore? : {| - user_id : string, - access_token : string, - feed_id : string, - div? : string, - lang? : string - |}, - paramsToPropertyIdUrl? : ParamsToPropertyIdUrl -|}; - -const storage = { - paypalCrCart: 'paypal-cr-cart', - paypalCrCartExpiry: 'paypal-cr-cart-expiry' -}; - -const sevenDays = 6.048e+8; - -const accessTokenUrl = 'https://www.paypal.com/muse/api/partner-token'; - -const getUserIdCookie = () : ?string => { - return getCookie('paypal-user-id') || null; -}; - -const setRandomUserIdCookie = () : void => { - const ONE_MONTH_IN_MILLISECONDS = 30 * 24 * 60 * 60 * 1000; - setCookie('paypal-user-id', generate.generateId(), ONE_MONTH_IN_MILLISECONDS); -}; - -const composeCart = (type, data) => { - // Copy the data so we don't modify it outside the scope of this method. - let _data = { ...data }; - - // Devnote: Checking for cookie for backwards compatibility (the cookie check can be removed - // a couple weeks after deploy because any cart cookie storage will be moved to localStorage - // in this function). - const storedCart = window.localStorage.getItem(storage.paypalCrCart) || getCookie(storage.paypalCrCart) || '{}'; - const expiry = window.localStorage.getItem(storage.paypalCrCartExpiry); - const cart = JSON.parse(storedCart); - const currentItems = cart ? cart.items : []; - - if (!expiry) { - window.localStorage.setItem(storage.paypalCrCartExpiry, Date.now() + sevenDays); - } - - switch (type) { - case 'add': - _data.items = addToCart(data.items, currentItems); - break; - case 'set': - _data.items = data.items; - break; - case 'remove': - _data = { ...cart, ...data }; - _data.items = removeFromCart(data.items, currentItems); - break; - default: - throw new Error('invalid cart action'); - } - - window.localStorage.setItem(storage.paypalCrCart, JSON.stringify(_data)); - - return _data; -}; +import { composeCart } from './lib/compose-cart'; +import { track } from './lib/track'; +import constants from './lib/constants'; +import type { + CartEventType, + TrackingType, + ViewData, + CartData, + CancelCartData, + RemoveCartData, + PurchaseData, + UserData, + IdentityData, + JetloreConfig, + Config +} from './lib/types'; + +const { + accessTokenUrl, + storage, + defaultTrackerConfig +} = constants; const getAccessToken = (url : string, mrid : string) : Promise => { return fetch(url, { @@ -214,59 +95,27 @@ const getJetlorePayload = (type : string, options : Object) : Object => { let trackEventQueue = []; -const track = (config : Config, trackingType : TrackingType, trackingData : T) => { +export const clearTrackQueue = (config : Config) => { + // TODO: replace 'filter' with 'forEach' + // $FlowFixMe + return trackEventQueue.filter(([ trackingType, trackingData ]) => { // eslint-disable-line array-callback-return + track(config, trackingType, trackingData); + }); +}; + +export const trackEvent = (config : Config, trackingType : TrackingType, trackingData : T) => { + // Events cannot be fired without a propertyId. We add events + // to a queue if a propertyId has not yet been returned. if (!config.propertyId) { trackEventQueue.push([ trackingType, trackingData ]); return; } - const encodeData = data => encodeURIComponent(btoa(JSON.stringify(data))); - - const img = document.createElement('img'); - img.style.display = 'none'; - if (!getUserIdCookie()) { - setRandomUserIdCookie(); - } - const user = { - ...config.user, - id: getUserIdCookie() - }; - const deviceInfo = getDeviceInfo(); - const data = { - ...trackingData, - user, - propertyId: config.propertyId, - trackingType, - clientId: getClientID(), - merchantId: getMerchantID().join(','), - deviceInfo - }; - - // paramsToBeaconUrl is a function that gives you the ability to override the beacon url - // to whatever you want it to be based on the trackingType string and data object. - // This can be useful for testing purposes, this feature won't be used by merchants. - if (config.paramsToBeaconUrl) { - img.src = config.paramsToBeaconUrl({ trackingType, data }); - } else { - img.src = `https://www.paypal.com/targeting/track/${ trackingType }?data=${ encodeData(data) }`; - } - if (document.body) { - document.body.appendChild(img); - } -}; - -// eslint-disable-next-line flowtype/no-weak-types -export const clearTrackQueue = (config : Config, queue : any) => { - // eslint-disable-next-line array-callback-return - return queue.filter(([ trackingType, trackingData ]) => { - track(config, trackingType, trackingData); - }); + track(config, trackingType, trackingData); }; const trackCartEvent = (config : Config, cartEventType : CartEventType, trackingData : T) => - track(config, 'cartEvent', { ...trackingData, cartEventType }); - -const defaultTrackerConfig = { user: { email: undefined, name: undefined } }; + trackEvent(config, 'cartEvent', { ...trackingData, cartEventType }); const clearExpiredCart = () => { const expiry = window.localStorage.getItem(storage.paypalCrCartExpiry); @@ -281,38 +130,6 @@ const clearExpiredCart = () => { } }; -const getPropertyId = ({ paramsToPropertyIdUrl }) => { - return new Promise(resolve => { - const clientId = getClientID(); - const merchantId = getMerchantID()[0]; - const propertyIdKey = `property-id-${ clientId }-${ merchantId }`; - const savedPropertyId = window.localStorage.getItem(propertyIdKey); - const currentUrl = `${ window.location.protocol }//${ window.location.host }`; - if (savedPropertyId) { - return resolve(savedPropertyId); - } - let url; - if (paramsToPropertyIdUrl) { - url = paramsToPropertyIdUrl(); - } else { - url = 'https://paypal.com/tagmanager/containers/xo'; - } - return window.fetch(`${ url }?mrid=${ merchantId }&url=${ encodeURIComponent(currentUrl) }`) - .then(res => { - if (res.status === 200) { - return res; - } - }) - .then(r => r.json()).then(container => { - window.localStorage.setItem(propertyIdKey, container.id); - resolve(container.id); - }) - .catch(() => { - // doing nothing for now since there's no logging - }); - }); -}; - export const setImplicitPropertyId = (config : Config) => { /* ** this is used for backwards compatibility @@ -325,7 +142,7 @@ export const setImplicitPropertyId = (config : Config) => { getPropertyId(config).then(propertyId => { config.propertyId = propertyId; if (trackEventQueue.length) { - trackEventQueue = clearTrackQueue(config, trackEventQueue); + trackEventQueue = clearTrackQueue(config); } }); }; @@ -333,8 +150,10 @@ const clearCancelledCart = () => { window.localStorage.removeItem(storage.paypalCrCartExpiry); window.localStorage.removeItem(storage.paypalCrCart); }; - -export const Tracker = (config? : Config = defaultTrackerConfig) => { +// $FlowFixMe +export const Tracker = (config? : Config = {}) => { + // $FlowFixMe + config = { ...defaultTrackerConfig, ...config }; /* * Use the get param ?ppDebug=true to see logs * @@ -404,19 +223,26 @@ export const Tracker = (config? : Config = defaultTrackerConfig) => { }, purchase: (data : PurchaseData) => track(config, 'purchase', data), setUser: (data : UserData) => { + const user = data.user || data; + const configUser = config.user || {}; + + const userId = user.id !== undefined ? user.id : configUser.id; + const userEmail = user.email !== undefined ? user.email : configUser.email; + const userName = user.name !== undefined ? user.name : configUser.name; + config = { ...config, user: { - ...config.user, - email: data.user.email || ((config && config.user) || {}).email, - name: data.user.name || ((config && config.user) || {}).name + id: userId, + email: userEmail, + name: userName } }; - track(config, 'setUser', { oldUserId: getUserIdCookie() }); + trackEvent(config, 'setUser', { oldUserId: getUserIdCookie() }); }, cancelCart: (data : CancelCartData) => { clearCancelledCart(); - track(config, 'cancelCart', data); + trackEvent(config, 'cancelCart', data); }, setPropertyId: (id : string) => { config.propertyId = id; @@ -456,7 +282,7 @@ export const Tracker = (config? : Config = defaultTrackerConfig) => { // https://github.com/paypal/paypal-muse-components/commit/b3e76554fadd72ad24b6a900b99b8ff75af08815 const trackerFunctions = trackers; - const trackEvent = (type : string, data : Object) => { + const trackEventByType = (type : string, data : Object) => { const isJetloreType = config.jetlore ? jetloreTrackTypes.includes(type) : false; @@ -505,7 +331,7 @@ export const Tracker = (config? : Config = defaultTrackerConfig) => { return { // bringing in tracking functions for backwards compatibility ...trackerFunctions, - track: trackEvent, + track: trackEventByType, identify, getJetlorePayload }; diff --git a/src/types.js b/src/types.js deleted file mode 100644 index e438a64f..00000000 --- a/src/types.js +++ /dev/null @@ -1,12 +0,0 @@ -/* @flow */ - -export const TYPES = true; - -export type MuseServerConfigType = {| - assetsUrl : string -|}; - -export type MuseGlobalType = {| - serverConfig : MuseServerConfigType -|}; - diff --git a/test/paypal.js b/test/paypal.js index 905af63b..fe4c1660 100644 --- a/test/paypal.js +++ b/test/paypal.js @@ -19,8 +19,8 @@ setupSDK([ } ]); -// JSDOM initializes with the 'DOMContentLoaded' event having -// already been fired. We manually fire it after insterting the +// JSDOM initializes with the 'DOMContentLoaded' event having +// already been fired. We manually fire it after insterting the // sdk. const loadEvent = document.createEvent('Event'); loadEvent.initEvent('DOMContentLoaded', true, true); diff --git a/test/tracker-component.test.js b/test/tracker-component.test.js index 54ae78a3..90837a9d 100644 --- a/test/tracker-component.test.js +++ b/test/tracker-component.test.js @@ -1,9 +1,9 @@ /* globals describe beforeAll afterAll afterEach it expect */ /* @flow */ -import { Tracker, clearTrackQueue } from '../src/tracker-component'; +import { Tracker } from '../src/tracker-component'; import { setCookie } from '../src/lib/cookie-utils'; // $FlowFixMe -import generateIdModule from '../src/generate-id'; +import generateIdModule from '../src/lib/generate-id'; const decode = (encodedDataParam : string) : string => { return JSON.parse(atob(decodeURIComponent(encodedDataParam))); @@ -462,9 +462,9 @@ describe('paypal.Tracker', () => { JSON.stringify({ oldUserId: 'abc123', user: { + id: 'abc123', email: '__test__email9', - name: '__test__userName9', - id: 'abc123' + name: '__test__userName9' }, propertyId, trackingType: 'setUser', @@ -496,9 +496,9 @@ describe('paypal.Tracker', () => { JSON.stringify({ oldUserId: 'abc123', user: { + id: 'abc123', email: '__test__email@gmail.com', - name: '__test__name', - id: 'abc123' + name: '__test__name' }, propertyId, trackingType: 'setUser', @@ -544,9 +544,9 @@ describe('paypal.Tracker', () => { currencyCode: 'USD', cartEventType: 'addToCart', user: { + id: 'abc123', email: '__test__email2', - name: '__test__name1', - id: 'abc123' + name: '__test__name1' }, propertyId, trackingType: 'cartEvent', @@ -588,7 +588,7 @@ describe('paypal.Tracker', () => { total: '12345.67', currencyCode: 'USD', cartEventType: 'addToCart', - user: { id: '__test__cookie-id' }, + user: { id: '__test__cookie-id', email: null, name: null }, propertyId, trackingType: 'cartEvent', clientId: 'abcxyz123', @@ -746,7 +746,7 @@ describe('paypal.Tracker', () => { const email = '__test__email3@gmail.com'; const userName = '__test__userName3'; // clear local storage to ensure a request happens - window.localStorage.removeItem('property-id-abcxyz123-xyz') + window.localStorage.removeItem('property-id-abcxyz123-xyz'); Tracker({ user: { email, name: userName } }); expect(appendChildCalls).toBe(0); @@ -758,41 +758,9 @@ describe('paypal.Tracker', () => { const email = '__test__email3@gmail.com'; const userName = '__test__userName3'; // clear local storage to ensure a request happens - window.localStorage.removeItem('property-id-abcxyz123-xyz') + window.localStorage.removeItem('property-id-abcxyz123-xyz'); Tracker({ user: { email, name: userName }, propertyId: 'hello' }); expect(fetchCalls.length).toBe(0); }); - - it('should clear trackEventQueue when function is called', () => { - const email = '__test__email4@gmail.com'; - const userName = '__test__userName4'; - const id = '__test__cookie-id'; - const trackingData = { - cartId: '__test__cartId', - items: [ - { - id: '__test__productId', - url: 'https://example.com/__test__productId' - } - ], - emailCampaignId: '__test__emailCampaignId', - total: '12345.67', - currencyCode: 'USD', - cartEventType: 'addToCart', - user: { email, name: userName, id }, - propertyId, - trackingType: 'cartEvent', - clientId: 'abcxyz123', - merchantId: 'xyz,hij,lmno', - deviceInfo - }; - const trackEventQueue = [ - [ 'addToCart', trackingData ], - [ 'setCart', trackingData ], - [ 'remoteFromCart', trackingData ] - ]; - const result = clearTrackQueue({}, trackEventQueue); - expect(result.length).toBe(0); - }); }); diff --git a/test/tracker/set-user.test.js b/test/tracker/set-user.test.js new file mode 100644 index 00000000..a2826895 --- /dev/null +++ b/test/tracker/set-user.test.js @@ -0,0 +1,189 @@ +/* @flow */ +/* global it describe beforeEach afterAll expect jest */ +import { Tracker } from '../../src/tracker-component'; +import { getPropertyId } from '../../src/lib/get-property-id'; +import { track } from '../../src/lib/track'; +import constants from '../../src/lib/constants'; + +jest.mock('../../src/lib/track'); +jest.mock('../../src/lib/get-property-id', () => { + return { + // eslint-disable-next-line require-await + getPropertyId: async () => 'mockpropertyidofsomekind' + }; +}); + +describe('setUser', () => { + const { defaultTrackerConfig } = constants; + + let config; + let mockItem; + + beforeEach(() => { + config = { + propertyId: 'foobar', + user: { + id: 'arglebargle123', + name: 'Bob Ross', + email: 'bossrob21@pbs.org' + } + }; + + mockItem = { + id: 'XL novelty hat', + imgUrl: 'https://www.paypalobjects.com/digitalassets/c/gifts/media/catalog/product/b/e/bestbuy.jpg', + price: '100.00', + title: 'Best Buy', + url: 'http://localhost.paypal.com:8080/us/gifts/brands/best-buy', + quantity: 1 + }; + }); + + afterEach(() => { + track.mockReset(); + window.localStorage.removeItem('paypal-cr-cart'); + window.localStorage.removeItem('paypal-cr-cart-expirty'); + }); + + afterAll(() => { + track.mockRestore(); + getPropertyId.mockRestore(); + }); + + it('user should be set when the tracker is initialized', () => { + const tracker = Tracker(config); + + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); + + const args = track.mock.calls; + + expect(args[0][0].user).toEqual(config.user); + expect(args[1][0].user).toEqual(config.user); + }); + + it('no user should be set if no configuration is passed to initialization', (done) => { + const tracker = Tracker(); + + // wait for mock propertyId to resolve + setTimeout(() => { + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); + + const args = track.mock.calls; + expect(args[0][0].user).toEqual(defaultTrackerConfig.user); + expect(args[1][0].user).toEqual(defaultTrackerConfig.user); + done(); + }, 100); + }); + + it('no user should be set if no user is passed to initialization', () => { + const tracker = Tracker({ propertyId: 'somevalue' }); + + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); + + const args = track.mock.calls; + expect(args[0][0].user).toEqual(defaultTrackerConfig.user); + expect(args[1][0].user).toEqual(defaultTrackerConfig.user); + }); + + it('user should be set when set user is called', () => { + const tracker = Tracker({ propertyId: 'somevalue' }); + + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); + + tracker.setUser(config.user); + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); + + const args = track.mock.calls; + expect(args[0][0].user).toEqual(defaultTrackerConfig.user); + expect(args[1][0].user).toEqual(defaultTrackerConfig.user); + expect(args[2][0].user).toEqual(config.user); + expect(args[3][0].user).toEqual(config.user); + expect(args[4][0].user).toEqual(config.user); + }); + + it('already-existing user should be updated when set user is called', () => { + const alternateUser = { + id: 'wut', + name: 'Steve Jobs', + email: 'steve@apple.com' + }; + + const tracker = Tracker(config); + + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); + + tracker.setUser(alternateUser); + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); + + const args = track.mock.calls; + expect(args[0][0].user).toEqual(config.user); + expect(args[1][0].user).toEqual(config.user); + expect(args[2][0].user).toEqual(alternateUser); + expect(args[3][0].user).toEqual(alternateUser); + expect(args[4][0].user).toEqual(alternateUser); + }); + + + it('setUser accepts different types of input', () => { + const alternateUser = { + id: 'wut', + name: 'Steve Jobs', + email: 'steve@apple.com' + }; + + const tracker = Tracker({ propertyId: 'somevalue' }); + + tracker.setUser(alternateUser); + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); + + tracker.setUser({ user: config.user }); + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); + const args = track.mock.calls; + + expect(args[0][0].user).toEqual(alternateUser); + expect(args[1][0].user).toEqual(alternateUser); + expect(args[2][0].user).toEqual(alternateUser); + expect(args[3][0].user).toEqual(config.user); + expect(args[4][0].user).toEqual(config.user); + expect(args[5][0].user).toEqual(config.user); + }); + + it('user can be unset by passing null', () => { + const alternateUser = { + id: 'wut', + name: 'Steve Jobs', + email: 'steve@apple.com' + }; + const tracker = Tracker(config); + + tracker.setUser({ + id: null, + email: null, + name: null + }); + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); + + tracker.setUser(alternateUser); + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); + const args = track.mock.calls; + + expect(args[0][0].user).toEqual(defaultTrackerConfig.user); + expect(args[1][0].user).toEqual(defaultTrackerConfig.user); + expect(args[2][0].user).toEqual(defaultTrackerConfig.user); + + expect(args[3][0].user).toEqual(alternateUser); + expect(args[4][0].user).toEqual(alternateUser); + expect(args[5][0].user).toEqual(alternateUser); + }); +});