From 29e2d5187b4f4efef0ab66db76733159276beed1 Mon Sep 17 00:00:00 2001 From: M-OC Date: Wed, 28 Aug 2019 10:40:18 -0700 Subject: [PATCH 1/6] Extract flow types to separate file --- src/declarations.js | 2 +- src/lib/types.js | 99 ++++++++++++++++++++++++++++++++++++++++ src/tracker-component.js | 98 ++++++--------------------------------- src/types.js | 12 ----- 4 files changed, 113 insertions(+), 98 deletions(-) create mode 100644 src/lib/types.js delete mode 100644 src/types.js 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/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..fd6c9150 100644 --- a/src/tracker-component.js +++ b/src/tracker-component.js @@ -9,91 +9,19 @@ import { getCookie, setCookie } from './lib/cookie-utils'; 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 -|}; +import type { + TrackingType, + CartEventType, + ViewData, + CartData, + CancelCartData, + RemoveCartData, + PurchaseData, + UserData, + IdentityData, + JetloreConfig, + Config +} from './lib/types'; const storage = { paypalCrCart: 'paypal-cr-cart', 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 -|}; - From cfc93a594b102ced693926b6241751a572570082 Mon Sep 17 00:00:00 2001 From: M-OC Date: Wed, 28 Aug 2019 11:51:02 -0700 Subject: [PATCH 2/6] create separate constants file --- .eslintrc.js | 4 +-- src/lib/compose-cart.js | 38 +++++++++++++++++++++++++ src/lib/constants.json | 8 ++++++ src/tracker-component.js | 51 ++++------------------------------ test/paypal.js | 4 +-- test/tracker-component.test.js | 4 +-- 6 files changed, 57 insertions(+), 52 deletions(-) create mode 100644 src/lib/constants.json diff --git a/.eslintrc.js b/.eslintrc.js index ca4447fd..3bc74f0f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -25,10 +25,10 @@ module.exports = { }, 'settings': { - 'import/extensions': [ '.js', '.jsx' ], + 'import/extensions': [ '.js', '.jsx', '.json' ], 'import/resolver': { 'node': { - 'extensions': [ '.js', '.jsx' ] + 'extensions': [ '.js', '.jsx', '.json' ] } }, 'react': { diff --git a/src/lib/compose-cart.js b/src/lib/compose-cart.js index d2ccbb5e..89fbde9b 100644 --- a/src/lib/compose-cart.js +++ b/src/lib/compose-cart.js @@ -1,4 +1,6 @@ /* @flow */ +import { getCookie } from './cookie-utils'; +import { storage, sevenDays } from './constants'; // $FlowFixMe export const removeFromCart = (items, currentItems = []) => { return items.reduce((accumulator, item) => { @@ -26,3 +28,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.json b/src/lib/constants.json new file mode 100644 index 00000000..aafd5a21 --- /dev/null +++ b/src/lib/constants.json @@ -0,0 +1,8 @@ +{ + "sevenDays": 6.048e+8, + "accessTokenUrl": "https://www.paypal.com/muse/api/partner-token", + "storage": { + "paypalCrCart": "paypal-cr-cart", + "paypalCrCartExpiry": "paypal-cr-cart-expiry" + } +} \ No newline at end of file diff --git a/src/tracker-component.js b/src/tracker-component.js index fd6c9150..d7288300 100644 --- a/src/tracker-component.js +++ b/src/tracker-component.js @@ -8,7 +8,11 @@ import generate from './generate-id'; import { getCookie, setCookie } from './lib/cookie-utils'; import getJetlore from './lib/jetlore'; import { getDeviceInfo } from './lib/get-device-info'; -import { removeFromCart, addToCart } from './lib/compose-cart'; +import { composeCart } from './lib/compose-cart'; +import { + accessTokenUrl, + storage +} from './lib/constants'; import type { TrackingType, CartEventType, @@ -23,15 +27,6 @@ import type { Config } from './lib/types'; -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; }; @@ -41,42 +36,6 @@ const setRandomUserIdCookie = () : void => { 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; -}; - const getAccessToken = (url : string, mrid : string) : Promise => { return fetch(url, { method: 'POST', 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..33af2bee 100644 --- a/test/tracker-component.test.js +++ b/test/tracker-component.test.js @@ -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,7 +758,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 }, propertyId: 'hello' }); expect(fetchCalls.length).toBe(0); From ea35142a501eb745625b12b5b5d691fef2a3d23a Mon Sep 17 00:00:00 2001 From: M-OC Date: Wed, 28 Aug 2019 15:45:51 -0700 Subject: [PATCH 3/6] pull track into separate file --- .eslintrc.js | 4 +- src/lib/{constants.json => constants.js} | 2 +- src/lib/cookie-utils.js | 10 ++++ src/{ => lib}/generate-id.js | 0 src/lib/track.js | 51 +++++++++++++++++++ src/tracker-component.js | 64 +++--------------------- test/tracker-component.test.js | 2 +- 7 files changed, 71 insertions(+), 62 deletions(-) rename src/lib/{constants.json => constants.js} (91%) rename src/{ => lib}/generate-id.js (100%) create mode 100644 src/lib/track.js diff --git a/.eslintrc.js b/.eslintrc.js index 3bc74f0f..ca4447fd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -25,10 +25,10 @@ module.exports = { }, 'settings': { - 'import/extensions': [ '.js', '.jsx', '.json' ], + 'import/extensions': [ '.js', '.jsx' ], 'import/resolver': { 'node': { - 'extensions': [ '.js', '.jsx', '.json' ] + 'extensions': [ '.js', '.jsx' ] } }, 'react': { diff --git a/src/lib/constants.json b/src/lib/constants.js similarity index 91% rename from src/lib/constants.json rename to src/lib/constants.js index aafd5a21..ab034f19 100644 --- a/src/lib/constants.json +++ b/src/lib/constants.js @@ -1,4 +1,4 @@ -{ +module.exports = { "sevenDays": 6.048e+8, "accessTokenUrl": "https://www.paypal.com/muse/api/partner-token", "storage": { 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 100% rename from src/generate-id.js rename to src/lib/generate-id.js diff --git a/src/lib/track.js b/src/lib/track.js new file mode 100644 index 00000000..c89ecc3e --- /dev/null +++ b/src/lib/track.js @@ -0,0 +1,51 @@ +/* @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'; + +// eslint-disable-next-line flowtype/no-weak-types +export const track = (config : Config, trackingType : TrackingType, trackingData : T, trackEventQueue? : any = []) => { + 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); + } +}; diff --git a/src/tracker-component.js b/src/tracker-component.js index d7288300..c675c2b8 100644 --- a/src/tracker-component.js +++ b/src/tracker-component.js @@ -4,17 +4,15 @@ 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 getJetlore from './lib/jetlore'; -import { getDeviceInfo } from './lib/get-device-info'; import { composeCart } from './lib/compose-cart'; +import { track } from './lib/track'; import { accessTokenUrl, storage } from './lib/constants'; import type { - TrackingType, CartEventType, ViewData, CartData, @@ -27,15 +25,6 @@ import type { Config } from './lib/types'; -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 getAccessToken = (url : string, mrid : string) : Promise => { return fetch(url, { method: 'POST', @@ -101,52 +90,11 @@ const getJetlorePayload = (type : string, options : Object) : Object => { let trackEventQueue = []; -const track = (config : Config, trackingType : TrackingType, trackingData : T) => { - 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, trackEventQueue); }); }; @@ -289,7 +237,7 @@ export const Tracker = (config? : Config = defaultTrackerConfig) => { trackCartEvent(config, 'removeFromCart', data); }, - purchase: (data : PurchaseData) => track(config, 'purchase', data), + purchase: (data : PurchaseData) => track(config, 'purchase', data, trackEventQueue), setUser: (data : UserData) => { config = { ...config, @@ -299,11 +247,11 @@ export const Tracker = (config? : Config = defaultTrackerConfig) => { name: data.user.name || ((config && config.user) || {}).name } }; - track(config, 'setUser', { oldUserId: getUserIdCookie() }); + track(config, 'setUser', { oldUserId: getUserIdCookie() }, trackEventQueue); }, cancelCart: (data : CancelCartData) => { clearCancelledCart(); - track(config, 'cancelCart', data); + track(config, 'cancelCart', data, trackEventQueue); }, setPropertyId: (id : string) => { config.propertyId = id; diff --git a/test/tracker-component.test.js b/test/tracker-component.test.js index 33af2bee..4c760d69 100644 --- a/test/tracker-component.test.js +++ b/test/tracker-component.test.js @@ -3,7 +3,7 @@ import { Tracker, clearTrackQueue } 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))); From 4cc58540ed15cd1f1b0ea6a83085a535d7972acf Mon Sep 17 00:00:00 2001 From: M-OC Date: Wed, 28 Aug 2019 16:38:08 -0700 Subject: [PATCH 4/6] check queue from outside track event --- .eslintrc.js | 2 +- src/lib/compose-cart.js | 7 ++++- src/lib/constants.js | 17 ++++++------ src/lib/generate-id.js | 1 - src/lib/track.js | 7 +---- src/tracker-component.js | 49 +++++++++++++++++++++------------- test/tracker-component.test.js | 34 +---------------------- test/tracker/set-user.test.js | 44 ++++++++++++++++++++++++++++++ 8 files changed, 93 insertions(+), 68 deletions(-) create mode 100644 test/tracker/set-user.test.js diff --git a/.eslintrc.js b/.eslintrc.js index ca4447fd..fe5c825f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -420,7 +420,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/src/lib/compose-cart.js b/src/lib/compose-cart.js index 89fbde9b..cd803408 100644 --- a/src/lib/compose-cart.js +++ b/src/lib/compose-cart.js @@ -1,6 +1,11 @@ /* @flow */ import { getCookie } from './cookie-utils'; -import { storage, sevenDays } from './constants'; +import constants from './constants'; + +const { + storage, + sevenDays +} = constants; // $FlowFixMe export const removeFromCart = (items, currentItems = []) => { return items.reduce((accumulator, item) => { diff --git a/src/lib/constants.js b/src/lib/constants.js index ab034f19..862454d6 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -1,8 +1,9 @@ -module.exports = { - "sevenDays": 6.048e+8, - "accessTokenUrl": "https://www.paypal.com/muse/api/partner-token", - "storage": { - "paypalCrCart": "paypal-cr-cart", - "paypalCrCartExpiry": "paypal-cr-cart-expiry" - } -} \ No newline at end of file +/* @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' + } +}; diff --git a/src/lib/generate-id.js b/src/lib/generate-id.js index 6359016b..b7fe3c09 100644 --- a/src/lib/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/track.js b/src/lib/track.js index c89ecc3e..59d779d1 100644 --- a/src/lib/track.js +++ b/src/lib/track.js @@ -8,12 +8,7 @@ import type { TrackingType } from './types'; -// eslint-disable-next-line flowtype/no-weak-types -export const track = (config : Config, trackingType : TrackingType, trackingData : T, trackEventQueue? : any = []) => { - if (!config.propertyId) { - trackEventQueue.push([ trackingType, trackingData ]); - return; - } +export const track = (config : Config, trackingType : TrackingType, trackingData : T) => { const encodeData = data => encodeURIComponent(btoa(JSON.stringify(data))); const img = document.createElement('img'); diff --git a/src/tracker-component.js b/src/tracker-component.js index c675c2b8..012ae60d 100644 --- a/src/tracker-component.js +++ b/src/tracker-component.js @@ -8,12 +8,10 @@ import { getUserIdCookie } from './lib/cookie-utils'; import getJetlore from './lib/jetlore'; import { composeCart } from './lib/compose-cart'; import { track } from './lib/track'; -import { - accessTokenUrl, - storage -} from './lib/constants'; +import constants from './lib/constants'; import type { CartEventType, + TrackingType, ViewData, CartData, CancelCartData, @@ -25,6 +23,11 @@ import type { Config } from './lib/types'; +const { + accessTokenUrl, + storage +} = constants; + const getAccessToken = (url : string, mrid : string) : Promise => { return fetch(url, { method: 'POST', @@ -90,18 +93,28 @@ const getJetlorePayload = (type : string, options : Object) : Object => { let trackEventQueue = []; -// 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, trackEventQueue); +const defaultTrackerConfig = { user: { email: undefined, name: undefined } }; + +export const clearTrackQueue = (config : Config) => { + // $FlowFixMe + return trackEventQueue.filter(([ trackingType, trackingData ]) => { // eslint-disable-line array-callback-return + track(config, trackingType, trackingData); }); }; -const trackCartEvent = (config : Config, cartEventType : CartEventType, trackingData : T) => - track(config, 'cartEvent', { ...trackingData, cartEventType }); +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 defaultTrackerConfig = { user: { email: undefined, name: undefined } }; + track(config, trackingType, trackingData); +}; + +const trackCartEvent = (config : Config, cartEventType : CartEventType, trackingData : T) => + trackEvent(config, 'cartEvent', { ...trackingData, cartEventType }); const clearExpiredCart = () => { const expiry = window.localStorage.getItem(storage.paypalCrCartExpiry); @@ -160,7 +173,7 @@ export const setImplicitPropertyId = (config : Config) => { getPropertyId(config).then(propertyId => { config.propertyId = propertyId; if (trackEventQueue.length) { - trackEventQueue = clearTrackQueue(config, trackEventQueue); + trackEventQueue = clearTrackQueue(config); } }); }; @@ -237,7 +250,7 @@ export const Tracker = (config? : Config = defaultTrackerConfig) => { trackCartEvent(config, 'removeFromCart', data); }, - purchase: (data : PurchaseData) => track(config, 'purchase', data, trackEventQueue), + purchase: (data : PurchaseData) => track(config, 'purchase', data), setUser: (data : UserData) => { config = { ...config, @@ -247,11 +260,11 @@ export const Tracker = (config? : Config = defaultTrackerConfig) => { name: data.user.name || ((config && config.user) || {}).name } }; - track(config, 'setUser', { oldUserId: getUserIdCookie() }, trackEventQueue); + trackEvent(config, 'setUser', { oldUserId: getUserIdCookie() }); }, cancelCart: (data : CancelCartData) => { clearCancelledCart(); - track(config, 'cancelCart', data, trackEventQueue); + trackEvent(config, 'cancelCart', data); }, setPropertyId: (id : string) => { config.propertyId = id; @@ -291,7 +304,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; @@ -340,7 +353,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/test/tracker-component.test.js b/test/tracker-component.test.js index 4c760d69..7199b83c 100644 --- a/test/tracker-component.test.js +++ b/test/tracker-component.test.js @@ -1,6 +1,6 @@ /* 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/lib/generate-id'; @@ -763,36 +763,4 @@ describe('paypal.Tracker', () => { 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..2345587c --- /dev/null +++ b/test/tracker/set-user.test.js @@ -0,0 +1,44 @@ +/* global it describe beforeEach expect */ +/* @flow */ +import { Tracker } from '../../src/tracker-component'; + +describe('setUser', () => { + let config; + + beforeEach(() => { + config = { + user: { + // id: 'arglebargle123', + name: 'Bob Ross', + email: 'bossrob21@pbs.org' + } + }; + }); + + it('user should be set when the tracker is initialized', () => { + Tracker(config); + + expect(true).toEqual(true); + }); + + // it('no user should be set if no user is passed to initialization', () => { + // expect(true).toEqual(false); + // }); + + // it('user should be set when set user is called', () => { + // expect(true).toEqual(false); + // }); + + // it('already-existing user should be updated when set user is called', () => { + // expect(true).toEqual(false); + // }); + + + // it('setUser accepts different types of input', () => { + // expect(true).toEqual(false); + // }); + + // it('user can be unset by passing null', () => { + // expect(true).toEqual(false); + // }); +}); From 55a85ba52cb214c9d05a05fcafa41ca884e2b732 Mon Sep 17 00:00:00 2001 From: M-OC Date: Thu, 29 Aug 2019 12:26:58 -0700 Subject: [PATCH 5/6] setUser tests pass --- src/lib/constants.js | 7 ++ src/lib/get-property-id.js | 33 ++++++ src/lib/track.js | 6 ++ src/tracker-component.js | 54 +++------- test/tracker-component.test.js | 6 +- test/tracker/set-user.test.js | 190 +++++++++++++++++++++++++++++---- 6 files changed, 235 insertions(+), 61 deletions(-) create mode 100644 src/lib/get-property-id.js diff --git a/src/lib/constants.js b/src/lib/constants.js index 862454d6..6af09fe7 100644 --- a/src/lib/constants.js +++ b/src/lib/constants.js @@ -5,5 +5,12 @@ export default { 'storage': { 'paypalCrCart': 'paypal-cr-cart', 'paypalCrCartExpiry': 'paypal-cr-cart-expiry' + }, + 'defaultTrackerConfig': { + 'user': { + 'id': null, + 'email': null, + 'name': null + } } }; diff --git a/src/lib/get-property-id.js b/src/lib/get-property-id.js new file mode 100644 index 00000000..00d2d887 --- /dev/null +++ b/src/lib/get-property-id.js @@ -0,0 +1,33 @@ +import { getClientID, getMerchantID } from '@paypal/sdk-client/src'; + +export 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 + }); + }); +}; \ No newline at end of file diff --git a/src/lib/track.js b/src/lib/track.js index 59d779d1..9508348f 100644 --- a/src/lib/track.js +++ b/src/lib/track.js @@ -20,6 +20,12 @@ export const track = (config : Config, trackingType : TrackingType, trackingD ...config.user, id: getUserIdCookie() }; + + // remove null, undefined values + user.id || delete user.id + user.email || delete user.email + user.name || delete user.name + const deviceInfo = getDeviceInfo(); const data = { ...trackingData, diff --git a/src/tracker-component.js b/src/tracker-component.js index 012ae60d..89c01abb 100644 --- a/src/tracker-component.js +++ b/src/tracker-component.js @@ -5,6 +5,7 @@ import { getClientID, getMerchantID } from '@paypal/sdk-client/src'; // $FlowFixMe import { getUserIdCookie } from './lib/cookie-utils'; +import { getPropertyId } from './lib/get-property-id'; import getJetlore from './lib/jetlore'; import { composeCart } from './lib/compose-cart'; import { track } from './lib/track'; @@ -25,7 +26,8 @@ import type { const { accessTokenUrl, - storage + storage, + defaultTrackerConfig } = constants; const getAccessToken = (url : string, mrid : string) : Promise => { @@ -93,8 +95,6 @@ const getJetlorePayload = (type : string, options : Object) : Object => { let trackEventQueue = []; -const defaultTrackerConfig = { user: { email: undefined, name: undefined } }; - export const clearTrackQueue = (config : Config) => { // $FlowFixMe return trackEventQueue.filter(([ trackingType, trackingData ]) => { // eslint-disable-line array-callback-return @@ -129,38 +129,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 @@ -182,7 +150,8 @@ const clearCancelledCart = () => { window.localStorage.removeItem(storage.paypalCrCart); }; -export const Tracker = (config? : Config = defaultTrackerConfig) => { +export const Tracker = (config? : Config = {}) => { + config = { ...defaultTrackerConfig, ...config } /* * Use the get param ?ppDebug=true to see logs * @@ -252,12 +221,19 @@ 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 : config.user.id + const userEmail = user.email !== undefined ? user.email : config.user.email + const userName = user.name !== undefined ? user.name : config.user.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 } }; trackEvent(config, 'setUser', { oldUserId: getUserIdCookie() }); diff --git a/test/tracker-component.test.js b/test/tracker-component.test.js index 7199b83c..c0cbf31e 100644 --- a/test/tracker-component.test.js +++ b/test/tracker-component.test.js @@ -462,9 +462,9 @@ describe('paypal.Tracker', () => { JSON.stringify({ oldUserId: 'abc123', user: { + id: 'abc123', email: '__test__email9', name: '__test__userName9', - id: 'abc123' }, 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' }, 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' }, propertyId, trackingType: 'cartEvent', diff --git a/test/tracker/set-user.test.js b/test/tracker/set-user.test.js index 2345587c..5bca585d 100644 --- a/test/tracker/set-user.test.js +++ b/test/tracker/set-user.test.js @@ -1,44 +1,196 @@ -/* global it describe beforeEach expect */ +/* global it describe beforeEach expect jest */ /* @flow */ 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 { + getPropertyId: async () => 'mockpropertyidofsomekind' + } +}) describe('setUser', () => { + const { defaultTrackerConfig } = constants + let config; + let mockItem; beforeEach(() => { config = { + propertyId: 'foobar', user: { - // id: 'arglebargle123', + 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', () => { - Tracker(config); + const tracker = Tracker(config); + let args + + tracker.addToCart({items: [mockItem]}) + tracker.removeFromCart({items: [mockItem]}) + + args = track.mock.calls - expect(true).toEqual(true); + expect(args[0][0].user).toEqual(config.user); + expect(args[1][0].user).toEqual(config.user); }); - // it('no user should be set if no user is passed to initialization', () => { - // expect(true).toEqual(false); - // }); + it('no user should be set if no configuration is passed to initialization', (done) => { + const tracker = Tracker(); + let args - // it('user should be set when set user is called', () => { - // expect(true).toEqual(false); - // }); + // wait for mock propertyId to resolve + setTimeout(() => { + tracker.addToCart({items: [mockItem]}) + tracker.removeFromCart({items: [mockItem]}) - // it('already-existing user should be updated when set user is called', () => { - // expect(true).toEqual(false); - // }); + 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'}); + let args + + tracker.addToCart({items: [mockItem]}) + tracker.removeFromCart({items: [mockItem]}) + + 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'}); + let args + tracker.addToCart({items: [mockItem]}) + tracker.removeFromCart({items: [mockItem]}) - // it('setUser accepts different types of input', () => { - // expect(true).toEqual(false); - // }); + tracker.setUser(config.user) + tracker.addToCart({items: [mockItem]}) + tracker.removeFromCart({items: [mockItem]}) + + 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('user can be unset by passing null', () => { - // expect(true).toEqual(false); - // }); + 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); + let args + + tracker.addToCart({items: [mockItem]}) + tracker.removeFromCart({items: [mockItem]}) + + tracker.setUser(alternateUser) + tracker.addToCart({items: [mockItem]}) + tracker.removeFromCart({items: [mockItem]}) + + 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'}); + let args + + tracker.setUser(alternateUser) + tracker.addToCart({items: [mockItem]}) + tracker.removeFromCart({items: [mockItem]}) + + tracker.setUser({user: config.user}) + tracker.addToCart({items: [mockItem]}) + tracker.removeFromCart({items: [mockItem]}) + 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); + let args + + 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]}) + 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); + }); }); From e626b22d1c4643134c662ff83512192a2a5fe29d Mon Sep 17 00:00:00 2001 From: M-OC Date: Thu, 29 Aug 2019 14:14:11 -0700 Subject: [PATCH 6/6] resolve comments. fix flow issues --- .eslintrc.js | 1 - .flowconfig | 1 + src/lib/get-property-id.js | 69 +++++++++++--------- src/lib/track.js | 7 +- src/tracker-component.js | 16 +++-- test/tracker-component.test.js | 8 +-- test/tracker/set-user.test.js | 113 ++++++++++++++++----------------- 7 files changed, 107 insertions(+), 108 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index fe5c825f..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', 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/lib/get-property-id.js b/src/lib/get-property-id.js index 00d2d887..8811c5c0 100644 --- a/src/lib/get-property-id.js +++ b/src/lib/get-property-id.js @@ -1,33 +1,40 @@ +/* @flow */ import { getClientID, getMerchantID } from '@paypal/sdk-client/src'; -export 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 - }); - }); -}; \ No newline at end of file +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 index 9508348f..f6cb412b 100644 --- a/src/lib/track.js +++ b/src/lib/track.js @@ -21,11 +21,6 @@ export const track = (config : Config, trackingType : TrackingType, trackingD id: getUserIdCookie() }; - // remove null, undefined values - user.id || delete user.id - user.email || delete user.email - user.name || delete user.name - const deviceInfo = getDeviceInfo(); const data = { ...trackingData, @@ -46,6 +41,8 @@ export const track = (config : Config, trackingType : TrackingType, trackingD 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/tracker-component.js b/src/tracker-component.js index 89c01abb..901e9131 100644 --- a/src/tracker-component.js +++ b/src/tracker-component.js @@ -96,6 +96,7 @@ const getJetlorePayload = (type : string, options : Object) : Object => { let trackEventQueue = []; 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); @@ -149,9 +150,10 @@ const clearCancelledCart = () => { window.localStorage.removeItem(storage.paypalCrCartExpiry); window.localStorage.removeItem(storage.paypalCrCart); }; - +// $FlowFixMe export const Tracker = (config? : Config = {}) => { - config = { ...defaultTrackerConfig, ...config } + // $FlowFixMe + config = { ...defaultTrackerConfig, ...config }; /* * Use the get param ?ppDebug=true to see logs * @@ -221,12 +223,12 @@ export const Tracker = (config? : Config = {}) => { }, purchase: (data : PurchaseData) => track(config, 'purchase', data), setUser: (data : UserData) => { - const user = data.user || data - const configUser = config.user || {} + const user = data.user || data; + const configUser = config.user || {}; - const userId = user.id !== undefined ? user.id : config.user.id - const userEmail = user.email !== undefined ? user.email : config.user.email - const userName = user.name !== undefined ? user.name : config.user.name + 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, diff --git a/test/tracker-component.test.js b/test/tracker-component.test.js index c0cbf31e..90837a9d 100644 --- a/test/tracker-component.test.js +++ b/test/tracker-component.test.js @@ -464,7 +464,7 @@ describe('paypal.Tracker', () => { user: { id: 'abc123', email: '__test__email9', - name: '__test__userName9', + name: '__test__userName9' }, propertyId, trackingType: 'setUser', @@ -498,7 +498,7 @@ describe('paypal.Tracker', () => { user: { id: 'abc123', email: '__test__email@gmail.com', - name: '__test__name', + name: '__test__name' }, propertyId, trackingType: 'setUser', @@ -546,7 +546,7 @@ describe('paypal.Tracker', () => { user: { id: 'abc123', email: '__test__email2', - name: '__test__name1', + 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', diff --git a/test/tracker/set-user.test.js b/test/tracker/set-user.test.js index 5bca585d..a2826895 100644 --- a/test/tracker/set-user.test.js +++ b/test/tracker/set-user.test.js @@ -1,20 +1,20 @@ -/* global it describe beforeEach expect jest */ /* @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/track'); jest.mock('../../src/lib/get-property-id', () => { return { + // eslint-disable-next-line require-await getPropertyId: async () => 'mockpropertyidofsomekind' - } -}) + }; +}); describe('setUser', () => { - const { defaultTrackerConfig } = constants + const { defaultTrackerConfig } = constants; let config; let mockItem; @@ -35,7 +35,7 @@ describe('setUser', () => { price: '100.00', title: 'Best Buy', url: 'http://localhost.paypal.com:8080/us/gifts/brands/best-buy', - quantity: 1, + quantity: 1 }; }); @@ -43,21 +43,20 @@ describe('setUser', () => { 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); - let args - tracker.addToCart({items: [mockItem]}) - tracker.removeFromCart({items: [mockItem]}) + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); - args = track.mock.calls + const args = track.mock.calls; expect(args[0][0].user).toEqual(config.user); expect(args[1][0].user).toEqual(config.user); @@ -65,44 +64,41 @@ describe('setUser', () => { it('no user should be set if no configuration is passed to initialization', (done) => { const tracker = Tracker(); - let args // wait for mock propertyId to resolve setTimeout(() => { - tracker.addToCart({items: [mockItem]}) - tracker.removeFromCart({items: [mockItem]}) + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); - args = track.mock.calls + const args = track.mock.calls; expect(args[0][0].user).toEqual(defaultTrackerConfig.user); expect(args[1][0].user).toEqual(defaultTrackerConfig.user); - done() - }, 100) + done(); + }, 100); }); it('no user should be set if no user is passed to initialization', () => { - const tracker = Tracker({propertyId: 'somevalue'}); - let args + const tracker = Tracker({ propertyId: 'somevalue' }); - tracker.addToCart({items: [mockItem]}) - tracker.removeFromCart({items: [mockItem]}) + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); - args = track.mock.calls + 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'}); - let args + const tracker = Tracker({ propertyId: 'somevalue' }); - tracker.addToCart({items: [mockItem]}) - tracker.removeFromCart({items: [mockItem]}) + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); - tracker.setUser(config.user) - tracker.addToCart({items: [mockItem]}) - tracker.removeFromCart({items: [mockItem]}) + tracker.setUser(config.user); + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); - args = track.mock.calls + 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); @@ -115,19 +111,18 @@ describe('setUser', () => { id: 'wut', name: 'Steve Jobs', email: 'steve@apple.com' - } + }; const tracker = Tracker(config); - let args - tracker.addToCart({items: [mockItem]}) - tracker.removeFromCart({items: [mockItem]}) + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); - tracker.setUser(alternateUser) - tracker.addToCart({items: [mockItem]}) - tracker.removeFromCart({items: [mockItem]}) + tracker.setUser(alternateUser); + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); - args = track.mock.calls + 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); @@ -141,19 +136,18 @@ describe('setUser', () => { id: 'wut', name: 'Steve Jobs', email: 'steve@apple.com' - } + }; - const tracker = Tracker({propertyId: 'somevalue'}); - let args + const tracker = Tracker({ propertyId: 'somevalue' }); - tracker.setUser(alternateUser) - tracker.addToCart({items: [mockItem]}) - tracker.removeFromCart({items: [mockItem]}) + tracker.setUser(alternateUser); + tracker.addToCart({ items: [ mockItem ] }); + tracker.removeFromCart({ items: [ mockItem ] }); - tracker.setUser({user: config.user}) - tracker.addToCart({items: [mockItem]}) - tracker.removeFromCart({items: [mockItem]}) - args = track.mock.calls + 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); @@ -168,22 +162,21 @@ describe('setUser', () => { id: 'wut', name: 'Steve Jobs', email: 'steve@apple.com' - } + }; const tracker = Tracker(config); - let args 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]}) - args = track.mock.calls + }); + 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);