Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions src/pages/home/GettingStartedSection/GettingStartedRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ function GettingStartedRow({item}: GettingStartedRowProps) {
const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'Checkmark'] as const);

const navigateToItem = () => {
if (!item.isFeatureEnabled) {
Comment thread
adamgrzybowski marked this conversation as resolved.
item.enableFeature?.();
}
Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(item.route));
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID';

import {enablePolicyCategories} from '@libs/actions/Policy/Category';
import {hasCompanyCardFeeds} from '@libs/CardUtils';
import Navigation from '@libs/Navigation/Navigation';
import {
Expand All @@ -21,8 +20,6 @@ import {

import isWithinGettingStartedPeriod from '@pages/home/GettingStartedSection/utils/isWithinGettingStartedPeriod';

import {enableCompanyCards, enableExpensifyCard, enablePolicyConnections} from '@userActions/Policy/Policy';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Route} from '@src/ROUTES';
Expand All @@ -38,8 +35,6 @@ type GettingStartedItem = {
subtitle?: string;
isComplete: boolean;
route: Route;
isFeatureEnabled?: boolean;
enableFeature?: () => void;
};

type UseGettingStartedItemsResult = {
Expand Down Expand Up @@ -112,14 +107,23 @@ function useGettingStartedItems(): UseGettingStartedItemsResult {
});

if (intent === CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE) {
items.push({
key: 'customizeCategories',
label: translate('homePage.gettingStartedSection.customizeCategories'),
isComplete: hasCustomCategories(policyCategories),
route: ROUTES.WORKSPACE_CATEGORIES.getRoute(activePolicyID),
isFeatureEnabled: policy.areCategoriesEnabled,
enableFeature: () => enablePolicyCategories({policy, categories: policyCategories ?? {}, tags: {}, reports: [], transactionsAndViolations: {}}, true, false),
});
if (policy.areCategoriesEnabled) {
items.push({
key: 'customizeCategories',
label: translate('homePage.gettingStartedSection.customizeCategories'),
isComplete: hasCustomCategories(policyCategories),
route: ROUTES.WORKSPACE_CATEGORIES.getRoute(activePolicyID),
});
}

if (policy.areCompanyCardsEnabled) {
items.push({
Comment thread
adamgrzybowski marked this conversation as resolved.
key: 'linkCompanyCards',
label: translate('homePage.gettingStartedSection.linkCompanyCards'),
isComplete: hasCompanyCardFeeds(allCardFeeds),
route: ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(activePolicyID),
});
}

const activeMemberCount = Object.values(policy.employeeList ?? {}).filter((member) => member?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length;
items.push({
Expand Down Expand Up @@ -147,17 +151,13 @@ function useGettingStartedItems(): UseGettingStartedItemsResult {
label: integrationName ? translate('homePage.gettingStartedSection.connectAccounting', {integrationName}) : translate('homePage.gettingStartedSection.connectAccountingDefault'),
isComplete: !!getValidConnectedIntegration(policy) || Object.values(policy?.connections ?? {}).some((conn) => !!conn?.lastSync?.successfulDate),
route: ROUTES.WORKSPACE_ACCOUNTING.getRoute(activePolicyID),
isFeatureEnabled: policy.areConnectionsEnabled,
enableFeature: () => enablePolicyConnections(activePolicyID, true, false),
});
} else {
} else if (policy.areCategoriesEnabled) {
items.push({
key: 'customizeCategories',
label: translate('homePage.gettingStartedSection.customizeCategories'),
isComplete: hasCustomCategories(policyCategories),
route: ROUTES.WORKSPACE_CATEGORIES.getRoute(activePolicyID),
isFeatureEnabled: policy.areCategoriesEnabled,
enableFeature: () => enablePolicyCategories({policy, categories: policyCategories ?? {}, tags: {}, reports: [], transactionsAndViolations: {}}, true, false),
});
}

Expand All @@ -168,8 +168,6 @@ function useGettingStartedItems(): UseGettingStartedItemsResult {
label: translate('homePage.gettingStartedSection.linkCompanyCards'),
isComplete: hasCompanyCardFeeds(allCardFeeds),
route: ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(activePolicyID),
isFeatureEnabled: policy.areCompanyCardsEnabled,
enableFeature: () => enableCompanyCards(activePolicyID, true, false),
});
}

Expand All @@ -180,8 +178,6 @@ function useGettingStartedItems(): UseGettingStartedItemsResult {
subtitle: translate('homePage.gettingStartedSection.issueExpensifyCardsSubtitle'),
isComplete: hasIssuedExpensifyCard,
route: ROUTES.WORKSPACE_EXPENSIFY_CARD.getRoute(activePolicyID),
isFeatureEnabled: policy.areExpensifyCardsEnabled,
enableFeature: () => enableExpensifyCard(activePolicyID, true, false),
});
}

