Skip to content

Commit

Permalink
Merge pull request #44770 from narefyev91/workspace-expensify-card
Browse files Browse the repository at this point in the history
[No QA]: Workspace Feed - Initial card page
  • Loading branch information
MariaHCD authored Jul 4, 2024
2 parents a84f68e + a8dc61f commit 2784712
Show file tree
Hide file tree
Showing 9 changed files with 770 additions and 159 deletions.
487 changes: 487 additions & 0 deletions assets/images/expensifyCard/cardIllustration.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions src/components/Icon/Illustrations.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -176,6 +177,7 @@ export {
Binoculars,
CompanyCard,
ReceiptUpload,
ExpensifyCardIllustration,
SplitBill,
PiggyBank,
Accounting,
Expand Down
10 changes: 10 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2401,6 +2401,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',
Expand Down
10 changes: 10 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2439,6 +2439,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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ type Screens = Partial<Record<keyof FullScreenNavigatorParamList, () => React.Co
const CENTRAL_PANE_WORKSPACE_SCREENS = {
[SCREENS.WORKSPACE.PROFILE]: () => require<ReactComponentModule>('../../../../pages/workspace/WorkspaceProfilePage').default,
[SCREENS.WORKSPACE.CARD]: () => require<ReactComponentModule>('../../../../pages/workspace/card/WorkspaceCardPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default,
[SCREENS.WORKSPACE.WORKFLOWS]: () => require<ReactComponentModule>('../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default,
[SCREENS.WORKSPACE.REIMBURSE]: () => require<ReactComponentModule>('../../../../pages/workspace/reimburse/WorkspaceReimbursePage').default,
[SCREENS.WORKSPACE.BILLS]: () => require<ReactComponentModule>('../../../../pages/workspace/bills/WorkspaceBillsPage').default,
Expand All @@ -32,6 +31,7 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = {
[SCREENS.WORKSPACE.TAGS]: () => require<ReactComponentModule>('../../../../pages/workspace/tags/WorkspaceTagsPage').default,
[SCREENS.WORKSPACE.TAXES]: () => require<ReactComponentModule>('../../../../pages/workspace/taxes/WorkspaceTaxesPage').default,
[SCREENS.WORKSPACE.REPORT_FIELDS]: () => require<ReactComponentModule>('../../../../pages/workspace/reportFields/WorkspaceReportFieldsPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default,
[SCREENS.WORKSPACE.DISTANCE_RATES]: () => require<ReactComponentModule>('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default,
} satisfies Screens;

Expand Down
173 changes: 173 additions & 0 deletions src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx
Original file line number Diff line number Diff line change
@@ -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<FullScreenNavigatorParamList, typeof SCREENS.WORKSPACE.EXPENSIFY_CARD>['route']};

// TODO: remove this const altogether and take the card data from component prop when Onyx data is available
const mockedCards: OnyxEntry<WorkspaceCardsList> = {
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 = () => (
<View style={[styles.w100, styles.flexRow, styles.gap2, shouldUseNarrowLayout && styles.mb3]}>
<Button
medium
success
onPress={() => {}} // 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}
/>
<Button
medium
onPress={() => {}} // 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}
/>
</View>
);

const renderItem = ({item, index}: ListRenderItemInfo<Card>) => (
<OfflineWithFeedback
key={`${item.nameValuePairs?.cardTitle}_${index}`}
errorRowStyles={styles.ph5}
errors={item.errors}
>
<PressableWithoutFeedback
role={CONST.ROLE.BUTTON}
accessibilityLabel="row"
onPress={() => {}} // TODO: add navigation action when card details screen is implemented (https://github.com/Expensify/App/issues/44325)
>
{({hovered}) => (
<WorkspaceCardListRow
style={hovered && styles.hoveredComponentBG}
lastFourPAN={item.lastFourPAN ?? ''}
// @ts-expect-error TODO: change cardholder to accountID and get personal details with it
cardholder={item.cardholder}
limit={item.nameValuePairs?.unapprovedExpenseLimit ?? 0}
name={item.nameValuePairs?.cardTitle ?? ''}
currency={policyCurrency}
/>
)}
</PressableWithoutFeedback>
</OfflineWithFeedback>
);

return (
<ScreenWrapper
shouldEnablePickerAvoiding={false}
shouldShowOfflineIndicatorInWideScreen
shouldEnableMaxHeight
testID={WorkspaceExpensifyCardListPage.displayName}
>
<HeaderWithBackButton
icon={Illustrations.HandCard}
title={translate('workspace.common.expensifyCard')}
shouldShowBackButton={shouldUseNarrowLayout}
onBackButtonPress={() => Navigation.goBack()}
>
{!shouldUseNarrowLayout && getHeaderButtons()}
</HeaderWithBackButton>

{shouldUseNarrowLayout && <View style={[styles.pl5, styles.pr5]}>{getHeaderButtons()}</View>}

<FlatList
data={sortedCards}
renderItem={renderItem}
ListHeaderComponent={WorkspaceCardListHeader}
/>
</ScreenWrapper>
);
}

WorkspaceExpensifyCardListPage.displayName = 'WorkspaceExpensifyCardListPage';

export default WorkspaceExpensifyCardListPage;
Loading

0 comments on commit 2784712

Please sign in to comment.