Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
29e2993
Add tests
adamgrzybowski Mar 30, 2026
86257b0
Add Getting Started onboarding checklist to Home page
adamgrzybowski Mar 30, 2026
4a806bd
Simplify control flow in hasNonDefaultRules
adamgrzybowski Mar 30, 2026
1875f8b
Remove unnecessary useCallback from GettingStartedRow
adamgrzybowski Mar 30, 2026
3703841
Replace magic numbers with consts in GettingStartedRow
adamgrzybowski Mar 30, 2026
7548620
Add explanatory comments to eslint-disable directives in useGettingSt…
adamgrzybowski Mar 30, 2026
7622ea8
Fix isWithinGettingStartedPeriod to reject future trial dates
adamgrzybowski Mar 30, 2026
547cc92
Fix create-workspace route assertions to use WORKSPACE_OVERVIEW
adamgrzybowski Mar 31, 2026
6bab2b8
Add gettingStartedSection translations and format suggestionsAvailable
adamgrzybowski Mar 31, 2026
9405144
Add Sentry label to GettingStartedRow pressable
adamgrzybowski Mar 31, 2026
dff7ec9
Remove default ID values from useOnyx collection keys
adamgrzybowski Mar 31, 2026
d75f4a7
Fix hasNonDefaultRules to detect customRules without rules object and…
adamgrzybowski Mar 31, 2026
b3869c4
Merge branch 'main' into @adamgrzybowski/onboarding-home
adamgrzybowski Mar 31, 2026
5297e87
Add policy guards and company card feed detection to Getting Started …
adamgrzybowski Apr 1, 2026
218be28
Merge branch 'main' into @adamgrzybowski/onboarding-home
adamgrzybowski Apr 2, 2026
72ec6af
Reuse getCompanyFeeds in hasCompanyCardFeeds to remove duplicated fil…
adamgrzybowski Apr 2, 2026
a886a1f
Revert accidental changes
adamgrzybowski Apr 2, 2026
4bb453f
Fix GettingStartedSection tests by adding policy type to mock data
adamgrzybowski Apr 2, 2026
a7c1262
Navigate to WorkspaceInitialPage on narrow layout in GettingStartedSe…
WojtekBoman Apr 8, 2026
11184c8
Enable feature and navigate directly when checklist item is disabled
WojtekBoman Apr 8, 2026
1d8ae1c
Fix navigating on create workspace button and adjust jest tests
WojtekBoman Apr 9, 2026
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
1 change: 1 addition & 0 deletions src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8974,6 +8974,7 @@ const CONST = {
},
HOME_PAGE: {
WIDGET_ITEM: 'HomePage-WidgetItem',
GETTING_STARTED_ROW: 'HomePage-GettingStartedRow',
},
CALENDAR_PICKER: {
YEAR_PICKER: 'CalendarPicker-YearPicker',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,14 @@ const translations: TranslationDeepObject<typeof en> = {
inDays: () => ({one: 'In 1 Tag', other: (count: number) => `In ${count} Tagen`}),
today: 'Heute',
},
gettingStartedSection: {
title: 'Erste Schritte',
createWorkspace: 'Workspace erstellen',
connectAccounting: ({integrationName}: {integrationName: string}) => `Mit ${integrationName} verbinden`,
customizeCategories: 'Buchhaltungskategorien anpassen',
linkCompanyCards: 'Firmenkarten verknüpfen',
setupRules: 'Ausgabelimits einrichten',
},
freeTrialSection: {
title: ({days}: {days: number}) => `Kostenlose Testversion: Noch ${days} ${days === 1 ? 'Tag' : 'Tage'}!`,
offer50Body: 'Sparen Sie 50 % im ersten Jahr!',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,14 @@ const translations = {
fireworksDescription: 'Upcoming to-dos will appear here.',
},
},
gettingStartedSection: {
title: 'Getting started',
createWorkspace: 'Create a workspace',
connectAccounting: ({integrationName}: {integrationName: string}) => `Connect to ${integrationName}`,
customizeCategories: 'Customize accounting categories',
linkCompanyCards: 'Link company cards',
setupRules: 'Set up spend rules',
},
upcomingTravel: 'Upcoming travel',
upcomingTravelSection: {
flightTo: ({destination}: {destination: string}) => `Flight to ${destination}`,
Expand Down
8 changes: 8 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,14 @@ const translations: TranslationDeepObject<typeof en> = {
fireworksDescription: 'Las próximas tareas aparecerán aquí.',
},
},
gettingStartedSection: {
title: 'Primeros pasos',
createWorkspace: 'Crear un espacio de trabajo',
connectAccounting: ({integrationName}: {integrationName: string}) => `Conectar con ${integrationName}`,
customizeCategories: 'Personalizar categorías contables',
linkCompanyCards: 'Vincular tarjetas corporativas',
setupRules: 'Configurar reglas de gasto',
},
upcomingTravel: 'Próximos viajes',
upcomingTravelSection: {
flightTo: ({destination}: {destination: string}) => `Vuelo a ${destination}`,
Expand Down
8 changes: 8 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1067,6 +1067,14 @@ const translations: TranslationDeepObject<typeof en> = {
other: (pluralCount: number) => `Temps restant : ${pluralCount} jours`,
}),
},
gettingStartedSection: {
title: 'Premiers pas',
createWorkspace: 'Créer un espace de travail',
connectAccounting: ({integrationName}: {integrationName: string}) => `Se connecter à ${integrationName}`,
customizeCategories: 'Personnaliser les catégories comptables',
linkCompanyCards: 'Lier des cartes d’entreprise',
setupRules: 'Configurer les règles de dépense',
},
},
allSettingsScreen: {
subscription: 'Abonnement',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,14 @@ const translations: TranslationDeepObject<typeof en> = {
other: (pluralCount: number) => `Tempo rimanente: ${pluralCount} giorni`,
}),
},
gettingStartedSection: {
title: 'Per iniziare',
createWorkspace: 'Crea uno spazio di lavoro',
connectAccounting: ({integrationName}: {integrationName: string}) => `Connetti a ${integrationName}`,
customizeCategories: 'Personalizza le categorie contabili',
linkCompanyCards: 'Collega carte aziendali',
setupRules: 'Configura le regole di spesa',
},
},
allSettingsScreen: {
subscription: 'Abbonamento',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,14 @@ const translations: TranslationDeepObject<typeof en> = {
other: (pluralCount: number) => `残り時間:${pluralCount}日`,
}),
},
gettingStartedSection: {
title: 'はじめに',
createWorkspace: 'ワークスペースを作成',
connectAccounting: ({integrationName}: {integrationName: string}) => `${integrationName}に接続する`,
customizeCategories: '会計カテゴリをカスタマイズする',
linkCompanyCards: '会社カードを連携',
setupRules: '支出ルールを設定',
},
},
allSettingsScreen: {
subscription: 'サブスクリプション',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1063,6 +1063,14 @@ const translations: TranslationDeepObject<typeof en> = {
other: (pluralCount: number) => `Resterende tijd: ${pluralCount} dagen`,
}),
},
gettingStartedSection: {
title: 'Aan de slag',
createWorkspace: 'Maak een werkruimte',
connectAccounting: ({integrationName}: {integrationName: string}) => `Verbind met ${integrationName}`,
customizeCategories: 'Boekhoudcategorieën aanpassen',
linkCompanyCards: 'Bedrijfspassen koppelen',
setupRules: 'Uitgavenregels instellen',
},
},
allSettingsScreen: {
subscription: 'Abonnement',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,14 @@ const translations: TranslationDeepObject<typeof en> = {
other: (pluralCount: number) => `Pozostały czas: ${pluralCount} dni`,
}),
},
gettingStartedSection: {
title: 'Pierwsze kroki',
createWorkspace: 'Utwórz przestrzeń roboczą',
connectAccounting: ({integrationName}: {integrationName: string}) => `Połącz z ${integrationName}`,
customizeCategories: 'Dostosuj kategorie księgowe',
linkCompanyCards: 'Połącz firmowe karty',
setupRules: 'Skonfiguruj zasady wydatków',
},
},
allSettingsScreen: {
subscription: 'Subskrypcja',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,14 @@ const translations: TranslationDeepObject<typeof en> = {
other: (pluralCount: number) => `Tempo restante: ${pluralCount} dias`,
}),
},
gettingStartedSection: {
title: 'Introdução',
createWorkspace: 'Criar um workspace',
connectAccounting: ({integrationName}: {integrationName: string}) => `Conectar ao ${integrationName}`,
customizeCategories: 'Personalizar categorias contábeis',
linkCompanyCards: 'Vincular cartões corporativos',
setupRules: 'Configurar regras de gasto',
},
},
allSettingsScreen: {
subscription: 'Assinatura',
Expand Down
8 changes: 8 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,14 @@ const translations: TranslationDeepObject<typeof en> = {
other: (pluralCount: number) => `剩余时间:${pluralCount}天`,
}),
},
gettingStartedSection: {
title: '入门',
createWorkspace: '创建工作区',
connectAccounting: ({integrationName}: {integrationName: string}) => `连接到 ${integrationName}`,
customizeCategories: '自定义会计类别',
linkCompanyCards: '关联公司卡',
setupRules: '设置消费规则',
},
},
allSettingsScreen: {
subscription: '订阅',
Expand Down
5 changes: 5 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,10 @@ function getCompanyFeeds(cardFeeds: OnyxEntry<CombinedCardFeeds>, shouldFilterOu
);
}

