Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NO QA] [3rd Party Feeds] Company Cards List #48176

Merged
Merged
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2753,6 +2753,7 @@ export default {
companyCards: {
addCompanyCards: 'Add company cards',
selectCardFeed: 'Select card feed',
assignCard: 'Assign card',
},
expensifyCard: {
issueAndManageCards: 'Issue and manage your Expensify Cards',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2800,6 +2800,7 @@ export default {
companyCards: {
addCompanyCards: 'Agregar tarjetas de empresa',
selectCardFeed: 'Seleccionar feed de tarjetas',
assignCard: 'Asignar tarjeta',
},
expensifyCard: {
issueAndManageCards: 'Emitir y gestionar Tarjetas Expensify',
Expand Down
102 changes: 102 additions & 0 deletions src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, {useCallback, useMemo} from 'react';
import type {ListRenderItemInfo} from 'react-native';
import {FlatList, View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import {PressableWithFeedback} from '@components/Pressable';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CardUtils from '@libs/CardUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Card, WorkspaceCardsList} from '@src/types/onyx';
import WorkspaceCompanyCardsListHeaderButtons from './WorkspaceCompanyCardsListHeaderButtons';
import WorkspaceCompanyCardsListRow from './WorkspaceCompanyCardsListRow';

type WorkspaceCompanyCardsListProps = {
/** The current policyID */
policyID: string;

/** List of company cards */
cardsList: OnyxEntry<WorkspaceCardsList>;

/** Currently selected feed */
selectedFeed: string;
};

function WorkspaceCompanyCardsList({policyID, cardsList, selectedFeed}: WorkspaceCompanyCardsListProps) {
const styles = useThemeStyles();
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);

const sortedCards = useMemo(() => CardUtils.sortCardsByCardholderName(cardsList, personalDetails), [cardsList, personalDetails]);

const renderItem = useCallback(
({item, index}: ListRenderItemInfo<Card>) => (
<OfflineWithFeedback
key={`${item.nameValuePairs?.cardTitle}_${index}`}
errorRowStyles={styles.ph5}
errors={item.errors}
>
<PressableWithFeedback
role={CONST.ROLE.BUTTON}
style={[styles.mh5, styles.br3, styles.mb3, styles.highlightBG]}
accessibilityLabel="row"
hoverStyle={styles.hoveredComponentBG}
onPress={() => {}}
>
<WorkspaceCompanyCardsListRow
cardholder={personalDetails?.[item.accountID ?? '-1']}
cardNumber={item?.cardNumber ?? ''}
name={item.nameValuePairs?.cardTitle ?? ''}
/>
</PressableWithFeedback>
</OfflineWithFeedback>
),
[personalDetails, styles],
);

const renderListHeader = useCallback(
() => (
<View style={[styles.flexRow, styles.justifyContentBetween, styles.mh5, styles.gap5, styles.p4]}>
<Text
numberOfLines={1}
style={[styles.textLabelSupporting, styles.lh16]}
>
Name
</Text>
<Text
numberOfLines={1}
style={[styles.textLabelSupporting, styles.lh16]}
>
Card number
</Text>
</View>
),
[styles],
);

return (
<ScreenWrapper
shouldEnablePickerAvoiding={false}
shouldShowOfflineIndicatorInWideScreen
shouldEnableMaxHeight
testID={WorkspaceCompanyCardsList.displayName}
>
<WorkspaceCompanyCardsListHeaderButtons
policyID={policyID}
selectedFeed={selectedFeed}
/>
<FlatList
data={sortedCards}
renderItem={renderItem}
ListHeaderComponent={renderListHeader}
/>
</ScreenWrapper>
);
}

WorkspaceCompanyCardsList.displayName = 'WorkspaceCompanyCardsList';

export default WorkspaceCompanyCardsList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import {View} from 'react-native';
import Button from '@components/Button';
import CaretWrapper from '@components/CaretWrapper';
import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import {PressableWithFeedback} from '@components/Pressable';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CardUtils from '@libs/CardUtils';
import Navigation from '@navigation/Navigation';
import variables from '@styles/variables';
import ROUTES from '@src/ROUTES';
import type {CardFeeds} from '@src/types/onyx';

const mockedFeeds = {
companyCardNicknames: {
cdfbmo: 'BMO MasterCard',
},
} as unknown as CardFeeds;

type WorkspaceCompanyCardsListHeaderButtonsProps = {
policyID: string;
selectedFeed: string;
};

function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed}: WorkspaceCompanyCardsListHeaderButtonsProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {shouldUseNarrowLayout} = useResponsiveLayout();
// TODO: use data form onyx instead of mocked one when API is implemented
// const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const cardFeeds = mockedFeeds ?? {};

return (
<View style={[styles.w100, styles.ph5, !shouldUseNarrowLayout && [styles.pv2, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween]]}>
<PressableWithFeedback
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.getRoute(policyID))}
style={[styles.flexRow, styles.alignItemsCenter, styles.gap3, styles.ml4, shouldUseNarrowLayout && styles.mb3]}
accessibilityLabel={cardFeeds?.companyCardNicknames?.[selectedFeed]}
>
<Icon
src={CardUtils.getCardFeedIcon(selectedFeed)}
width={variables.iconSizeExtraLarge}
height={variables.iconSizeExtraLarge}
/>
<View>
<CaretWrapper>
<Text style={styles.textStrong}>{cardFeeds?.companyCardNicknames?.[selectedFeed]}</Text>
</CaretWrapper>
<Text style={styles.textLabelSupporting}>Custom feed</Text>
</View>
</PressableWithFeedback>

<View style={[styles.flexRow, styles.gap2]}>
<Button
medium
success
// TODO: navigate to Assign card flow when it's implemented
onPress={() => {}}
icon={Expensicons.Plus}
text={translate('workspace.companyCards.assignCard')}
style={shouldUseNarrowLayout && styles.flex1}
/>
<Button
medium
// TODO: navigate to Settings screen when it's implemented
onPress={() => {}}
icon={Expensicons.Gear}
text={translate('common.settings')}
style={shouldUseNarrowLayout && styles.flex1}
/>
</View>
</View>
);
}

