diff --git a/assets/images/home-testdrive-image.png b/assets/images/home-testdrive-image.png
index 21f0408035dd..e763fca5dd9f 100644
Binary files a/assets/images/home-testdrive-image.png and b/assets/images/home-testdrive-image.png differ
diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 67571b1f02a8..b60eb9c52ca3 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -8187,6 +8187,29 @@ const CONST = {
DOMAIN_SECURITY_GROUP_PREFIX: 'domain_securityGroup_',
},
+ HOME: {
+ ANNOUNCEMENTS: [
+ {
+ title: 'Start the year with smarter spending, admin controls, and more.',
+ subtitle: 'Product update',
+ url: 'https://use.expensify.com/blog/expensify-january-2026-product-update',
+ publishedDate: '2026-01-28',
+ },
+ {
+ title: 'Our favorite features + final upgrades of the year',
+ subtitle: 'Product update',
+ url: 'https://use.expensify.com/blog/expensify-2025-year-end-product-update',
+ publishedDate: '2025-12-22',
+ },
+ {
+ title: 'Uber for business + Expensify automates ride and meal receipts',
+ subtitle: 'Product update',
+ url: 'https://use.expensify.com/blog/uber-for-business-and-expensify-integration-update',
+ publishedDate: '2025-12-01',
+ },
+ ],
+ },
+
SECTION_LIST_ITEM_TYPE: {
HEADER: 'header',
ROW: 'row',
diff --git a/src/components/DateIcon.tsx b/src/components/DateIcon.tsx
new file mode 100644
index 000000000000..cc07ec20025b
--- /dev/null
+++ b/src/components/DateIcon.tsx
@@ -0,0 +1,30 @@
+import {format, parseISO} from 'date-fns';
+import React from 'react';
+import {View} from 'react-native';
+import useStyleUtils from '@hooks/useStyleUtils';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import Text from './Text';
+
+type DateIconProps = {
+ /** Date string (e.g. ISO format YYYY-MM-DD) */
+ date: string;
+};
+
+function DateIcon({date}: DateIconProps) {
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const parsedDate = parseISO(date);
+ const monthAbbr = format(parsedDate, 'MMM');
+ const dayNumber = format(parsedDate, 'd');
+ const StyleUtils = useStyleUtils();
+
+ return (
+
+ {monthAbbr}
+ {dayNumber}
+
+ );
+}
+
+export default DateIcon;
diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx
index ba06841c08c1..bbd2c66df9b5 100644
--- a/src/components/MenuItem.tsx
+++ b/src/components/MenuItem.tsx
@@ -164,6 +164,9 @@ type MenuItemBaseProps = ForwardedFSClassProps &
/** Component to be displayed on the right */
rightComponent?: ReactNode;
+ /** Component to be displayed on the left */
+ leftComponent?: ReactNode;
+
/** A description text to show under the title */
description?: string;
@@ -503,6 +506,7 @@ function MenuItem({
shouldShowDescriptionOnTop = false,
shouldShowRightComponent = false,
rightComponent,
+ leftComponent,
rightIconReportID,
avatarSize = CONST.AVATAR_SIZE.DEFAULT,
isSmallAvatarSubscriptMenu = false,
@@ -778,6 +782,7 @@ function MenuItem({
)}
+ {!!leftComponent && {leftComponent}}
{/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */}
{isIDPassed && (
;
};
-function WidgetContainer({children, icon, title, titleColor, iconWidth = variables.iconSizeNormal, iconHeight = variables.iconSizeNormal}: WidgetContainerProps) {
+function WidgetContainer({children, icon, title, titleColor, iconWidth = variables.iconSizeNormal, iconHeight = variables.iconSizeNormal, containerStyles}: WidgetContainerProps) {
const styles = useThemeStyles();
const theme = useTheme();
const {shouldUseNarrowLayout} = useResponsiveLayout();
return (
-
+
{!!icon && (
diff --git a/src/pages/home/AnnouncementSection.tsx b/src/pages/home/AnnouncementSection.tsx
new file mode 100644
index 000000000000..f1f932643ccc
--- /dev/null
+++ b/src/pages/home/AnnouncementSection.tsx
@@ -0,0 +1,38 @@
+import React from 'react';
+import {Linking} from 'react-native';
+import DateIcon from '@components/DateIcon';
+import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
+import WidgetContainer from '@components/WidgetContainer';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+
+const announcements = CONST.HOME.ANNOUNCEMENTS;
+
+function AnnouncementSection() {
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ return (
+
+ {announcements.map((announcement) => (
+ Linking.openURL(announcement.url)}
+ shouldShowRightIcon
+ leftComponent={}
+ wrapperStyle={[styles.alignItemsCenter, shouldUseNarrowLayout ? styles.pl5 : styles.pl8]}
+ />
+ ))}
+
+ );
+}
+
+export default AnnouncementSection;
diff --git a/src/pages/home/DiscoverSection.tsx b/src/pages/home/DiscoverSection.tsx
index 7ffd2c09d714..995a203d7590 100644
--- a/src/pages/home/DiscoverSection.tsx
+++ b/src/pages/home/DiscoverSection.tsx
@@ -4,14 +4,19 @@ import HomeTestDriveImage from '@assets/images/home-testdrive-image.png';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import {PressableWithoutFeedback} from '@components/Pressable';
import WidgetContainer from '@components/WidgetContainer';
+import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin';
import useLocalize from '@hooks/useLocalize';
+import useOnboardingTaskInformation from '@hooks/useOnboardingTaskInformation';
import useOnyx from '@hooks/useOnyx';
+import useParentReportAction from '@hooks/useParentReportAction';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
+import {completeTestDriveTask} from '@libs/actions/Task';
import {getTestDriveURL} from '@libs/TourUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
+import {hasSeenTourSelector} from '@src/selectors/Onboarding';
const MAX_NUMBER_OF_LINES_TITLE = 4;
@@ -21,11 +26,38 @@ function DiscoverSection() {
const isCurrentUserPolicyAdmin = useIsPaidPolicyAdmin();
const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true});
const styles = useThemeStyles();
+ const currentUserPersonalDetails = useCurrentUserPersonalDetails();
+ const {
+ taskReport: viewTourTaskReport,
+ taskParentReport: viewTourTaskParentReport,
+ isOnboardingTaskParentReportArchived: isViewTourTaskParentReportArchived,
+ hasOutstandingChildTask,
+ } = useOnboardingTaskInformation(CONST.ONBOARDING_TASK_TYPE.VIEW_TOUR);
+ const parentReportAction = useParentReportAction(viewTourTaskReport);
+ const [hasSeenTour = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true});
const handlePress = () => {
Linking.openURL(getTestDriveURL(shouldUseNarrowLayout, introSelected, isCurrentUserPolicyAdmin));
+
+ if (hasSeenTour || !viewTourTaskReport || viewTourTaskReport.stateNum === CONST.REPORT.STATE_NUM.APPROVED) {
+ return;
+ }
+
+ completeTestDriveTask(
+ viewTourTaskReport,
+ viewTourTaskParentReport,
+ isViewTourTaskParentReportArchived,
+ currentUserPersonalDetails.accountID,
+ hasOutstandingChildTask,
+ parentReportAction,
+ false,
+ );
};
+ if (hasSeenTour) {
+ return null;
+ }
+
return (
EMPTY_STATE_MESSAGES.at(Math.floor(Math.random() * EMPTY_STATE_MESSAGES.length))!,
- [],
- );
+ const randomIndex = Math.floor(Math.random() * emptyStateMessages.length);
+ const emptyStateMessage = emptyStateMessages.at(randomIndex) ?? defaultEmptyStateMessage;
return (
@@ -47,6 +54,4 @@ function EmptyState() {
);
}
-EmptyState.displayName = 'EmptyState';
-
export default EmptyState;
diff --git a/src/pages/home/ForYouSection/index.tsx b/src/pages/home/ForYouSection/index.tsx
index bbf1825e65f7..291102e50445 100644
--- a/src/pages/home/ForYouSection/index.tsx
+++ b/src/pages/home/ForYouSection/index.tsx
@@ -2,8 +2,8 @@ import React from 'react';
import {View} from 'react-native';
import ActivityIndicator from '@components/ActivityIndicator';
import BaseWidgetItem from '@components/BaseWidgetItem';
-import {Cash, Export, Send, ThumbsUp} from '@components/Icon/Expensicons';
import WidgetContainer from '@components/WidgetContainer';
+import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
@@ -26,6 +26,7 @@ function ForYouSection() {
const [accountID] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false, selector: accountIDSelector});
const [isLoadingApp = true] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true});
const {reportCounts} = useTodos();
+ const icons = useMemoizedLazyExpensifyIcons(['Cash', 'Send', 'ThumbsUp', 'Export']);
const submitCount = reportCounts[CONST.SEARCH.SEARCH_KEYS.SUBMIT];
const approveCount = reportCounts[CONST.SEARCH.SEARCH_KEYS.APPROVE];
@@ -50,28 +51,28 @@ function ForYouSection() {
{
key: 'submit',
count: submitCount,
- icon: Send,
+ icon: icons.Send,
translationKey: 'homePage.forYouSection.submit' as const,
handler: createNavigationHandler(CONST.SEARCH.ACTION_FILTERS.SUBMIT, {from: [`${accountID}`]}),
},
{
key: 'approve',
count: approveCount,
- icon: ThumbsUp,
+ icon: icons.ThumbsUp,
translationKey: 'homePage.forYouSection.approve' as const,
handler: createNavigationHandler(CONST.SEARCH.ACTION_FILTERS.APPROVE, {to: [`${accountID}`]}),
},
{
key: 'pay',
count: payCount,
- icon: Cash,
+ icon: icons.Cash,
translationKey: 'homePage.forYouSection.pay' as const,
handler: createNavigationHandler(CONST.SEARCH.ACTION_FILTERS.PAY, {reimbursable: CONST.SEARCH.BOOLEAN.YES, payer: accountID?.toString()}),
},
{
key: 'export',
count: exportCount,
- icon: Export,
+ icon: icons.Export,
translationKey: 'homePage.forYouSection.export' as const,
handler: createNavigationHandler(CONST.SEARCH.ACTION_FILTERS.EXPORT, {exporter: [`${accountID}`], exportedOn: CONST.SEARCH.DATE_PRESETS.NEVER}),
},
diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx
index 244fdada1caa..971bd6f0077b 100644
--- a/src/pages/home/HomePage.tsx
+++ b/src/pages/home/HomePage.tsx
@@ -6,13 +6,11 @@ import TopBar from '@components/Navigation/TopBar';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
-import useOnyx from '@hooks/useOnyx';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import {confirmReadyToOpenApp} from '@libs/actions/App';
import usePreloadFullScreenNavigators from '@libs/Navigation/AppNavigator/usePreloadFullScreenNavigators';
-import ONYXKEYS from '@src/ONYXKEYS';
-import {hasSeenTourSelector} from '@src/selectors/Onboarding';
+import AnnouncementSection from './AnnouncementSection';
import DiscoverSection from './DiscoverSection';
import ForYouSection from './ForYouSection';
@@ -21,7 +19,6 @@ function HomePage() {
const shouldDisplayLHB = !shouldUseNarrowLayout;
const styles = useThemeStyles();
const {translate} = useLocalize();
- const [isSelfTourViewed = false] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector, canBeMissing: true});
// confirmReadyToOpenApp must be called after HomePage mounts
// to make sure everything loads properly
@@ -58,9 +55,11 @@ function HomePage() {
- {!isSelfTourViewed && }
+
+
+
+
-
{shouldDisplayLHB && }
diff --git a/src/styles/index.ts b/src/styles/index.ts
index f848059302af..5af960a7f2f7 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -5778,6 +5778,10 @@ const staticStyles = (theme: ThemeColors) =>
height: undefined,
aspectRatio: 2.2,
},
+ dateIconSize: {
+ width: variables.iconSizeExtraLarge,
+ height: variables.iconSizeExtraLarge,
+ },
}) satisfies StaticStyles;
const dynamicStyles = (theme: ThemeColors) =>
diff --git a/src/types/onyx/Announcement.ts b/src/types/onyx/Announcement.ts
new file mode 100644
index 000000000000..7280887232df
--- /dev/null
+++ b/src/types/onyx/Announcement.ts
@@ -0,0 +1,16 @@
+/** Model of announcement */
+type Announcement = {
+ /** The title of the announcement */
+ title: string;
+
+ /** The subtitle of the announcement */
+ subtitle: string;
+
+ /** The URL of the announcement */
+ url: string;
+
+ /** The published date of the announcement */
+ publishedDate: string;
+};
+
+export default Announcement;