diff --git a/__mocks__/@react-native-firebase/analytics.ts b/__mocks__/@react-native-firebase/analytics.ts new file mode 100644 index 000000000000..1cc35724c726 --- /dev/null +++ b/__mocks__/@react-native-firebase/analytics.ts @@ -0,0 +1,5 @@ +export default function analytics() { + return { + logEvent: jest.fn(), + }; +} diff --git a/config/webpack/webpack.common.ts b/config/webpack/webpack.common.ts index ab5c304fcd1e..8aa8f5aa566c 100644 --- a/config/webpack/webpack.common.ts +++ b/config/webpack/webpack.common.ts @@ -84,7 +84,7 @@ const getCommonConfiguration = ({file = '.env', platform = 'web'}: Environment): isWeb: platform === 'web', isProduction: file === '.env.production', isStaging: file === '.env.staging', - useThirdPartyScripts: process.env.USE_THIRD_PARTY_SCRIPTS === 'true' || (platform === 'web' && file === '.env.production'), + useThirdPartyScripts: process.env.USE_THIRD_PARTY_SCRIPTS === 'true' || (platform === 'web' && ['.env.production', '.env.staging'].includes(file)), }), new PreloadWebpackPlugin({ rel: 'preload', diff --git a/src/CONST.ts b/src/CONST.ts index e95e3d4a5603..9ac351de648c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6219,6 +6219,14 @@ const CONST = { HAS_VIOLATIONS: 'hasViolations', HAS_TRANSACTION_THREAD_VIOLATIONS: 'hasTransactionThreadViolations', }, + + ANALYTICS: { + EVENT: { + SIGN_UP: 'sign_up', + WORKSPACE_CREATED: 'workspace_created', + PAID_ADOPTION: 'paid_adoption', + }, + }, } as const; type Country = keyof typeof CONST.ALL_COUNTRIES; diff --git a/src/libs/GoogleTagManager/types.ts b/src/libs/GoogleTagManager/types.ts index a4af6aa00330..42e40099e93a 100644 --- a/src/libs/GoogleTagManager/types.ts +++ b/src/libs/GoogleTagManager/types.ts @@ -1,8 +1,11 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + /** * An event that can be published to Google Tag Manager. New events must be configured in GTM before they can be used * in the app. */ -type GoogleTagManagerEvent = 'sign_up' | 'workspace_created' | 'paid_adoption'; +type GoogleTagManagerEvent = ValueOf; type GoogleTagManagerModule = { publishEvent: (event: GoogleTagManagerEvent, accountID: number) => void; diff --git a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx index 4aae43987797..401e6b6ec809 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator.tsx @@ -1,11 +1,13 @@ import {createStackNavigator} from '@react-navigation/stack'; -import React, {useCallback} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; import NoDropZone from '@components/DragAndDrop/NoDropZone'; import FocusTrapForScreens from '@components/FocusTrap/FocusTrapForScreen'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; +import GoogleTagManager from '@libs/GoogleTagManager'; import OnboardingModalNavigatorScreenOptions from '@libs/Navigation/AppNavigator/OnboardingModalNavigatorScreenOptions'; import type {OnboardingModalNavigatorParamList} from '@libs/Navigation/types'; import OnboardingRefManager from '@libs/OnboardingRefManager'; @@ -14,6 +16,7 @@ import OnboardingEmployees from '@pages/OnboardingEmployees'; import OnboardingPersonalDetails from '@pages/OnboardingPersonalDetails'; import OnboardingPurpose from '@pages/OnboardingPurpose'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import SCREENS from '@src/SCREENS'; import Overlay from './Overlay'; @@ -23,6 +26,17 @@ function OnboardingModalNavigator() { const styles = useThemeStyles(); const {onboardingIsMediumOrLargerScreenWidth} = useResponsiveLayout(); const outerViewRef = React.useRef(null); + const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID ?? 0}); + + // Publish a sign_up event when we start the onboarding flow. This should track basic sign ups + // as well as Google and Apple SSO. + useEffect(() => { + if (!accountID) { + return; + } + + GoogleTagManager.publishEvent(CONST.ANALYTICS.EVENT.SIGN_UP, accountID); + }, [accountID]); const handleOuterClick = useCallback(() => { OnboardingRefManager.handleOuterClick(); diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 1db2555f3393..fc37c2f781df 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -36,6 +36,7 @@ import DateUtils from '@libs/DateUtils'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import * as FileUtils from '@libs/fileDownload/FileUtils'; +import GoogleTagManager from '@libs/GoogleTagManager'; import * as IOUUtils from '@libs/IOUUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import * as Localize from '@libs/Localize'; @@ -3422,6 +3423,7 @@ function categorizeTrackedExpense( comment: string, merchant: string, created: string, + isDraftPolicy: boolean, category?: string, tag?: string, taxCode = '', @@ -3478,6 +3480,12 @@ function categorizeTrackedExpense( }; API.write(WRITE_COMMANDS.CATEGORIZE_TRACKED_EXPENSE, parameters, {optimisticData, successData, failureData}); + + // If a draft policy was used, then the CategorizeTrackedExpense command will create a real one + // so let's track that conversion here + if (isDraftPolicy) { + GoogleTagManager.publishEvent(CONST.ANALYTICS.EVENT.WORKSPACE_CREATED, userAccountID); + } } function shareTrackedExpense( @@ -3777,6 +3785,7 @@ function trackExpense( payeeAccountID: number, participant: Participant, comment: string, + isDraftPolicy: boolean, receipt?: Receipt, category?: string, tag?: string, @@ -3872,6 +3881,7 @@ function trackExpense( comment, merchant, created, + isDraftPolicy, category, tag, taxCode, diff --git a/src/libs/actions/PaymentMethods.ts b/src/libs/actions/PaymentMethods.ts index 893540e1dfac..e645267b32fd 100644 --- a/src/libs/actions/PaymentMethods.ts +++ b/src/libs/actions/PaymentMethods.ts @@ -16,6 +16,7 @@ import type { } from '@libs/API/parameters'; import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as CardUtils from '@libs/CardUtils'; +import GoogleTagManager from '@libs/GoogleTagManager'; import Navigation from '@libs/Navigation/Navigation'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -159,7 +160,7 @@ function makeDefaultPaymentMethod(bankAccountID: number, fundID: number, previou * Calls the API to add a new card. * */ -function addPaymentCard(params: PaymentCardParams) { +function addPaymentCard(accountID: number, params: PaymentCardParams) { const cardMonth = CardUtils.getMonthFromExpirationDateString(params.expirationDate); const cardYear = CardUtils.getYearFromExpirationDateString(params.expirationDate); @@ -203,21 +204,26 @@ function addPaymentCard(params: PaymentCardParams) { successData, failureData, }); + + GoogleTagManager.publishEvent(CONST.ANALYTICS.EVENT.PAID_ADOPTION, accountID); } /** * Calls the API to add a new card. * */ -function addSubscriptionPaymentCard(cardData: { - cardNumber: string; - cardYear: string; - cardMonth: string; - cardCVV: string; - addressName: string; - addressZip: string; - currency: ValueOf; -}) { +function addSubscriptionPaymentCard( + accountID: number, + cardData: { + cardNumber: string; + cardYear: string; + cardMonth: string; + cardCVV: string; + addressName: string; + addressZip: string; + currency: ValueOf; + }, +) { const {cardNumber, cardYear, cardMonth, cardCVV, addressName, addressZip, currency} = cardData; const parameters: AddPaymentCardParams = { @@ -265,6 +271,8 @@ function addSubscriptionPaymentCard(cardData: { failureData, }); } + + GoogleTagManager.publishEvent(CONST.ANALYTICS.EVENT.PAID_ADOPTION, accountID); } /** diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index d87f0321bab0..963770d65ccd 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -62,6 +62,7 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import DateUtils from '@libs/DateUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; +import GoogleTagManager from '@libs/GoogleTagManager'; import Log from '@libs/Log'; import * as NetworkStore from '@libs/Network/NetworkStore'; import * as NumberUtils from '@libs/NumberUtils'; @@ -1838,6 +1839,11 @@ function createWorkspace(policyOwnerEmail = '', makeMeAdmin = false, policyName const {optimisticData, failureData, successData, params} = buildPolicyData(policyOwnerEmail, makeMeAdmin, policyName, policyID, undefined, engagementChoice); API.write(WRITE_COMMANDS.CREATE_WORKSPACE, params, {optimisticData, successData, failureData}); + // Publish a workspace created event if this is their first policy + if (getAdminPolicies().length === 0) { + GoogleTagManager.publishEvent(CONST.ANALYTICS.EVENT.WORKSPACE_CREATED, sessionAccountID); + } + return params; } @@ -3743,6 +3749,10 @@ function setWorkspaceEReceiptsEnabled(policyID: string, eReceipts: boolean) { API.write(WRITE_COMMANDS.SET_WORKSPACE_ERECEIPTS_ENABLED, parameters, onyxData); } +function getAdminPolicies(): Policy[] { + return Object.values(allPolicies ?? {}).filter((policy): policy is Policy => !!policy && policy.role === CONST.POLICY.ROLE.ADMIN && policy.type !== CONST.POLICY.TYPE.PERSONAL); +} + function getAdminPoliciesConnectedToSageIntacct(): Policy[] { return Object.values(allPolicies ?? {}).filter((policy): policy is Policy => !!policy && policy.role === CONST.POLICY.ROLE.ADMIN && !!policy?.connections?.intacct); } diff --git a/src/pages/iou/request/step/IOURequestStepAmount.tsx b/src/pages/iou/request/step/IOURequestStepAmount.tsx index 9c6f39ea8c5a..70a9c545dc8e 100644 --- a/src/pages/iou/request/step/IOURequestStepAmount.tsx +++ b/src/pages/iou/request/step/IOURequestStepAmount.tsx @@ -45,24 +45,25 @@ type IOURequestStepAmountProps = WithCurrentUserPersonalDetailsProps & function IOURequestStepAmount({ report, route: { - params: {iouType, reportID, transactionID, backTo, pageIndex, action, currency: selectedCurrency = ''}, + params: {iouType, reportID, transactionID = '-1', backTo, pageIndex, action, currency: selectedCurrency = ''}, }, transaction, currentUserPersonalDetails, shouldKeepUserInput = false, }: IOURequestStepAmountProps) { - const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID ?? -1}`); - const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID ?? -1}`); - const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID ?? -1}`); - const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : -1}`); - const {translate} = useLocalize(); const textInput = useRef(null); const focusTimeoutRef = useRef(null); const isSaveButtonPressed = useRef(false); const iouRequestType = getRequestType(transaction); + const policyID = report?.policyID ?? '-1'; + const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID ?? -1}`); + const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + const [draftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`); + const [splitDraftTransaction] = useOnyx(`${ONYXKEYS.COLLECTION.SPLIT_TRANSACTION_DRAFT}${transactionID}`); + const [skipConfirmation] = useOnyx(`${ONYXKEYS.COLLECTION.SKIP_CONFIRMATION}${transactionID}`); const isEditing = action === CONST.IOU.ACTION.EDIT; const isSplitBill = iouType === CONST.IOU.TYPE.SPLIT; @@ -236,6 +237,7 @@ function IOURequestStepAmount({ currentUserPersonalDetails.accountID, participants.at(0) ?? {}, '', + false, ); return; } diff --git a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx index aa3a432a0e5a..06e4ed83d936 100644 --- a/src/pages/iou/request/step/IOURequestStepConfirmation.tsx +++ b/src/pages/iou/request/step/IOURequestStepConfirmation.tsx @@ -63,6 +63,7 @@ function IOURequestStepConfirmation({ const report = reportReal ?? reportDraft; const policy = policyReal ?? policyDraft; + const isDraftPolicy = policy === policyDraft; const policyCategories = policyCategoriesReal ?? policyCategoriesDraft; const styles = useThemeStyles(); @@ -287,6 +288,7 @@ function IOURequestStepConfirmation({ currentUserPersonalDetails.accountID, participant, trimmedComment, + isDraftPolicy, receiptObj, transaction.category, transaction.tag, @@ -317,6 +319,7 @@ function IOURequestStepConfirmation({ policyCategories, action, customUnitRateID, + isDraftPolicy, ], ); diff --git a/src/pages/iou/request/step/IOURequestStepDistance.tsx b/src/pages/iou/request/step/IOURequestStepDistance.tsx index aec3b0146138..ae3ecff9adb2 100644 --- a/src/pages/iou/request/step/IOURequestStepDistance.tsx +++ b/src/pages/iou/request/step/IOURequestStepDistance.tsx @@ -319,6 +319,7 @@ function IOURequestStepDistance({ currentUserPersonalDetails.accountID, participant, '', + false, {}, '', '', diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx index f7e575b898fd..418474e117ed 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.native.tsx @@ -251,6 +251,7 @@ function IOURequestStepScan({ currentUserPersonalDetails.accountID, participant, '', + false, receipt, ); } else { @@ -335,6 +336,7 @@ function IOURequestStepScan({ currentUserPersonalDetails.accountID, participant, '', + false, receipt, '', '', diff --git a/src/pages/iou/request/step/IOURequestStepScan/index.tsx b/src/pages/iou/request/step/IOURequestStepScan/index.tsx index ecf84c877496..375936b05c1a 100644 --- a/src/pages/iou/request/step/IOURequestStepScan/index.tsx +++ b/src/pages/iou/request/step/IOURequestStepScan/index.tsx @@ -280,6 +280,7 @@ function IOURequestStepScan({ currentUserPersonalDetails.accountID, participant, '', + false, receipt, ); } else { @@ -365,6 +366,7 @@ function IOURequestStepScan({ currentUserPersonalDetails.accountID, participant, '', + false, receipt, '', '', diff --git a/src/pages/settings/Subscription/PaymentCard/index.tsx b/src/pages/settings/Subscription/PaymentCard/index.tsx index cdeb1ae0a580..da977f170a9b 100644 --- a/src/pages/settings/Subscription/PaymentCard/index.tsx +++ b/src/pages/settings/Subscription/PaymentCard/index.tsx @@ -27,6 +27,7 @@ function AddPaymentCard() { const styles = useThemeStyles(); const {translate} = useLocalize(); const [privateSubscription] = useOnyx(ONYXKEYS.NVP_PRIVATE_SUBSCRIPTION); + const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID ?? 0}); const subscriptionPlan = useSubscriptionPlan(); const subscriptionPrice = useSubscriptionPrice(); @@ -43,18 +44,21 @@ function AddPaymentCard() { }; }, []); - const addPaymentCard = useCallback((values: FormOnyxValues) => { - const cardData = { - cardNumber: CardUtils.getMCardNumberString(values.cardNumber), - cardMonth: CardUtils.getMonthFromExpirationDateString(values.expirationDate), - cardYear: CardUtils.getYearFromExpirationDateString(values.expirationDate), - cardCVV: values.securityCode, - addressName: values.nameOnCard, - addressZip: values.addressZipCode, - currency: values.currency ?? CONST.PAYMENT_CARD_CURRENCY.USD, - }; - PaymentMethods.addSubscriptionPaymentCard(cardData); - }, []); + const addPaymentCard = useCallback( + (values: FormOnyxValues) => { + const cardData = { + cardNumber: CardUtils.getMCardNumberString(values.cardNumber), + cardMonth: CardUtils.getMonthFromExpirationDateString(values.expirationDate), + cardYear: CardUtils.getYearFromExpirationDateString(values.expirationDate), + cardCVV: values.securityCode, + addressName: values.nameOnCard, + addressZip: values.addressZipCode, + currency: values.currency ?? CONST.PAYMENT_CARD_CURRENCY.USD, + }; + PaymentMethods.addSubscriptionPaymentCard(accountID ?? 0, cardData); + }, + [accountID], + ); const [formData] = useOnyx(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM); const prevFormDataSetupComplete = usePrevious(!!formData?.setupComplete); diff --git a/src/pages/settings/Wallet/AddDebitCardPage.tsx b/src/pages/settings/Wallet/AddDebitCardPage.tsx index d785df0ea324..042459f9ac72 100644 --- a/src/pages/settings/Wallet/AddDebitCardPage.tsx +++ b/src/pages/settings/Wallet/AddDebitCardPage.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {useOnyx} from 'react-native-onyx'; import PaymentCardForm from '@components/AddPaymentCard/PaymentCardForm'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -6,6 +6,7 @@ import type {AnimatedTextInputRef} from '@components/RNTextInput'; import ScreenWrapper from '@components/ScreenWrapper'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; +import type {PaymentCardParams} from '@libs/API/parameters'; import Navigation from '@libs/Navigation/Navigation'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import * as PaymentMethods from '@userActions/PaymentMethods'; @@ -19,6 +20,7 @@ function DebitCardPage() { const [formData] = useOnyx(ONYXKEYS.FORMS.ADD_PAYMENT_CARD_FORM); const prevFormDataSetupComplete = usePrevious(!!formData?.setupComplete); const nameOnCardRef = useRef(null); + const [accountID] = useOnyx(ONYXKEYS.SESSION, {selector: (session) => session?.accountID ?? 0}); /** * Reset the form values on the mount and unmount so that old errors don't show when this form is displayed again. @@ -39,6 +41,13 @@ function DebitCardPage() { PaymentMethods.continueSetup(); }, [prevFormDataSetupComplete, formData?.setupComplete]); + const addPaymentCard = useCallback( + (params: PaymentCardParams) => { + PaymentMethods.addPaymentCard(accountID ?? 0, params); + }, + [accountID], + ); + return ( nameOnCardRef.current?.focus()} @@ -55,7 +64,7 @@ function DebitCardPage() { showAddressField isDebitCard showStateSelector - addPaymentCard={PaymentMethods.addPaymentCard} + addPaymentCard={addPaymentCard} submitButtonText={translate('common.save')} /> diff --git a/tests/unit/GoogleTagManagerTest.tsx b/tests/unit/GoogleTagManagerTest.tsx new file mode 100644 index 000000000000..dcb6bdea0eec --- /dev/null +++ b/tests/unit/GoogleTagManagerTest.tsx @@ -0,0 +1,135 @@ +import {NavigationContainer} from '@react-navigation/native'; +import {render} from '@testing-library/react-native'; +import Onyx from 'react-native-onyx'; +import * as IOU from '@libs/actions/IOU'; +import * as PaymentMethods from '@libs/actions/PaymentMethods'; +import * as Policy from '@libs/actions/Policy/Policy'; +import GoogleTagManager from '@libs/GoogleTagManager'; +import OnboardingModalNavigator from '@libs/Navigation/AppNavigator/Navigators/OnboardingModalNavigator'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +jest.mock('@libs/GoogleTagManager'); + +// Mock the Overlay since it doesn't work in tests +jest.mock('@libs/Navigation/AppNavigator/Navigators/Overlay'); + +describe('GoogleTagManagerTest', () => { + const accountID = 123456; + + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + initialKeyStates: { + session: {accountID}, + }, + }); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('sign_up', () => { + // When we render the OnboardingModal a few times + const {rerender} = render( + + + , + ); + rerender( + + + , + ); + rerender( + + + , + ); + + // Then we publish the sign_up event only once + expect(GoogleTagManager.publishEvent).toBeCalledTimes(1); + expect(GoogleTagManager.publishEvent).toBeCalledWith(CONST.ANALYTICS.EVENT.SIGN_UP, accountID); + }); + + test('workspace_created', async () => { + // When we run the createWorkspace action a few times + Policy.createWorkspace(); + await waitForBatchedUpdates(); + Policy.createWorkspace(); + await waitForBatchedUpdates(); + Policy.createWorkspace(); + + // Then we publish a workspace_created event only once + expect(GoogleTagManager.publishEvent).toBeCalledTimes(1); + expect(GoogleTagManager.publishEvent).toBeCalledWith(CONST.ANALYTICS.EVENT.WORKSPACE_CREATED, accountID); + }); + + test('workspace_created - categorizeTrackedExpense', () => { + // When we categorize a tracked expense with a draft policy + IOU.trackExpense( + {reportID: '123'}, + 1000, + 'USD', + '2024-10-30', + 'merchant', + undefined, + 0, + {accountID}, + 'comment', + true, + undefined, + 'category', + 'tag', + 'taxCode', + 0, + undefined, + undefined, + undefined, + undefined, + undefined, + undefined, + CONST.IOU.ACTION.CATEGORIZE, + 'actionableWhisperReportActionID', + {actionName: 'IOU', reportActionID: 'linkedTrackedExpenseReportAction', created: '2024-10-30'}, + 'linkedTrackedExpenseReportID', + ); + + // Then we publish a workspace_created event only once + expect(GoogleTagManager.publishEvent).toBeCalledTimes(1); + expect(GoogleTagManager.publishEvent).toBeCalledWith('workspace_created', accountID); + }); + + test('paid_adoption - addPaymentCard', () => { + // When we add a payment card + PaymentMethods.addPaymentCard(accountID, { + expirationDate: '2077-10-30', + addressZipCode: 'addressZipCode', + cardNumber: 'cardNumber', + nameOnCard: 'nameOnCard', + securityCode: 'securityCode', + }); + + // Then we publish a paid_adoption event only once + expect(GoogleTagManager.publishEvent).toBeCalledTimes(1); + expect(GoogleTagManager.publishEvent).toBeCalledWith(CONST.ANALYTICS.EVENT.PAID_ADOPTION, accountID); + }); + + test('paid_adoption - addSubscriptionPaymentCard', () => { + // When we add a payment card + PaymentMethods.addSubscriptionPaymentCard(accountID, { + cardNumber: 'cardNumber', + cardYear: 'cardYear', + cardMonth: 'cardMonth', + cardCVV: 'cardCVV', + addressName: 'addressName', + addressZip: 'addressZip', + currency: 'USD', + }); + + // Then we publish a paid_adoption event only once + expect(GoogleTagManager.publishEvent).toBeCalledTimes(1); + expect(GoogleTagManager.publishEvent).toBeCalledWith(CONST.ANALYTICS.EVENT.PAID_ADOPTION, accountID); + }); +});