diff --git a/assets/images/home-testdrive-image.png b/assets/images/home-testdrive-image.png new file mode 100644 index 000000000000..21f0408035dd Binary files /dev/null and b/assets/images/home-testdrive-image.png differ diff --git a/src/components/WidgetContainer.tsx b/src/components/WidgetContainer.tsx new file mode 100644 index 000000000000..f79bf5c6a1d8 --- /dev/null +++ b/src/components/WidgetContainer.tsx @@ -0,0 +1,59 @@ +import type {ReactNode} from 'react'; +import React from 'react'; +import {View} from 'react-native'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import variables from '@styles/variables'; +import type IconAsset from '@src/types/utils/IconAsset'; +import Icon from './Icon'; +import Text from './Text'; + +type WidgetContainerProps = { + /** The icon to display along with the title */ + icon?: IconAsset; + + /** The text to display in the title of the widget */ + title?: string; + + /** Custom color for the title text */ + titleColor?: string; + + /** The width of the icon. */ + iconWidth?: number; + + /** The height of the icon. */ + iconHeight?: number; + + /** The content to display inside the widget container */ + children: ReactNode; +}; + +function WidgetContainer({children, icon, title, titleColor, iconWidth = variables.iconSizeNormal, iconHeight = variables.iconSizeNormal}: WidgetContainerProps) { + const styles = useThemeStyles(); + const theme = useTheme(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + + return ( + + + {!!icon && ( + + + + )} + + {!!title && {title}} + + + {children} + + ); +} + +export type {WidgetContainerProps}; +export default WidgetContainer; diff --git a/src/languages/de.ts b/src/languages/de.ts index cb8825895951..ab9aca8e88ae 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -8211,6 +8211,16 @@ Hier ist ein *Testbeleg*, um dir zu zeigen, wie es funktioniert:`, }, fabGpsTripExplained: 'Zur GPS-Ansicht wechseln (Schnellaktion)', }, + homePage: { + forYou: 'Für dich', + announcements: 'Ankündigungen', + discoverSection: { + title: 'Entdecken', + menuItemTitleNonAdmin: 'Erfahren Sie, wie Sie Ausgaben erstellen und Berichte einreichen.', + menuItemTitleAdmin: 'Erfahren Sie, wie Sie Mitglieder einladen, Genehmigungsworkflows bearbeiten und Firmenkarten abstimmen.', + menuItemDescription: 'Sehen Sie, was Expensify in 2 Minuten kann', + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/en.ts b/src/languages/en.ts index 4a583cc1c0c0..ebf1631b23f2 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -988,6 +988,16 @@ const translations = { description: "We're fine-tuning a few more bits and pieces of New Expensify to accommodate your specific setup. In the meantime, head over to Expensify Classic.", }, }, + homePage: { + forYou: 'For you', + announcements: 'Announcements', + discoverSection: { + title: 'Discover', + menuItemTitleNonAdmin: 'Learn how to create expenses and submit reports.', + menuItemTitleAdmin: 'Learn how to invite members, edit approval workflows, and reconcile company cards.', + menuItemDescription: 'See what Expensify can do in 2 min', + }, + }, allSettingsScreen: { subscription: 'Subscription', domains: 'Domains', diff --git a/src/languages/es.ts b/src/languages/es.ts index 01c42783ffc0..2e873d3510fc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -733,6 +733,16 @@ const translations: TranslationDeepObject = { description: 'Estamos ajustando algunos detalles de New Expensify para adaptarla a tu configuración específica. Mientras tanto, dirígete a Expensify Classic.', }, }, + homePage: { + forYou: 'Para ti', + announcements: 'Anuncios', + discoverSection: { + title: 'Descubrir', + menuItemTitleNonAdmin: 'Aprende a crear gastos y enviar informes.', + menuItemTitleAdmin: 'Aprende a invitar a miembros, editar flujos de aprobación y conciliar tarjetas corporativas.', + menuItemDescription: 'Descubre lo que Expensify puede hacer en 2 minutos', + }, + }, allSettingsScreen: { subscription: 'Suscripcion', domains: 'Dominios', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 37cbe700d9c7..52e3d9509b88 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -8217,6 +8217,16 @@ Voici un *reçu test* pour vous montrer comment cela fonctionne :`, }, fabGpsTripExplained: 'Aller à l’écran GPS (action flottante)', }, + homePage: { + forYou: 'Pour vous', + announcements: 'Annonces', + discoverSection: { + title: 'Découvrir', + menuItemTitleNonAdmin: 'Découvrez comment créer des dépenses et soumettre des rapports.', + menuItemTitleAdmin: 'Apprenez à inviter des membres, à modifier les circuits d’approbation et à rapprocher les cartes de l’entreprise.', + menuItemDescription: 'Découvrez ce qu’Expensify peut faire en 2 minutes', + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/it.ts b/src/languages/it.ts index ed81344a9314..1b17f3cde9b1 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -6987,8 +6987,8 @@ Richiedi dettagli di spesa come ricevute e descrizioni, imposta limiti e valori [CONST.SEARCH.GROUP_BY.CARD]: 'Carta', [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID prelievo', //_/\__/_/ \_,_/\__/\__/\_,_/ [CONST.SEARCH.GROUP_BY.CATEGORY]: 'Categoria', - [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Commerciante', - [CONST.SEARCH.GROUP_BY.TAG]: 'Etichetta', + [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Esercente', + [CONST.SEARCH.GROUP_BY.TAG]: 'Tag', [CONST.SEARCH.GROUP_BY.MONTH]: 'Mese', }, feed: 'Feed', @@ -8197,6 +8197,16 @@ Ecco una *ricevuta di prova* per mostrarti come funziona:`, }, fabGpsTripExplained: 'Vai alla schermata GPS (azione flottante)', }, + homePage: { + forYou: 'Per te', + announcements: 'Annunci', + discoverSection: { + title: 'Scopri', + menuItemTitleNonAdmin: 'Scopri come creare spese e inviare report.', + menuItemTitleAdmin: 'Scopri come invitare membri, modificare i flussi di approvazione e riconciliare le carte aziendali.', + menuItemDescription: 'Scopri cosa può fare Expensify in 2 minuti', + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 53da5dd99c36..6e70da589e5e 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -6923,10 +6923,10 @@ ${reportName} reimbursable: '精算対象', purchaseCurrency: '購入通貨', groupBy: { - [CONST.SEARCH.GROUP_BY.FROM]: '送信者', + [CONST.SEARCH.GROUP_BY.FROM]: '差出人', [CONST.SEARCH.GROUP_BY.CARD]: 'カード', [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: '出金ID', - [CONST.SEARCH.GROUP_BY.CATEGORY]: 'カテゴリー', + [CONST.SEARCH.GROUP_BY.CATEGORY]: 'カテゴリ', [CONST.SEARCH.GROUP_BY.MERCHANT]: '加盟店', [CONST.SEARCH.GROUP_BY.TAG]: 'タグ', [CONST.SEARCH.GROUP_BY.MONTH]: '月', @@ -8110,6 +8110,16 @@ Expensify の使い方をお見せするための*テストレシート*がこ }, fabGpsTripExplained: 'GPS画面へ移動(フローティングアクション)', }, + homePage: { + forYou: 'あなた向け', + announcements: 'お知らせ', + discoverSection: { + title: '発見', + menuItemTitleNonAdmin: '経費の作成方法とレポートの提出方法を学びましょう。', + menuItemTitleAdmin: 'メンバーの招待方法、承認ワークフローの編集方法、会社カードの照合方法について学びましょう。', + menuItemDescription: '2 分で Expensify でできることを確認する', + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 6841f8dbb484..2ddc9f8299b5 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -6970,8 +6970,8 @@ Vraag verplichte uitgavedetails zoals bonnetjes en beschrijvingen, stel limieten [CONST.SEARCH.GROUP_BY.CARD]: 'Kaart', [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'Opname-ID', [CONST.SEARCH.GROUP_BY.CATEGORY]: 'Categorie', - [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Verkoper', - [CONST.SEARCH.GROUP_BY.TAG]: 'Label', + [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Handelaar', + [CONST.SEARCH.GROUP_BY.TAG]: 'Tag', [CONST.SEARCH.GROUP_BY.MONTH]: 'Maand', }, feed: 'Feed', @@ -8173,6 +8173,16 @@ Hier is een *testbon* om je te laten zien hoe het werkt:`, }, fabGpsTripExplained: 'Ga naar GPS-scherm (Zwevende actie)', }, + homePage: { + forYou: 'Voor jou', + announcements: 'Aankondigingen', + discoverSection: { + title: 'Ontdek', + menuItemTitleNonAdmin: 'Leer hoe je uitgaven aanmaakt en rapporten indient.', + menuItemTitleAdmin: 'Leer hoe u leden uitnodigt, goedkeuringsworkflows bewerkt en bedrijfskaarten afstemt.', + menuItemDescription: 'Ontdek wat Expensify in 2 minuten kan doen', + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 4f135c5c7a0f..d58658315e0a 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -8159,6 +8159,16 @@ Oto *paragon testowy*, który pokazuje, jak to działa:`, }, fabGpsTripExplained: 'Przejdź do ekranu GPS (przycisk akcji)', }, + homePage: { + forYou: 'Dla ciebie', + announcements: 'Ogłoszenia', + discoverSection: { + title: 'Odkryj', + menuItemTitleNonAdmin: 'Dowiedz się, jak tworzyć wydatki i składać raporty.', + menuItemTitleAdmin: 'Dowiedz się, jak zapraszać członków, edytować przepływy akceptacji i uzgadniać karty firmowe.', + menuItemDescription: 'Zobacz, co Expensify potrafi w 2 minuty', + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index f1d837ef5707..0f08e19e655a 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -6960,7 +6960,7 @@ Exija detalhes de despesas como recibos e descrições, defina limites e padrõe [CONST.SEARCH.GROUP_BY.CARD]: 'Cartão', [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: 'ID da retirada', [CONST.SEARCH.GROUP_BY.CATEGORY]: 'Categoria', - [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Comerciante', + [CONST.SEARCH.GROUP_BY.MERCHANT]: 'Estabelecimento', [CONST.SEARCH.GROUP_BY.TAG]: 'Etiqueta', [CONST.SEARCH.GROUP_BY.MONTH]: 'Mês', }, @@ -8168,6 +8168,16 @@ Aqui está um *recibo de teste* para mostrar como funciona:`, }, fabGpsTripExplained: 'Ir para a tela de GPS (Ação flutuante)', }, + homePage: { + forYou: 'Para você', + announcements: 'Anúncios', + discoverSection: { + title: 'Descobrir', + menuItemTitleNonAdmin: 'Aprenda a criar despesas e enviar relatórios.', + menuItemTitleAdmin: 'Aprenda a convidar membros, editar fluxos de aprovação e reconciliar cartões corporativos.', + menuItemDescription: 'Veja o que o Expensify pode fazer em 2 minutos', + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 0203fdfc7509..a78ef1288677 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -6804,10 +6804,10 @@ ${reportName} purchaseCurrency: '购买货币', groupBy: { [CONST.SEARCH.GROUP_BY.FROM]: '来自', - [CONST.SEARCH.GROUP_BY.CARD]: '卡片', - [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: '提现 ID', + [CONST.SEARCH.GROUP_BY.CARD]: '卡', + [CONST.SEARCH.GROUP_BY.WITHDRAWAL_ID]: '提现编号', [CONST.SEARCH.GROUP_BY.CATEGORY]: '类别', - [CONST.SEARCH.GROUP_BY.MERCHANT]: '商家', + [CONST.SEARCH.GROUP_BY.MERCHANT]: '商户', [CONST.SEARCH.GROUP_BY.TAG]: '标签', [CONST.SEARCH.GROUP_BY.MONTH]: '月', }, @@ -7933,6 +7933,16 @@ ${reportName} locationServicesRequiredModal: {title: '需要访问位置信息', confirm: '打开设置', prompt: '请在设备设置中允许位置访问,以开始 GPS 距离跟踪。'}, fabGpsTripExplained: '前往 GPS 屏幕(悬浮操作)', }, + homePage: { + forYou: '为你', + announcements: '公告', + discoverSection: { + title: '发现', + menuItemTitleNonAdmin: '了解如何创建费用并提交报表。', + menuItemTitleAdmin: '了解如何邀请成员、编辑审批流程以及对公司卡进行对账。', + menuItemDescription: '看看 Expensify 在 2 分钟内能为你做什么', + }, + }, }; // IMPORTANT: This line is manually replaced in generate translation files by scripts/generateTranslations.ts, // so if you change it here, please update it there as well. diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 1afe91bbd0a2..d2be5fccf9a0 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -86,7 +86,7 @@ const loadLogOutPreviousUserPage = () => require('../../.. const loadConciergePage = () => require('../../../pages/ConciergePage').default; const loadTrackExpensePage = () => require('../../../pages/TrackExpensePage').default; const loadSubmitExpensePage = () => require('../../../pages/SubmitExpensePage').default; -const loadHomePage = () => require('../../../pages/HomePage').default; +const loadHomePage = () => require('../../../pages/home/HomePage').default; const loadWorkspaceJoinUser = () => require('@pages/workspace/WorkspaceJoinUserPage').default; const loadReportSplitNavigator = () => require('./Navigators/ReportsSplitNavigator').default; diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx deleted file mode 100644 index b388ea37bdc6..000000000000 --- a/src/pages/HomePage.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import NavigationTabBar from '@components/Navigation/NavigationTabBar'; -import NAVIGATION_TABS from '@components/Navigation/NavigationTabBar/NAVIGATION_TABS'; -import TopBar from '@components/Navigation/TopBar'; -import ScreenWrapper from '@components/ScreenWrapper'; -import useLocalize from '@hooks/useLocalize'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; - -function HomePage() { - const {shouldUseNarrowLayout} = useResponsiveLayout(); - const shouldDisplayLHB = !shouldUseNarrowLayout; - const {translate} = useLocalize(); - - return ( - - ) - } - > - - {shouldDisplayLHB && } - - ); -} - -export default HomePage; diff --git a/src/pages/home/DiscoverSection.tsx b/src/pages/home/DiscoverSection.tsx new file mode 100644 index 000000000000..7ffd2c09d714 --- /dev/null +++ b/src/pages/home/DiscoverSection.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import {Image, Linking, View} from 'react-native'; +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 useIsPaidPolicyAdmin from '@hooks/useIsPaidPolicyAdmin'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {getTestDriveURL} from '@libs/TourUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +const MAX_NUMBER_OF_LINES_TITLE = 4; + +function DiscoverSection() { + const {translate} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const isCurrentUserPolicyAdmin = useIsPaidPolicyAdmin(); + const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); + const styles = useThemeStyles(); + + const handlePress = () => { + Linking.openURL(getTestDriveURL(shouldUseNarrowLayout, introSelected, isCurrentUserPolicyAdmin)); + }; + + return ( + + + + + + + + + ); +} + +export default DiscoverSection; diff --git a/src/pages/home/HomePage.tsx b/src/pages/home/HomePage.tsx new file mode 100644 index 000000000000..52116b9426f4 --- /dev/null +++ b/src/pages/home/HomePage.tsx @@ -0,0 +1,67 @@ +import React, {useEffect} from 'react'; +import {View} from 'react-native'; +import NavigationTabBar from '@components/Navigation/NavigationTabBar'; +import NAVIGATION_TABS from '@components/Navigation/NavigationTabBar/NAVIGATION_TABS'; +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 DiscoverSection from './DiscoverSection'; + +function HomePage() { + const {shouldUseNarrowLayout} = useResponsiveLayout(); + 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 + useEffect(() => { + confirmReadyToOpenApp(); + }, []); + + // This hook preloads the screens of adjacent tabs to make changing tabs faster. + usePreloadFullScreenNavigators(); + + return ( + + ) + } + > + + + + {!isSelfTourViewed && } + + + + {shouldDisplayLHB && } + + ); +} + +export default HomePage; diff --git a/src/styles/index.ts b/src/styles/index.ts index fbd184e9bfe1..0222cad186fb 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -3663,6 +3663,17 @@ const staticStyles = (theme: ThemeColors) => marginHorizontal: variables.sectionMargin, }, + widgetContainer: { + backgroundColor: theme.cardBG, + borderRadius: variables.componentBorderRadiusLarge, + overflow: 'hidden', + }, + + homePageContentContainer: { + flexGrow: 1, + padding: 20, + }, + cardSectionIllustration: { width: 'auto', height: variables.sectionIllustrationHeight, @@ -5699,6 +5710,11 @@ const staticStyles = (theme: ThemeColors) => paymentMethodErrorRow: { paddingHorizontal: variables.iconSizeMenuItem + variables.iconSizeNormal / 2, }, + discoverSectionImage: { + width: '100%', + height: undefined, + aspectRatio: 2.2, + }, }) satisfies StaticStyles; const dynamicStyles = (theme: ThemeColors) => @@ -6193,6 +6209,29 @@ const plainStyles = (theme: ThemeColors) => searchTopBarZIndexStyle: { zIndex: variables.searchTopBarZIndex, }, + + getWidgetContainerTitleStyle: (color: string) => + ({ + ...FontUtils.fontFamily.platform.EXP_NEUE_BOLD, + fontSize: 17, + lineHeight: 20, + color, + }) satisfies TextStyle, + + homePageMainLayout: (shouldUseNarrowLayout: boolean) => + ({ + flexDirection: shouldUseNarrowLayout ? 'column' : 'row', + gap: 20, + width: '100%', + }) satisfies ViewStyle, + + homePageLeftColumn: (shouldUseNarrowLayout: boolean) => + shouldUseNarrowLayout + ? ({width: '100%', flexDirection: 'column', gap: 20} satisfies ViewStyle) + : ({flex: 2, flexBasis: '66.666%', maxWidth: variables.homePageLeftColumnMaxWidth, flexDirection: 'column', gap: 20} satisfies ViewStyle), + + homePageRightColumn: (shouldUseNarrowLayout: boolean) => + shouldUseNarrowLayout ? ({width: '100%'} satisfies ViewStyle) : ({flex: 1, flexBasis: '33.333%', maxWidth: variables.homePageRightColumnMaxWidth} satisfies ViewStyle), }) satisfies Styles; const styles = (theme: ThemeColors) => diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 4c509891322d..ae9ebc8f305f 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -104,6 +104,8 @@ export default { sideBarWidth: 375, sidePanelWidth: 375, receiptPaneRHPMaxWidth: 465, + homePageLeftColumnMaxWidth: 920, + homePageRightColumnMaxWidth: 460, superWideRHPMaxWidth: 1260, minScanTooltipWidth: 320, uploadViewMargin: 20,