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
3 changes: 2 additions & 1 deletion src/components/EmptyStateComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ function EmptyStateComponent({
headerMedia,
buttonText,
buttonAction,
containerStyles,
title,
subtitle,
headerStyles,
Expand Down Expand Up @@ -77,7 +78,7 @@ function EmptyStateComponent({
}, [headerMedia, headerMediaType, headerContentStyles, videoAspectRatio, styles.emptyStateVideo]);

return (
<ScrollView contentContainerStyle={[styles.emptyStateScrollView, {minHeight: minModalHeight}]}>
<ScrollView contentContainerStyle={[styles.emptyStateScrollView, {minHeight: minModalHeight}, containerStyles]}>
<View style={styles.skeletonBackground}>
<SkeletonComponent
gradientOpacityEnabled
Expand Down
1 change: 1 addition & 0 deletions src/components/EmptyStateComponent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type SharedProps<T> = {
subtitle: string;
buttonText?: string;
buttonAction?: () => void;
containerStyles?: StyleProp<ViewStyle>;
headerStyles?: StyleProp<ViewStyle>;
headerMediaType: T;
headerContentStyles?: StyleProp<ViewStyle & ImageStyle>;
Expand Down
3 changes: 3 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2755,6 +2755,9 @@ export default {
companyCards: {
addCompanyCards: 'Add company cards',
selectCardFeed: 'Select card feed',
assignCard: 'Assign card',
cardNumber: 'Card number',
customFeed: 'Custom feed',
},
expensifyCard: {
issueAndManageCards: 'Issue and manage your Expensify Cards',
Expand Down
3 changes: 3 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2800,6 +2800,9 @@ export default {
companyCards: {
addCompanyCards: 'Agregar tarjetas de empresa',
selectCardFeed: 'Seleccionar feed de tarjetas',
assignCard: 'Asignar tarjeta',
cardNumber: 'Número de la tarjeta',
customFeed: 'Fuente personalizada',
},
expensifyCard: {
issueAndManageCards: 'Emitir y gestionar Tarjetas Expensify',
Expand Down
12 changes: 10 additions & 2 deletions src/pages/workspace/WorkspacePageWithSections.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps &
/** Whether to show the not found page */
shouldShowNotFoundPage?: boolean;

/** Whether to include safe area padding bottom or not */
includeSafeAreaPaddingBottom?: boolean;

/** Makes firstRender ref display loading page before isLoading is change to true */
showLoadingAsFirstRender?: boolean;

/** Policy values needed in the component */
policy: OnyxEntry<Policy>;

Expand Down Expand Up @@ -115,11 +121,13 @@ function WorkspacePageWithSections({
reimbursementAccount = CONST.REIMBURSEMENT_ACCOUNT.DEFAULT_DATA,
route,
shouldUseScrollView = false,
showLoadingAsFirstRender = true,
shouldSkipVBBACall = true,
shouldShowBackButton = false,
user,
shouldShowLoading = true,
shouldShowOfflineIndicatorInWideScreen = false,
includeSafeAreaPaddingBottom = false,
shouldShowNonAdmin = false,
headerContent,
testID,
Expand All @@ -138,7 +146,7 @@ function WorkspacePageWithSections({
const hasVBA = achState === BankAccount.STATE.OPEN;
const content = typeof children === 'function' ? children(hasVBA, policyID, isUsingECard) : children;
const {shouldUseNarrowLayout} = useResponsiveLayout();
const firstRender = useRef(true);
const firstRender = useRef(showLoadingAsFirstRender);
const isFocused = useIsFocused();
const prevPolicy = usePrevious(policy);

Expand Down Expand Up @@ -169,7 +177,7 @@ function WorkspacePageWithSections({

return (
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
includeSafeAreaPaddingBottom={includeSafeAreaPaddingBottom}
shouldEnablePickerAvoiding={false}
shouldEnableMaxHeight
testID={testID ?? WorkspacePageWithSections.displayName}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ function WorkspaceCompanyCardFeedSelectorPage({route}: WorkspaceCompanyCardFeedS
),
}));

const goBack = () => Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID));
const goBack = () => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID));

const selectFeed = (feed: CardFeedListItem) => {
Card.updateSelectedFeed(feed.value, policyID);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ function WorkspaceCompanyCardsFeedAddedEmptyPage() {
SkeletonComponent={TableListItemSkeleton}
headerMediaType={CONST.EMPTY_STATE_MEDIA.ILLUSTRATION}
headerMedia={Illustrations.CompanyCardsEmptyState}
containerStyles={styles.mt4}
headerStyles={[styles.emptyStateCardIllustrationContainer, styles.justifyContentStart, {backgroundColor: colors.blue700}]}
headerContentStyles={styles.emptyStateCardIllustration}
title={translate('workspace.moreFeatures.companyCards.emptyAddedFeedTitle')}
Expand Down
91 changes: 91 additions & 0 deletions src/pages/workspace/companyCards/WorkspaceCompanyCardsList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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 Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
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 WorkspaceCompanyCardsFeedAddedEmptyPage from './WorkspaceCompanyCardsFeedAddedEmptyPage';
import WorkspaceCompanyCardsListRow from './WorkspaceCompanyCardsListRow';

type WorkspaceCompanyCardsListProps = {
/** List of company cards */
cardsList: OnyxEntry<WorkspaceCardsList>;
};

function WorkspaceCompanyCardsList({cardsList}: WorkspaceCompanyCardsListProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
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}
// TODO: navigate to Card Details screen when implemented
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.appBG, styles.justifyContentBetween, styles.mh5, styles.gap5, styles.p4]}>
<Text
numberOfLines={1}
style={[styles.textLabelSupporting, styles.lh16]}
>
{translate('common.name')}
</Text>
<Text
numberOfLines={1}
style={[styles.textLabelSupporting, styles.lh16]}
>
{translate('workspace.companyCards.cardNumber')}
</Text>
</View>
),
[styles, translate],
);

if (sortedCards.length === 0) {
return <WorkspaceCompanyCardsFeedAddedEmptyPage />;
}

return (
<FlatList
contentContainerStyle={styles.flexGrow1}
data={sortedCards}
renderItem={renderItem}
ListHeaderComponent={renderListHeader}
stickyHeaderIndices={[0]}
/>
);
}

export default WorkspaceCompanyCardsList;
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 = {
/** Current policy id */
policyID: string;

/** Currently selected feed */
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] : styles.pb2]}>
<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}>{translate('workspace.companyCards.customFeed')}</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
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS.getRoute(policyID))}
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>
<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;
Loading
Loading