diff --git a/assets/images/expensifyCard/cardIllustration.svg b/assets/images/expensifyCard/cardIllustration.svg
new file mode 100644
index 000000000000..f8162bbd913f
--- /dev/null
+++ b/assets/images/expensifyCard/cardIllustration.svg
@@ -0,0 +1,487 @@
+
+
+
diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts
index e699badc43ec..bd0824372799 100644
--- a/src/components/Icon/Illustrations.ts
+++ b/src/components/Icon/Illustrations.ts
@@ -1,3 +1,4 @@
+import ExpensifyCardIllustration from '@assets/images/expensifyCard/cardIllustration.svg';
import Abracadabra from '@assets/images/product-illustrations/abracadabra.svg';
import BankArrowPink from '@assets/images/product-illustrations/bank-arrow--pink.svg';
import BankMouseGreen from '@assets/images/product-illustrations/bank-mouse--green.svg';
@@ -176,6 +177,7 @@ export {
Binoculars,
CompanyCard,
ReceiptUpload,
+ ExpensifyCardIllustration,
SplitBill,
PiggyBank,
Accounting,
diff --git a/src/languages/en.ts b/src/languages/en.ts
index 936941003073..bf6b490a1c66 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -2390,6 +2390,16 @@ export default {
disableCardTitle: 'Disable Expensify Card',
disableCardPrompt: 'You can’t disable the Expensify Card because it’s already in use. Reach out to Concierge for next steps.',
disableCardButton: 'Chat with Concierge',
+ feed: {
+ title: 'Get the Expensify Card',
+ subTitle: 'Streamline your business with the Expensify Card',
+ features: {
+ cashBack: 'Up to 2% cash back on every US purchase',
+ unlimited: 'Issue unlimited virtual cards',
+ spend: 'Spend controls and custom limits',
+ },
+ ctaTitle: 'Issue new card',
+ },
},
workflows: {
title: 'Workflows',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 59aad3275c41..f3e6d88a470b 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -2420,6 +2420,16 @@ export default {
disableCardTitle: 'Deshabilitar la Tarjeta Expensify',
disableCardPrompt: 'No puedes deshabilitar la Tarjeta Expensify porque ya está en uso. Por favor, contacta con Concierge para conocer los pasos a seguir.',
disableCardButton: 'Chatear con Concierge',
+ feed: {
+ title: 'Consigue la Tarjeta Expensify',
+ subTitle: 'Optimiza tu negocio con la Tarjeta Expensify',
+ features: {
+ cashBack: 'Hasta un 2% de devolución en cada compra en Estadios Unidos',
+ unlimited: 'Emitir un número ilimitado de tarjetas virtuales',
+ spend: 'Controles de gastos y límites personalizados',
+ },
+ ctaTitle: 'Emitir nueva tarjeta',
+ },
},
distanceRates: {
title: 'Tasas de distancia',
diff --git a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
index 16e8404f5fe9..748d92b49a1c 100644
--- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
+++ b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx
@@ -19,7 +19,6 @@ type Screens = Partial React.Co
const CENTRAL_PANE_WORKSPACE_SCREENS = {
[SCREENS.WORKSPACE.PROFILE]: () => require('../../../../pages/workspace/WorkspaceProfilePage').default,
[SCREENS.WORKSPACE.CARD]: () => require('../../../../pages/workspace/card/WorkspaceCardPage').default,
- [SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default,
[SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default,
[SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../../pages/workspace/reimburse/WorkspaceReimbursePage').default,
[SCREENS.WORKSPACE.BILLS]: () => require('../../../../pages/workspace/bills/WorkspaceBillsPage').default,
@@ -32,6 +31,7 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = {
[SCREENS.WORKSPACE.TAGS]: () => require('../../../../pages/workspace/tags/WorkspaceTagsPage').default,
[SCREENS.WORKSPACE.TAXES]: () => require('../../../../pages/workspace/taxes/WorkspaceTaxesPage').default,
[SCREENS.WORKSPACE.REPORT_FIELDS]: () => require('../../../../pages/workspace/reportFields/WorkspaceReportFieldsPage').default,
+ [SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default,
[SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default,
} satisfies Screens;
diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx
new file mode 100644
index 000000000000..36aee07ab6b5
--- /dev/null
+++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx
@@ -0,0 +1,173 @@
+import {useFocusEffect} from '@react-navigation/native';
+import type {StackScreenProps} from '@react-navigation/stack';
+import React, {useCallback, useMemo} from 'react';
+import type {ListRenderItemInfo} from 'react-native';
+import {FlatList, View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+import Button from '@components/Button';
+import HeaderWithBackButton from '@components/HeaderWithBackButton';
+import * as Expensicons from '@components/Icon/Expensicons';
+import * as Illustrations from '@components/Icon/Illustrations';
+import OfflineWithFeedback from '@components/OfflineWithFeedback';
+import {PressableWithoutFeedback} from '@components/Pressable';
+import ScreenWrapper from '@components/ScreenWrapper';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useThemeStyles from '@hooks/useThemeStyles';
+import localeCompare from '@libs/LocaleCompare';
+import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
+import Navigation from '@navigation/Navigation';
+import type {FullScreenNavigatorParamList} from '@navigation/types';
+import CONST from '@src/CONST';
+import ONYXKEYS from '@src/ONYXKEYS';
+import type SCREENS from '@src/SCREENS';
+import type {Card, WorkspaceCardsList} from '@src/types/onyx';
+import WorkspaceCardListHeader from './WorkspaceCardListHeader';
+import WorkspaceCardListRow from './WorkspaceCardListRow';
+
+type WorkspaceExpensifyCardListPageProps = {route: StackScreenProps['route']};
+
+// TODO: remove this const altogether and take the card data from component prop when Onyx data is available
+const mockedCards: OnyxEntry = {
+ test1: {
+ // @ts-expect-error TODO: change cardholder to accountID
+ cardholder: {accountID: 1, lastName: 'Smith', firstName: 'Bob', displayName: 'Bob Smith'},
+ nameValuePairs: {
+ unapprovedExpenseLimit: 1000,
+ cardTitle: 'Test 1',
+ },
+ lastFourPAN: '1234',
+ },
+ test2: {
+ // @ts-expect-error TODO: change cardholder to accountID
+ cardholder: {accountID: 2, lastName: 'Miller', firstName: 'Alex', displayName: 'Alex Miller'},
+ nameValuePairs: {
+ unapprovedExpenseLimit: 2000,
+ cardTitle: 'Test 2',
+ },
+ lastFourPAN: '1234',
+ },
+ test3: {
+ // @ts-expect-error TODO: change cardholder to accountID
+ cardholder: {accountID: 3, lastName: 'Brown', firstName: 'Kevin', displayName: 'Kevin Brown'},
+ nameValuePairs: {
+ unapprovedExpenseLimit: 3000,
+ cardTitle: 'Test 3',
+ },
+ lastFourPAN: '1234',
+ },
+};
+
+function WorkspaceExpensifyCardListPage({route}: WorkspaceExpensifyCardListPageProps) {
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+
+ const policyID = route.params.policyID;
+ const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
+
+ const policyCurrency = useMemo(() => policy?.outputCurrency ?? CONST.CURRENCY.USD, [policy]);
+
+ // TODO: uncomment the code line below to use cardsList data from Onyx when it's supported
+ // const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${policyID}_${CONST.EXPENSIFY_CARD.BANK}`);
+ const cardsList = mockedCards;
+
+ const fetchExpensifyCards = useCallback(() => {
+ // TODO: uncomment when OpenPolicyExpensifyCardsPage API call is supported
+ // Policy.openPolicyExpensifyCardsPage(policyID);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [policyID]);
+
+ useFocusEffect(fetchExpensifyCards);
+
+ const sortedCards = useMemo(
+ () =>
+ Object.values(cardsList ?? {}).sort((a, b) => {
+ // @ts-expect-error TODO: change cardholder to accountID and get personal details with it
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
+ const aName = PersonalDetailsUtils.getDisplayNameOrDefault(a.cardholder ?? {});
+ // @ts-expect-error TODO: change cardholder to accountID and get personal details with it
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
+ const bName = PersonalDetailsUtils.getDisplayNameOrDefault(b.cardholder ?? {});
+ return localeCompare(aName, bName);
+ }),
+ [cardsList],
+ );
+
+ const getHeaderButtons = () => (
+
+
+ );
+
+ const renderItem = ({item, index}: ListRenderItemInfo) => (
+
+ {}} // TODO: add navigation action when card details screen is implemented (https://github.com/Expensify/App/issues/44325)
+ >
+ {({hovered}) => (
+
+ )}
+
+
+ );
+
+ return (
+
+ Navigation.goBack()}
+ >
+ {!shouldUseNarrowLayout && getHeaderButtons()}
+
+
+ {shouldUseNarrowLayout && {getHeaderButtons()}}
+
+
+
+ );
+}
+
+WorkspaceExpensifyCardListPage.displayName = 'WorkspaceExpensifyCardListPage';
+
+export default WorkspaceExpensifyCardListPage;
diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx
index fde42d795e6c..25be3775964c 100644
--- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx
+++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPage.tsx
@@ -1,177 +1,33 @@
-import {useFocusEffect} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
-import React, {useCallback, useMemo} from 'react';
-import type {ListRenderItemInfo} from 'react-native';
-import {FlatList, View} from 'react-native';
+import React from 'react';
import type {OnyxEntry} from 'react-native-onyx';
-import {useOnyx} from 'react-native-onyx';
-import Button from '@components/Button';
-import HeaderWithBackButton from '@components/HeaderWithBackButton';
-import * as Expensicons from '@components/Icon/Expensicons';
-import * as Illustrations from '@components/Icon/Illustrations';
-import OfflineWithFeedback from '@components/OfflineWithFeedback';
-import {PressableWithoutFeedback} from '@components/Pressable';
-import ScreenWrapper from '@components/ScreenWrapper';
-import useLocalize from '@hooks/useLocalize';
-import useResponsiveLayout from '@hooks/useResponsiveLayout';
-import useThemeStyles from '@hooks/useThemeStyles';
-import localeCompare from '@libs/LocaleCompare';
-import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
-import Navigation from '@navigation/Navigation';
-import type {FullScreenNavigatorParamList} from '@navigation/types';
+import type {FullScreenNavigatorParamList} from '@libs/Navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import CONST from '@src/CONST';
-import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
-import type {Card, WorkspaceCardsList} from '@src/types/onyx';
-import WorkspaceCardListHeader from './WorkspaceCardListHeader';
-import WorkspaceCardListRow from './WorkspaceCardListRow';
+import type {WorkspaceCardsList} from '@src/types/onyx';
+import {isEmptyObject} from '@src/types/utils/EmptyObject';
+import WorkspaceExpensifyCardListPage from './WorkspaceExpensifyCardListPage';
+import WorkspaceExpensifyCardPageEmptyState from './WorkspaceExpensifyCardPageEmptyState';
type WorkspaceExpensifyCardPageProps = StackScreenProps;
-// TODO: remove when Onyx data is available
-const mockedCards: OnyxEntry = {
- test1: {
- // @ts-expect-error TODO: change cardholder to accountID
- cardholder: {accountID: 1, lastName: 'Smith', firstName: 'Bob', displayName: 'Bob Smith'},
- nameValuePairs: {
- unapprovedExpenseLimit: 1000,
- cardTitle: 'Test 1',
- },
- lastFourPAN: '1234',
- },
- test2: {
- // @ts-expect-error TODO: change cardholder to accountID
- cardholder: {accountID: 2, lastName: 'Miller', firstName: 'Alex', displayName: 'Alex Miller'},
- nameValuePairs: {
- unapprovedExpenseLimit: 2000,
- cardTitle: 'Test 2',
- },
- lastFourPAN: '1234',
- },
- test3: {
- // @ts-expect-error TODO: change cardholder to accountID
- cardholder: {accountID: 3, lastName: 'Brown', firstName: 'Kevin', displayName: 'Kevin Brown'},
- nameValuePairs: {
- unapprovedExpenseLimit: 3000,
- cardTitle: 'Test 3',
- },
- lastFourPAN: '1234',
- },
-};
+// TODO: remove when Onyx data is available, and pass the data to 'WorkspaceExpensifyCardListPage' so that we will not make the same 'Onyx' call twice
+const cardsList: OnyxEntry = {};
function WorkspaceExpensifyCardPage({route}: WorkspaceExpensifyCardPageProps) {
- const {shouldUseNarrowLayout} = useResponsiveLayout();
- const {translate} = useLocalize();
- const styles = useThemeStyles();
-
- const policyID = route.params.policyID;
- const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
-
- const policyCurrency = useMemo(() => policy?.outputCurrency ?? CONST.CURRENCY.USD, [policy]);
-
- // TODO: uncomment the code line below to use cardsList data from Onyx when it's supported
+ // const policyID = route.params.policyID ?? '-1';
// const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${policyID}_${CONST.EXPENSIFY_CARD.BANK}`);
- const cardsList = mockedCards;
-
- const fetchExpensifyCards = useCallback(() => {
- // TODO: uncomment when OpenPolicyExpensifyCardsPage API call is supported
- // Policy.openPolicyExpensifyCardsPage(policyID);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [policyID]);
-
- useFocusEffect(fetchExpensifyCards);
-
- const sortedCards = useMemo(
- () =>
- Object.values(cardsList ?? {}).sort((a, b) => {
- // @ts-expect-error TODO: change cardholder to accountID and get personal details with it
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
- const aName = PersonalDetailsUtils.getDisplayNameOrDefault(a.cardholder ?? {});
- // @ts-expect-error TODO: change cardholder to accountID and get personal details with it
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
- const bName = PersonalDetailsUtils.getDisplayNameOrDefault(b.cardholder ?? {});
- return localeCompare(aName, bName);
- }),
- [cardsList],
- );
-
- const getHeaderButtons = () => (
-
- {}} // TODO: add navigation action when card issue flow is implemented (https://github.com/Expensify/App/issues/44309)
- icon={Expensicons.Plus}
- text={translate('workspace.expensifyCard.issueCard')}
- style={shouldUseNarrowLayout && styles.flex1}
- />
- {}} // TODO: add navigation action when settings screen is implemented (https://github.com/Expensify/App/issues/44311)
- icon={Expensicons.Gear}
- text={translate('common.settings')}
- style={shouldUseNarrowLayout && styles.flex1}
- />
-
- );
-
- const renderItem = ({item, index}: ListRenderItemInfo) => (
-
- {}} // TODO: add navigation action when card details screen is implemented (https://github.com/Expensify/App/issues/44325)
- >
- {({hovered}) => (
-
- )}
-
-
- );
return (
-
- Navigation.goBack()}
- >
- {!shouldUseNarrowLayout && getHeaderButtons()}
-
-
- {shouldUseNarrowLayout && {getHeaderButtons()}}
-
-
-
+ {/* After BE will be implemented we will probably want to have ActivityIndicator during fetch for cardsList */}
+ {isEmptyObject(cardsList) && }
+ {!isEmptyObject(cardsList) && }
);
}
diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx
new file mode 100644
index 000000000000..6e04ae6f5d0e
--- /dev/null
+++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardPageEmptyState.tsx
@@ -0,0 +1,68 @@
+import type {StackScreenProps} from '@react-navigation/stack';
+import React from 'react';
+import {View} from 'react-native';
+import FeatureList from '@components/FeatureList';
+import type {FeatureListItem} from '@components/FeatureList';
+import * as Illustrations from '@components/Icon/Illustrations';
+import useLocalize from '@hooks/useLocalize';
+import useResponsiveLayout from '@hooks/useResponsiveLayout';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import type {FullScreenNavigatorParamList} from '@libs/Navigation/types';
+import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
+import CONST from '@src/CONST';
+import type SCREENS from '@src/SCREENS';
+
+const expensifyCardFeatures: FeatureListItem[] = [
+ {
+ icon: Illustrations.MoneyReceipts,
+ translationKey: 'workspace.moreFeatures.expensifyCard.feed.features.cashBack',
+ },
+ {
+ icon: Illustrations.CreditCardsNew,
+ translationKey: 'workspace.moreFeatures.expensifyCard.feed.features.unlimited',
+ },
+ {
+ icon: Illustrations.MoneyWings,
+ translationKey: 'workspace.moreFeatures.expensifyCard.feed.features.spend',
+ },
+];
+type WorkspaceExpensifyCardPageEmptyStateProps = {route: StackScreenProps['route']};
+
+function WorkspaceExpensifyCardPageEmptyState({route}: WorkspaceExpensifyCardPageEmptyStateProps) {
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const theme = useTheme();
+ const {shouldUseNarrowLayout} = useResponsiveLayout();
+
+ return (
+
+
+ {}}
+ illustrationBackgroundColor={theme.fallbackIconColor}
+ illustration={Illustrations.ExpensifyCardIllustration}
+ illustrationStyle={styles.expensifyCardIllustrationContainer}
+ titleStyles={styles.textHeadlineH1}
+ contentPaddingOnLargeScreens={styles.p5}
+ />
+
+
+ );
+}
+
+WorkspaceExpensifyCardPageEmptyState.displayName = 'WorkspaceExpensifyCardPageEmptyState';
+
+export default WorkspaceExpensifyCardPageEmptyState;
diff --git a/src/styles/index.ts b/src/styles/index.ts
index 6e2f95780941..8d43dc7033f3 100644
--- a/src/styles/index.ts
+++ b/src/styles/index.ts
@@ -5016,6 +5016,11 @@ const styles = (theme: ThemeColors) =>
flex: 1,
},
+ expensifyCardIllustrationContainer: {
+ width: 680,
+ height: 220,
+ },
+
computerIllustrationContainer: {
width: 272,
height: 188,