function hasCompanyCardFeeds(cardFeeds: OnyxEntry<CombinedCardFeeds>): boolean {
Comment thread
adamgrzybowski marked this conversation as resolved.
return Object.keys(getCompanyFeeds(cardFeeds, true, true)).length > 0;
}

function getBankName(feedType: CardFeedWithNumber | CardFeedWithDomainID): string {
const cacheKey = feedType ?? '';
const cached = getBankNameCache.get(cacheKey);
Expand Down Expand Up @@ -1590,6 +1594,7 @@ export {
isSelectedFeedExpired,
isTravelCard,
getCompanyFeeds,
hasCompanyCardFeeds,
isPersonalCardBrokenConnection,
isCustomFeed,
isCSVFeedOrExpensifyCard,
Expand Down
20 changes: 20 additions & 0 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@

let allPolicies: OnyxCollection<Policy>;

Onyx.connect({

Check warning on line 76 in src/libs/PolicyUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
Expand Down Expand Up @@ -687,6 +687,25 @@
return Object.values(policyCategories).some((category) => category && category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && !defaultCategoryNames.has(category.name));
}

/**
* Checks if a policy has any non-default rules configured.
* Defaults are: no approval/expense/coding rules and no custom rules text.
*/
function hasNonDefaultRules(policy: OnyxEntry<Policy>): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add unit test for this?

if (!policy) {
return false;
}

const hasCustomRules = !!policy.customRules && policy.customRules.trim().length > 0;

const {rules} = policy;
const hasApprovalRules = !!rules?.approvalRules && rules.approvalRules.length > 0;
const hasExpenseRules = !!rules?.expenseRules && rules.expenseRules.length > 0;
const hasCodingRules = !!rules?.codingRules && Object.keys(rules.codingRules).length > 0;

return hasCustomRules || hasApprovalRules || hasExpenseRules || hasCodingRules;
Comment on lines +699 to +706

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super minor thing, but you could return early for the hasCustomRules before we spread the policy to get rules

}

/**
* Gets a tag list of a policy by a tag index
*/
Expand Down Expand Up @@ -2074,6 +2093,7 @@
getTagLists,
hasTags,
hasCustomCategories,
hasNonDefaultRules,
getTaxByID,
getUnitRateValue,
getRateDisplayValue,
Expand Down
4 changes: 2 additions & 2 deletions src/libs/actions/Policy/Policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@

const deprecatedAllPolicies: OnyxCollection<Policy> = {};
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY,

Check warning on line 255 in src/libs/actions/Policy/Policy.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
callback: (val, key) => {
if (!key) {
return;
Expand All @@ -268,7 +268,7 @@

let deprecatedAllReportActions: OnyxCollection<ReportActions>;
Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,

Check warning on line 271 in src/libs/actions/Policy/Policy.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
waitForCollectionCallback: true,
callback: (actions) => {
deprecatedAllReportActions = actions;
Expand All @@ -278,7 +278,7 @@
let deprecatedSessionEmail = '';
let deprecatedSessionAccountID = 0;
Onyx.connect({
key: ONYXKEYS.SESSION,

Check warning on line 281 in src/libs/actions/Policy/Policy.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
callback: (val) => {
deprecatedSessionEmail = val?.email ?? '';
deprecatedSessionAccountID = val?.accountID ?? CONST.DEFAULT_NUMBER_ID;
Expand All @@ -287,7 +287,7 @@

let deprecatedAllPersonalDetails: OnyxEntry<PersonalDetailsList>;
Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS_LIST,

Check warning on line 290 in src/libs/actions/Policy/Policy.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
callback: (val) => (deprecatedAllPersonalDetails = val),
});

Expand Down Expand Up @@ -4391,7 +4391,7 @@
return {policyID, workspaceChatReportID: memberData.workspaceChatReportID, reportPreviewReportActionID: reportPreviewAction?.reportActionID, adminsChatReportID};
}

function enablePolicyConnections(policyID: string, enabled: boolean) {
function enablePolicyConnections(policyID: string, enabled: boolean, shouldGoBack = true) {
const onyxData: OnyxData<typeof ONYXKEYS.COLLECTION.POLICY> = {
optimisticData: [
{
Expand Down Expand Up @@ -4434,7 +4434,7 @@

API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS, parameters, onyxData);

if (enabled && getIsNarrowLayout()) {
if (enabled && getIsNarrowLayout() && shouldGoBack) {
goBackWhenEnableFeature();
}
}
Expand Down
79 changes: 79 additions & 0 deletions src/pages/home/GettingStartedSection/GettingStartedRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';
import {View} from 'react-native';
import Checkbox from '@components/Checkbox';
import Icon from '@components/Icon';
import {PressableWithoutFeedback} from '@components/Pressable';
import Text from '@components/Text';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import type {GettingStartedItem} from './hooks/useGettingStartedItems';

type GettingStartedRowProps = {
item: GettingStartedItem;
};

function GettingStartedRow({item}: GettingStartedRowProps) {
const styles = useThemeStyles();
const theme = useTheme();
const StyleUtils = useStyleUtils();
const {shouldUseNarrowLayout} = useResponsiveLayout();
const icons = useMemoizedLazyExpensifyIcons(['ArrowRight', 'Checkmark'] as const);

const navigateToItem = () => {
if (!item.isFeatureEnabled) {
item.enableFeature?.();
}
Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.navigate(item.route));
};

return (
<PressableWithoutFeedback
onPress={navigateToItem}
accessibilityLabel={item.label}
sentryLabel={CONST.SENTRY_LABEL.HOME_PAGE.GETTING_STARTED_ROW}
>
{({hovered}) => (
<View style={[styles.flexRow, styles.alignItemsCenter, styles.gap3, shouldUseNarrowLayout ? styles.ph5 : styles.ph8, styles.pv3, hovered && styles.hoveredComponentBG]}>
{item.isComplete ? (
<View
style={[
StyleUtils.getCheckboxContainerStyle(variables.iconSizeNormal, variables.componentBorderRadiusSmall),
{backgroundColor: theme.icon, borderColor: theme.icon},
]}
>
<Icon
Comment thread
adamgrzybowski marked this conversation as resolved.
src={icons.Checkmark}
fill={theme.textLight}
height={variables.iconSizeSemiSmall}
width={variables.iconSizeSemiSmall}
/>
</View>
) : (
<Checkbox
isChecked={false}
onPress={navigateToItem}
accessibilityLabel={item.label}
/>
)}
<Text style={[styles.flex1, styles.textBold, item.isComplete && {color: theme.textSupporting}]}>{item.label}</Text>
{!item.isComplete && (
<Icon
src={icons.ArrowRight}
width={variables.iconSizeNormal}
height={variables.iconSizeNormal}
fill={theme.icon}
/>
)}
</View>
)}
</PressableWithoutFeedback>
);
}

export default GettingStartedRow;
Loading
Loading