Skip to content

Commit

Permalink
Merge pull request #45155 from VickyStash/feature/44325-card-details-…
Browse files Browse the repository at this point in the history
…page

[No QA] Create new Card Details page
  • Loading branch information
mountiny authored Jul 15, 2024
2 parents 33df500 + 22f6876 commit cf34d87
Show file tree
Hide file tree
Showing 14 changed files with 213 additions and 15 deletions.
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/expensify-card',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card` as const,
},
WORKSPACE_EXPENSIFY_CARD_DETAILS: {
route: 'settings/workspaces/:policyID/expensify-card/:cardID',
getRoute: (policyID: string, cardID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/${cardID}`, backTo),
},
WORKSPACE_EXPENSIFY_CARD_ISSUE_NEW: {
route: 'settings/workspaces/:policyID/expensify-card/issue-new',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/issue-new` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ const SCREENS = {
RATE_AND_UNIT_RATE: 'Workspace_RateAndUnit_Rate',
RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit',
EXPENSIFY_CARD: 'Workspace_ExpensifyCard',
EXPENSIFY_CARD_DETAILS: 'Workspace_ExpensifyCard_Details',
EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New',
EXPENSIFY_CARD_BANK_ACCOUNT: 'Workspace_ExpensifyCard_BankAccount',
BILLS: 'Workspace_Bills',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2641,6 +2641,7 @@ export default {
limit: 'Limit',
currentBalance: 'Current balance',
currentBalanceDescription: 'Current balance is the sum of all posted Expensify Card transactions that have occurred since the last settlement date.',
cardLimit: 'Card limit',
remainingLimit: 'Remaining limit',
requestLimitIncrease: 'Request limit increase',
remainingLimitDescription:
Expand All @@ -2653,6 +2654,10 @@ export default {
chooseExistingBank: 'Choose an existing business bank account to pay your Expensify Card balance, or add a new bank account',
accountEndingIn: 'Account ending in',
addNewBankAccount: 'Add a new bank account',
cardDetails: 'Card details',
virtual: 'Virtual',
physical: 'Physical',
deactivate: 'Deactivate card',
},
categories: {
deleteCategories: 'Delete categories',
Expand Down
5 changes: 5 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2690,6 +2690,7 @@ export default {
currentBalance: 'Saldo actual',
currentBalanceDescription:
'El saldo actual es la suma de todas las transacciones contabilizadas con la Tarjeta Expensify que se han producido desde la última fecha de liquidación.',
cardLimit: 'Límite de la tarjeta',
remainingLimit: 'Límite restante',
requestLimitIncrease: 'Solicitar aumento de límite',
remainingLimitDescription:
Expand All @@ -2702,6 +2703,10 @@ export default {
chooseExistingBank: 'Elige una cuenta bancaria comercial existente para pagar el saldo de su Tarjeta Expensify o añade una nueva cuenta bancaria.',
accountEndingIn: 'Cuenta terminada en',
addNewBankAccount: 'Añadir nueva cuenta bancaria',
cardDetails: 'Datos de la tarjeta',
virtual: 'Virtual',
physical: 'Física',
deactivate: 'Desactivar tarjeta',
},
categories: {
deleteCategories: 'Eliminar categorías',
Expand Down
16 changes: 16 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import lodash from 'lodash';
import Onyx from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {OnyxValues} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Card, CardList} from '@src/types/onyx';
Expand Down Expand Up @@ -143,6 +145,19 @@ function getMCardNumberString(cardNumber: string): string {
return cardNumber.replace(/\s/g, '');
}

function getTranslationKeyForLimitType(limitType: ValueOf<typeof CONST.EXPENSIFY_CARD.LIMIT_TYPES> | undefined): TranslationPaths | '' {
switch (limitType) {
case CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART:
return 'workspace.card.issueNewCard.smartLimit';
case CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED:
return 'workspace.card.issueNewCard.fixedAmount';
case CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY:
return 'workspace.card.issueNewCard.monthly';
default:
return '';
}
}

export {
isExpensifyCard,
isCorporateCard,
Expand All @@ -155,4 +170,5 @@ export {
findPhysicalCard,
hasDetectedFraud,
getMCardNumberString,
getTranslationKeyForLimitType,
};
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.TAX_CREATE]: () => require<ReactComponentModule>('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require<ReactComponentModule>('../../../../pages/workspace/card/issueNew/IssueNewCardPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage').default,
[SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require<ReactComponentModule>('../../../../pages/TeachersUnite/SaveTheWorldPage').default,
[SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: () => require<ReactComponentModule>('../../../../pages/settings/PaymentCard/ChangeCurrency').default,
[SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: () => require<ReactComponentModule>('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial<Record<FullScreenName, string[]>> = {
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE,
SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE,
],
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT],
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT, SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS],
};

export default FULL_SCREEN_TO_RHP_MAPPING;
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,9 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT.route,
},
[SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.route,
},
[SCREENS.WORKSPACE.RATE_AND_UNIT]: {
path: ROUTES.WORKSPACE_RATE_AND_UNIT.route,
},
Expand Down
5 changes: 5 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -656,6 +656,11 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: {
policyID: string;
};
[SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: {
policyID: string;
cardID: string;
backTo?: Routes;
};
} & ReimbursementAccountNavigatorParamList;

type NewChatNavigatorParamList = {
Expand Down
14 changes: 1 addition & 13 deletions src/pages/workspace/card/issueNew/ConfirmationStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useThemeStyles from '@hooks/useThemeStyles';
import {getTranslationKeyForLimitType} from '@libs/CardUtils';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import Navigation from '@navigation/Navigation';
Expand All @@ -19,19 +20,6 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {IssueNewCardStep} from '@src/types/onyx/Card';

function getTranslationKeyForLimitType(limitType: string | undefined) {
switch (limitType) {
case CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART:
return 'workspace.card.issueNewCard.smartLimit';
case CONST.EXPENSIFY_CARD.LIMIT_TYPES.FIXED:
return 'workspace.card.issueNewCard.fixedAmount';
case CONST.EXPENSIFY_CARD.LIMIT_TYPES.MONTHLY:
return 'workspace.card.issueNewCard.monthly';
default:
return '';
}
}

function ConfirmationStep() {
const {translate} = useLocalize();
const styles = useThemeStyles();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import type {StackScreenProps} from '@react-navigation/stack';
import React from 'react';
import {View} from 'react-native';
import {useOnyx} from 'react-native-onyx';
import ExpensifyCardImage from '@assets/images/expensify-card.svg';
import Badge from '@components/Badge';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import {FallbackAvatar} from '@components/Icon/Expensicons';
import * as Expensicons from '@components/Icon/Expensicons';
import ImageSVG from '@components/ImageSVG';
import MenuItem from '@components/MenuItem';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import useLocalize from '@hooks/useLocalize';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import * as CardUtils from '@libs/CardUtils';
import * as CurrencyUtils from '@libs/CurrencyUtils';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import Navigation from '@navigation/Navigation';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';

// TODO: remove when Onyx data is available
const mockedCard = {
accountID: 885646,
availableSpend: 1000,
nameValuePairs: {
cardTitle: 'Test 1',
isVirtual: true,
limit: 2000,
limitType: CONST.EXPENSIFY_CARD.LIMIT_TYPES.SMART,
},
lastFourPAN: '1234',
};

type WorkspaceExpensifyCardDetailsPageProps = StackScreenProps<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS>;

function WorkspaceExpensifyCardDetailsPage({route}: WorkspaceExpensifyCardDetailsPageProps) {
const {policyID, cardID, backTo} = route.params;

const {translate} = useLocalize();
const styles = useThemeStyles();
const theme = useTheme();

const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST);
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${policyID}_${CONST.EXPENSIFY_CARD.BANK}`);

const card = cardsList?.[cardID] ?? mockedCard;
const cardholder = personalDetails?.[card.accountID ?? -1];
const isVirtual = !!card.nameValuePairs?.isVirtual;
const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(card.availableSpend);
const formattedLimit = CurrencyUtils.convertToDisplayString(card.nameValuePairs?.limit);
const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(cardholder);
const translationForLimitType = CardUtils.getTranslationKeyForLimitType(card.nameValuePairs?.limitType);

return (
<AccessOrNotFoundWrapper
accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]}
policyID={policyID}
featureName={CONST.POLICY.MORE_FEATURES.ARE_EXPENSIFY_CARDS_ENABLED}
>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={WorkspaceExpensifyCardDetailsPage.displayName}
>
{({safeAreaPaddingBottomStyle}) => (
<>
<HeaderWithBackButton
title={translate('workspace.expensifyCard.cardDetails')}
onBackButtonPress={() => Navigation.goBack(backTo)}
/>
<ScrollView contentContainerStyle={safeAreaPaddingBottomStyle}>
<View style={[styles.walletCard, styles.mb3]}>
<ImageSVG
contentFit="contain"
src={ExpensifyCardImage}
pointerEvents="none"
height={variables.cardPreviewHeight}
width={variables.cardPreviewWidth}
/>
<Badge
badgeStyles={styles.cardBadge}
textStyles={styles.cardBadgeText}
text={translate(isVirtual ? 'workspace.expensifyCard.virtual' : 'workspace.expensifyCard.physical')}
/>
</View>

<MenuItem
label={translate('workspace.card.issueNewCard.cardholder')}
title={displayName}
icon={cardholder?.avatar ?? FallbackAvatar}
iconType={CONST.ICON_TYPE_AVATAR}
description={cardholder?.login}
interactive={false}
/>
<MenuItemWithTopDescription
description={translate(isVirtual ? 'cardPage.virtualCardNumber' : 'cardPage.physicalCardNumber')}
title={CardUtils.maskCard(card.lastFourPAN)}
interactive={false}
titleStyle={styles.walletCardNumber}
/>
<MenuItemWithTopDescription
description={translate('cardPage.availableSpend')}
title={formattedAvailableSpendAmount}
interactive={false}
titleStyle={styles.newKansasLarge}
/>
<MenuItemWithTopDescription
description={translate('workspace.expensifyCard.cardLimit')}
title={formattedLimit}
shouldShowRightIcon
onPress={() => {}} // TODO: navigate to Edit card limit page https://github.com/Expensify/App/issues/44326
/>
<MenuItemWithTopDescription
description={translate('workspace.card.issueNewCard.limitType')}
title={translationForLimitType ? translate(translationForLimitType) : ''}
shouldShowRightIcon
onPress={() => {}} // TODO: navigate to Edit limit type page https://github.com/Expensify/App/issues/44328
/>
<MenuItemWithTopDescription
description={translate('workspace.card.issueNewCard.cardName')}
title={card.nameValuePairs?.cardTitle}
shouldShowRightIcon
onPress={() => {}} // TODO: navigate to Edit card name page https://github.com/Expensify/App/issues/44327
/>
<MenuItem
icon={Expensicons.Trashcan}
iconFill={theme.icon}
title={translate('workspace.expensifyCard.deactivate')}
style={styles.mv1}
onPress={() => {}} // TODO: create Deactivate card logic https://github.com/Expensify/App/issues/44320
/>
</ScrollView>
</>
)}
</ScreenWrapper>
</AccessOrNotFoundWrapper>
);
}

WorkspaceExpensifyCardDetailsPage.displayName = 'WorkspaceExpensifyCardDetailsPage';

export default WorkspaceExpensifyCardDetailsPage;
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const mockedCards: OnyxEntry<WorkspaceCardsList> = {
test1: {
// @ts-expect-error TODO: change cardholder to accountID
cardholder: {accountID: 1, lastName: 'Smith', firstName: 'Bob', displayName: 'Bob Smith'},
cardID: 1,
nameValuePairs: {
unapprovedExpenseLimit: 1000,
cardTitle: 'Test 1',
Expand All @@ -43,6 +44,7 @@ const mockedCards: OnyxEntry<WorkspaceCardsList> = {
test2: {
// @ts-expect-error TODO: change cardholder to accountID
cardholder: {accountID: 2, lastName: 'Miller', firstName: 'Alex', displayName: 'Alex Miller'},
cardID: 2,
nameValuePairs: {
unapprovedExpenseLimit: 2000,
cardTitle: 'Test 2',
Expand All @@ -52,6 +54,7 @@ const mockedCards: OnyxEntry<WorkspaceCardsList> = {
test3: {
// @ts-expect-error TODO: change cardholder to accountID
cardholder: {accountID: 3, lastName: 'Brown', firstName: 'Kevin', displayName: 'Kevin Brown'},
cardID: 3,
nameValuePairs: {
unapprovedExpenseLimit: 3000,
cardTitle: 'Test 3',
Expand Down Expand Up @@ -127,7 +130,7 @@ function WorkspaceExpensifyCardListPage({route}: WorkspaceExpensifyCardListPageP
style={[styles.mh5, styles.br3, styles.mb3, styles.highlightBG]}
accessibilityLabel="row"
hoverStyle={[styles.hoveredComponentBG]}
onPress={() => {}} // TODO: add navigation action when card details screen is implemented (https://github.com/Expensify/App/issues/44325)
onPress={() => Navigation.navigate(ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.getRoute(policyID, item.cardID.toString()))}
>
<WorkspaceCardListRow
lastFourPAN={item.lastFourPAN ?? ''}
Expand Down
15 changes: 15 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -886,6 +886,16 @@ const styles = (theme: ThemeColors) =>
alignItems: 'center',
},

cardBadge: {
position: 'absolute',
top: 20,
left: 16,
marginLeft: 0,
paddingHorizontal: 8,
minHeight: 20,
borderColor: colors.productDark500,
},

environmentBadge: {
minHeight: 12,
borderRadius: 14,
Expand Down Expand Up @@ -951,6 +961,11 @@ const styles = (theme: ThemeColors) =>
...whiteSpace.noWrap,
},

cardBadgeText: {
color: colors.white,
fontSize: variables.fontSizeExtraSmall,
},

activeItemBadge: {
borderColor: theme.buttonHoveredBG,
},
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 @@ -39,6 +39,9 @@ type Card = {
/** Type of card spending limits */
limitType?: ValueOf<typeof CONST.EXPENSIFY_CARD.LIMIT_TYPES>;

/** Card spending limit */
limit?: number;

/** User-defined nickname for the card */
cardTitle?: string;

Expand Down

0 comments on commit cf34d87

Please sign in to comment.