diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 1e99e2132203..7d9c552b940d 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -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, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 8a71030dff44..ad06ad33b319 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -341,6 +341,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', diff --git a/src/languages/en.ts b/src/languages/en.ts index 8761366685ee..dbd5231a4699 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2636,6 +2636,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: @@ -2648,6 +2649,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', diff --git a/src/languages/es.ts b/src/languages/es.ts index 75d23c8691e3..850e71a98c3b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2684,6 +2684,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: @@ -2696,6 +2697,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', diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 6f80a8a20a6b..1f90513ad29d 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -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'; @@ -143,6 +145,19 @@ function getMCardNumberString(cardNumber: string): string { return cardNumber.replace(/\s/g, ''); } +function getTranslationKeyForLimitType(limitType: ValueOf | 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, @@ -155,4 +170,5 @@ export { findPhysicalCard, hasDetectedFraud, getMCardNumberString, + getTranslationKeyForLimitType, }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 7a6701de1a2d..7e2df2ee27af 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -409,6 +409,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/card/issueNew/IssueNewCardPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts').default, + [SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage').default, [SCREENS.SETTINGS.SAVE_THE_WORLD]: () => require('../../../../pages/TeachersUnite/SaveTheWorldPage').default, [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_PAYMENT_CURRENCY]: () => require('../../../../pages/settings/PaymentCard/ChangeCurrency').default, [SCREENS.SETTINGS.SUBSCRIPTION.CHANGE_BILLING_CURRENCY]: () => require('../../../../pages/settings/Subscription/PaymentCard/ChangeBillingCurrency').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 2b0f6f763262..cb7f4b861e86 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -160,7 +160,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { 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; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index b8ee32576365..381d1fd862c5 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -467,6 +467,9 @@ const config: LinkingOptions['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, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 82a5be980a1d..d25682cd6acd 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -653,6 +653,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 = { diff --git a/src/pages/workspace/card/issueNew/ConfirmationStep.tsx b/src/pages/workspace/card/issueNew/ConfirmationStep.tsx index 35f9fab6598f..787e564676f4 100644 --- a/src/pages/workspace/card/issueNew/ConfirmationStep.tsx +++ b/src/pages/workspace/card/issueNew/ConfirmationStep.tsx @@ -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'; @@ -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(); diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx new file mode 100644 index 000000000000..6934776e44ba --- /dev/null +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx @@ -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; + +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 ( + + + {({safeAreaPaddingBottomStyle}) => ( + <> + Navigation.goBack(backTo)} + /> + + + + + + + + + + {}} // TODO: navigate to Edit card limit page https://github.com/Expensify/App/issues/44326 + /> + {}} // TODO: navigate to Edit limit type page https://github.com/Expensify/App/issues/44328 + /> + {}} // TODO: navigate to Edit card name page https://github.com/Expensify/App/issues/44327 + /> + {}} // TODO: create Deactivate card logic https://github.com/Expensify/App/issues/44320 + /> + + + )} + + + ); +} + +WorkspaceExpensifyCardDetailsPage.displayName = 'WorkspaceExpensifyCardDetailsPage'; + +export default WorkspaceExpensifyCardDetailsPage; diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx index 91a5f3b8f3f1..32d0b5136842 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardListPage.tsx @@ -34,6 +34,7 @@ const mockedCards: OnyxEntry = { 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', @@ -43,6 +44,7 @@ const mockedCards: OnyxEntry = { 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', @@ -52,6 +54,7 @@ const mockedCards: OnyxEntry = { 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', @@ -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()))} > alignItems: 'center', }, + cardBadge: { + position: 'absolute', + top: 20, + left: 16, + marginLeft: 0, + paddingHorizontal: 8, + minHeight: 20, + borderColor: colors.productDark500, + }, + environmentBadge: { minHeight: 12, borderRadius: 14, @@ -951,6 +961,11 @@ const styles = (theme: ThemeColors) => ...whiteSpace.noWrap, }, + cardBadgeText: { + color: colors.white, + fontSize: variables.fontSizeExtraSmall, + }, + activeItemBadge: { borderColor: theme.buttonHoveredBG, }, diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index cdaaddd7dce2..eb584fa73ab0 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -39,6 +39,9 @@ type Card = { /** Type of card spending limits */ limitType?: ValueOf; + /** Card spending limit */ + limit?: number; + /** User-defined nickname for the card */ cardTitle?: string;