Expand Down
142 changes: 89 additions & 53 deletions tests/unit/hooks/useGettingStartedItems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ const useResponsiveLayoutMock = jest.requireMock<jest.Mock>('@hooks/useResponsiv
jest.mock('@userActions/Policy/Category', () => ({enablePolicyCategories: jest.fn()}));
jest.mock('@userActions/Policy/Policy', () => ({enableCompanyCards: jest.fn(), enableExpensifyCard: jest.fn(), enablePolicyConnections: jest.fn(), enablePolicyRules: jest.fn()}));

const {enableCompanyCards, enableExpensifyCard} = jest.requireMock<{enableCompanyCards: jest.Mock; enableExpensifyCard: jest.Mock}>('@userActions/Policy/Policy');

const POLICY_ID = '1';

// Trial dates relative to today so the 60-day Getting Started window check
Expand All @@ -59,7 +57,8 @@ function buildPolicy(overrides: Partial<Policy> = {}): Policy {
}

async function setupTrackWorkspaceScenario(overrides: {policy?: Partial<Policy>; firstDayTrial?: string; lastDayTrial?: string; intentSource?: 'introSelected' | 'onboardingPurpose'} = {}) {
const policy = buildPolicy(overrides.policy);
// New workspaces enable Categories by default, so keep the categories step visible unless a test opts out.
const policy = buildPolicy({areCategoriesEnabled: true, ...overrides.policy});
await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);

if (overrides.intentSource === 'onboardingPurpose') {
Expand All @@ -80,7 +79,8 @@ async function setupTrackWorkspaceScenario(overrides: {policy?: Partial<Policy>;
}

async function setupManageTeamScenario(overrides: {policy?: Partial<Policy>; accounting?: string | null; firstDayTrial?: string; lastDayTrial?: string} = {}) {
const policy = buildPolicy(overrides.policy);
// New workspaces enable Categories by default, so keep the categories step visible unless a test opts out.
const policy = buildPolicy({areCategoriesEnabled: true, ...overrides.policy});
await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);
await Onyx.merge(ONYXKEYS.NVP_INTRO_SELECTED, {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM});
await Onyx.merge(ONYXKEYS.NVP_ACTIVE_POLICY_ID, POLICY_ID);
Expand Down Expand Up @@ -157,7 +157,7 @@ describe('useGettingStartedItems', () => {
it('should fall back to ONBOARDING_PURPOSE_SELECTED when NVP_INTRO_SELECTED is not available', async () => {
await Onyx.merge(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.MANAGE_TEAM);
await Onyx.merge(ONYXKEYS.NVP_ACTIVE_POLICY_ID, POLICY_ID);
const policy = buildPolicy();
const policy = buildPolicy({areCategoriesEnabled: true});
await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);
await Onyx.merge(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL, RECENT_TRIAL_START);
await Onyx.merge(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, FUTURE_TRIAL_END);
Expand Down Expand Up @@ -438,17 +438,7 @@ describe('useGettingStartedItems', () => {
expect(categoriesItem).toBeDefined();
});

it('should have isFeatureEnabled=true when accounting connections feature is enabled', async () => {
await setupManageTeamScenario({accounting: CONST.POLICY.CONNECTIONS.NAME.QBO, policy: {areConnectionsEnabled: true}});

const {result} = renderHook(() => useGettingStartedItems());
await waitForBatchedUpdates();

const connectItem = result.current.items.find((item) => item.key === 'connectAccounting');
expect(connectItem?.isFeatureEnabled).toBe(true);
});

it('should have isFeatureEnabled=false when accounting connections feature is not enabled but an existing connection makes the row visible', async () => {
it('should still show the connect accounting row when connections feature is disabled but an existing connection makes the row visible', async () => {
await setupManageTeamScenario({
accounting: CONST.POLICY.CONNECTIONS.NAME.QBO,
policy: {
Expand All @@ -472,7 +462,6 @@ describe('useGettingStartedItems', () => {
await waitFor(() => {
const connectItem = result.current.items.find((item) => item.key === 'connectAccounting');
expect(connectItem).toBeDefined();
expect(connectItem?.isFeatureEnabled).toBe(false);
});
});
});
Expand Down Expand Up @@ -505,25 +494,24 @@ describe('useGettingStartedItems', () => {
expect(connectItem).toBeUndefined();
});

it('should have isFeatureEnabled=true when categories feature is enabled', async () => {
it('should show the categories row when categories feature is enabled', async () => {
await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: true}});

const {result} = renderHook(() => useGettingStartedItems());
await waitForBatchedUpdates();

const categoriesItem = result.current.items.find((item) => item.key === 'customizeCategories');
expect(categoriesItem?.isFeatureEnabled).toBe(true);
expect(categoriesItem).toBeDefined();
});

it('should have isFeatureEnabled=false when categories feature is not enabled', async () => {
it('should not show the categories row when categories feature is not enabled', async () => {
await setupManageTeamScenario({accounting: 'none', policy: {areCategoriesEnabled: false}});

const {result} = renderHook(() => useGettingStartedItems());
await waitForBatchedUpdates();

const categoriesItem = result.current.items.find((item) => item.key === 'customizeCategories');
expect(categoriesItem).toBeDefined();
expect(categoriesItem?.isFeatureEnabled).toBe(false);
expect(categoriesItem).toBeUndefined();
});

it('should navigate to workspace categories route', async () => {
Expand Down Expand Up @@ -598,19 +586,6 @@ describe('useGettingStartedItems', () => {
expect(companyCardsItem).toBeDefined();
});

it('should have isFeatureEnabled=true when company cards feature is enabled', async () => {
await setupManageTeamScenario({
accounting: CONST.POLICY.CONNECTIONS.NAME.QBO,
policy: {areCompanyCardsEnabled: true},
});

const {result} = renderHook(() => useGettingStartedItems());
await waitForBatchedUpdates();

const companyCardsItem = result.current.items.find((item) => item.key === 'linkCompanyCards');
expect(companyCardsItem?.isFeatureEnabled).toBe(true);
});

it('should not be shown when company cards feature is not enabled', async () => {
await setupManageTeamScenario({
accounting: CONST.POLICY.CONNECTIONS.NAME.QBO,
Expand Down Expand Up @@ -683,23 +658,6 @@ describe('useGettingStartedItems', () => {
expect(expensifyCardItem?.subtitle).toBeTruthy();
});

it('should wire enableFeature to enableExpensifyCard and never re-enable company cards', async () => {
enableCompanyCards.mockClear();
enableExpensifyCard.mockClear();
await setupManageTeamScenario({
accounting: CONST.POLICY.CONNECTIONS.NAME.QBO,
policy: {areCompanyCardsEnabled: false, areExpensifyCardsEnabled: true, policyAccountID: WORKSPACE_ACCOUNT_ID},
});

const {result} = renderHook(() => useGettingStartedItems());
await waitForBatchedUpdates();

const expensifyCardItem = result.current.items.find((item) => item.key === 'issueExpensifyCards');
expensifyCardItem?.enableFeature?.();
expect(enableExpensifyCard).toHaveBeenCalledWith(POLICY_ID, true, false);
expect(enableCompanyCards).not.toHaveBeenCalled();
});

it('should be not completed when the workspace has no Expensify card provisioned', async () => {
await setupManageTeamScenario({
accounting: CONST.POLICY.CONNECTIONS.NAME.QBO,
Expand Down Expand Up @@ -1027,7 +985,7 @@ describe('useGettingStartedItems', () => {
await Onyx.merge(ONYXKEYS.NVP_INTRO_SELECTED, {choice: CONST.ONBOARDING_CHOICES.MANAGE_TEAM});
await Onyx.merge(ONYXKEYS.ONBOARDING_PURPOSE_SELECTED, CONST.ONBOARDING_CHOICES.PERSONAL_SPEND);
await Onyx.merge(ONYXKEYS.NVP_ACTIVE_POLICY_ID, POLICY_ID);
const policy = buildPolicy();
const policy = buildPolicy({areCategoriesEnabled: true});
await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, policy);
await Onyx.merge(ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL, RECENT_TRIAL_START);
await Onyx.merge(ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL, FUTURE_TRIAL_END);
Expand Down Expand Up @@ -1298,6 +1256,84 @@ describe('useGettingStartedItems', () => {
expect(inviteAccountantItem?.isComplete).toBe(true);
});
});

describe('link company card step', () => {
it('should insert linkCompanyCards between customizeCategories and inviteAccountant when company cards are enabled', async () => {
await setupTrackWorkspaceScenario({policy: {areCompanyCardsEnabled: true}});

const {result} = renderHook(() => useGettingStartedItems());

const keys = result.current.items.map((item) => item.key);
expect(keys).toEqual(['createWorkspace', 'customizeCategories', 'linkCompanyCards', 'inviteAccountant']);
});

it('should navigate to the workspace company cards route', async () => {
await setupTrackWorkspaceScenario({policy: {areCompanyCardsEnabled: true}});

const {result} = renderHook(() => useGettingStartedItems());

const companyCardsItem = result.current.items.find((item) => item.key === 'linkCompanyCards');
expect(companyCardsItem?.route).toBe(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(POLICY_ID));
});

it('should have isComplete=false when the workspace has no connected company card feed', async () => {
await setupTrackWorkspaceScenario({policy: {areCompanyCardsEnabled: true}});

const {result} = renderHook(() => useGettingStartedItems());

const companyCardsItem = result.current.items.find((item) => item.key === 'linkCompanyCards');
expect(companyCardsItem?.isComplete).toBe(false);
});

it('should have isComplete=true when the workspace has a connected company card feed', async () => {
const policyAccountID = 7777777;
const commercialFeed = CONST.COMPANY_CARD.FEED_BANK_NAME.VISA;
await Onyx.merge(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${policyAccountID}`, {
settings: {
companyCards: {
[commercialFeed]: {preferredPolicy: POLICY_ID, liabilityType: 'corporate'},
},
},
});
await setupTrackWorkspaceScenario({policy: {policyAccountID, areCompanyCardsEnabled: true}});

const {result} = renderHook(() => useGettingStartedItems());
await waitFor(() => expect(result.current.items.find((item) => item.key === 'linkCompanyCards')?.isComplete).toBe(true));
});
});

describe('feature toggles hide and show steps', () => {
it('should hide the customizeCategories step when Categories is disabled', async () => {
await setupTrackWorkspaceScenario({policy: {areCategoriesEnabled: false, areCompanyCardsEnabled: true}});

const {result} = renderHook(() => useGettingStartedItems());

const keys = result.current.items.map((item) => item.key);
expect(keys).toEqual(['createWorkspace', 'linkCompanyCards', 'inviteAccountant']);
});

it('should hide the linkCompanyCards step when Company cards is disabled', async () => {
await setupTrackWorkspaceScenario({policy: {areCategoriesEnabled: true, areCompanyCardsEnabled: false}});

const {result} = renderHook(() => useGettingStartedItems());

const keys = result.current.items.map((item) => item.key);
expect(keys).toEqual(['createWorkspace', 'customizeCategories', 'inviteAccountant']);
});

it('should preserve relative order and only collapse the hidden step when both features toggle', async () => {
await setupTrackWorkspaceScenario({policy: {areCategoriesEnabled: false, areCompanyCardsEnabled: false}});

const {result: bothDisabled} = renderHook(() => useGettingStartedItems());
expect(bothDisabled.current.items.map((item) => item.key)).toEqual(['createWorkspace', 'inviteAccountant']);

await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${POLICY_ID}`, {areCategoriesEnabled: true, areCompanyCardsEnabled: true});
await waitForBatchedUpdates();

const {result: bothEnabled} = renderHook(() => useGettingStartedItems());
expect(bothEnabled.current.items.map((item) => item.key)).toEqual(['createWorkspace', 'customizeCategories', 'linkCompanyCards', 'inviteAccountant']);
});
});
});

describe('hides the section once all to-dos are complete', () => {
Expand Down
Loading