WorkspaceCompanyCardsListHeaderButtons.displayName = 'WorkspaceCompanyCardsListHeaderButtons';

export default WorkspaceCompanyCardsListHeaderButtons;
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import React, {useMemo} from 'react';
import {View} from 'react-native';
import Avatar from '@components/Avatar';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import {getDefaultAvatarURL} from '@libs/UserUtils';
import CONST from '@src/CONST';
import type {PersonalDetails} from '@src/types/onyx';

type WorkspaceCompanyCardsListRowProps = {
/** Card number */
cardNumber: string;

/** Card name */
name: string;

/** Cardholder personal details */
cardholder?: PersonalDetails | null;
};

function WorkspaceCompanyCardsListRow({cardholder, name, cardNumber}: WorkspaceCompanyCardsListRowProps) {
const styles = useThemeStyles();
const cardholderName = useMemo(() => PersonalDetailsUtils.getDisplayNameOrDefault(cardholder), [cardholder]);

return (
<View style={[styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.br3, styles.p4]}>
<View style={[styles.flexRow, styles.gap3, styles.alignItemsCenter]}>
<Avatar
source={getDefaultAvatarURL(cardholder?.accountID)}
avatarID={cardholder?.accountID}
type={CONST.ICON_TYPE_AVATAR}
size={CONST.AVATAR_SIZE.DEFAULT}
/>
<View style={styles.flex1}>
<Text
numberOfLines={1}
style={[styles.optionDisplayName, styles.textStrong, styles.pre]}
>
{cardholderName}
</Text>
<Text
numberOfLines={1}
style={[styles.textLabelSupporting, styles.lh16]}
>
{name}
</Text>
</View>
</View>
<Text
numberOfLines={1}
style={[styles.textLabelSupporting, styles.lh16]}
>
{cardNumber}
</Text>
</View>
);
}

WorkspaceCompanyCardsListRow.displayName = 'WorkspaceCompanyCardsListRow';

export default WorkspaceCompanyCardsListRow;
63 changes: 56 additions & 7 deletions src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,71 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import * as Illustrations from '@components/Icon/Illustrations';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import type {FullScreenNavigatorParamList} from '@libs/Navigation/types';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import type {CardFeeds, WorkspaceCardsList} from '@src/types/onyx';
import WorkspaceCompanyCardsList from './WorkspaceCompanyCardsList';

const mockedFeeds: CardFeeds = {
companyCards: {
cdfbmo: {
pending: false,
asrEnabled: true,
forceReimbursable: 'force_no',
liabilityType: 'corporate',
preferredPolicy: '',
reportTitleFormat: '{report:card}{report:bank}{report:submit:from}{report:total}{report:enddate:MMMM}',
statementPeriodEndDay: 'LAST_DAY_OF_MONTH',
},
},
companyCardNicknames: {
cdfbmo: 'BMO MasterCard',
},
};

const mockedCards = {
id1: {
accountID: 885646,
nameValuePairs: {
cardTitle: 'Test 1',
},
cardNumber: '1234 56XX XXXX 1222',
},
id2: {
accountID: 885646,
nameValuePairs: {
cardTitle: 'Test 2',
},
cardNumber: '1234 56XX XXXX 1222',
},
} as unknown as WorkspaceCardsList;

type WorkspaceCompanyCardPageProps = StackScreenProps<FullScreenNavigatorParamList, typeof SCREENS.WORKSPACE.COMPANY_CARDS>;

function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) {
const policyID = route.params.policyID;
const {translate} = useLocalize();
const styles = useThemeStyles();
const {shouldUseNarrowLayout} = useResponsiveLayout();

// TODO: use data form onyx instead of mocked one when API is implemented
// const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const cardFeeds = mockedFeeds;
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
const defaultFeed = Object.keys(cardFeeds?.companyCards ?? {})[0];
const selectedFeed = lastSelectedFeed ?? defaultFeed;

// TODO: use data form onyx instead of mocked one when API is implemented
// const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`);
const cardsList = mockedCards ?? {};

return (
<AccessOrNotFoundWrapper
policyID={route.params.policyID}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_COMPANY_CARDS_ENABLED}
>
<WorkspacePageWithSections
Expand All @@ -31,7 +76,11 @@ function WorkspaceCompanyCardPage({route}: WorkspaceCompanyCardPageProps) {
guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_COMPANY_CARDS}
shouldShowOfflineIndicatorInWideScreen
>
<View style={[styles.mt3, shouldUseNarrowLayout ? styles.workspaceSectionMobile : styles.workspaceSection]} />
<WorkspaceCompanyCardsList
cardsList={cardsList}
policyID={policyID}
selectedFeed={selectedFeed}
/>
</WorkspacePageWithSections>
</AccessOrNotFoundWrapper>
);
Expand Down
3 changes: 3 additions & 0 deletions src/types/onyx/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type Card = {
/** Last four Primary Account Number digits */
lastFourPAN?: string;

/** Card number */
cardNumber?: string;

/** Current fraud state of the card */
fraud: ValueOf<typeof CONST.EXPENSIFY_CARD.FRAUD_TYPES>;

Expand Down
Loading