From 5823d37204ea10f8b3eae4081fe9ea39fe8e8419 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 6 Sep 2023 23:12:50 +0200 Subject: [PATCH 01/25] create the Expensify card page --- assets/images/expensify-card.svg | 69 ++++++++++++++++++ src/components/CardPreview.js | 71 +++++++++++++++++++ src/languages/en.ts | 4 ++ src/languages/es.ts | 4 ++ .../AppNavigator/ModalStackNavigators.js | 7 ++ .../settings/Wallet/ExpensifyCardPage.js | 60 ++++++++++++++++ src/styles/styles.js | 15 ++++ 7 files changed, 230 insertions(+) create mode 100644 assets/images/expensify-card.svg create mode 100644 src/components/CardPreview.js create mode 100644 src/pages/settings/Wallet/ExpensifyCardPage.js diff --git a/assets/images/expensify-card.svg b/assets/images/expensify-card.svg new file mode 100644 index 000000000000..f95e3ed20288 --- /dev/null +++ b/assets/images/expensify-card.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/CardPreview.js b/src/components/CardPreview.js new file mode 100644 index 000000000000..31549f8dc6d1 --- /dev/null +++ b/src/components/CardPreview.js @@ -0,0 +1,71 @@ +import React from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import PropTypes from 'prop-types'; + +import styles from '../styles/styles'; +import Text from './Text'; +import usePrivatePersonalDetails from '../hooks/usePrivatePersonalDetails'; +import ONYXKEYS from '../ONYXKEYS'; + +import ExpensifyCardImage from '../../assets/images/expensify-card.svg'; + +const propTypes = { + /** User's private personal details */ + privatePersonalDetails: PropTypes.shape({ + legalFirstName: PropTypes.string, + legalLastName: PropTypes.string, + }), + session: PropTypes.shape({ + /** Currently logged-in user email */ + email: PropTypes.string, + }), +}; + +const defaultProps = { + privatePersonalDetails: { + legalFirstName: '', + legalLastName: '', + }, + session: { + email: '', + }, +}; + +function CardPreview(props) { + usePrivatePersonalDetails(); + const legalFirstName = props.privatePersonalDetails.legalFirstName || ''; + const legalLastName = props.privatePersonalDetails.legalLastName || ''; + + const cardHolder = legalFirstName && legalLastName ? `${legalFirstName} ${legalLastName}` : props.session.email; + + return ( + + + + {cardHolder} + + + ); +} + +CardPreview.propTypes = propTypes; +CardPreview.defaultProps = defaultProps; +CardPreview.displayName = CardPreview; + +export default withOnyx({ + privatePersonalDetails: { + key: ONYXKEYS.PRIVATE_PERSONAL_DETAILS, + }, + session: { + key: ONYXKEYS.SESSION, + }, +})(CardPreview); diff --git a/src/languages/en.ts b/src/languages/en.ts index c31e39319a98..006d2e1e9efa 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -806,6 +806,10 @@ export default { }, addBankAccountFailure: 'An unexpected error occurred while trying to add your bank account. Please try again.', }, + cardPage: { + expensifyCard: 'Expensify Card', + availableSpend: 'Remaining spending power', + }, transferAmountPage: { transfer: ({amount}: TransferParams) => `Transfer${amount ? ` ${amount}` : ''}`, instant: 'Instant (Debit card)', diff --git a/src/languages/es.ts b/src/languages/es.ts index d83104ff85e0..87d231b8e693 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -801,6 +801,10 @@ export default { }, addBankAccountFailure: 'Ocurrió un error inesperado al intentar añadir la cuenta bancaria. Inténtalo de nuevo.', }, + cardPage: { + expensifyCard: 'Tarjeta Expensify', + availableSpend: 'Poder de gasto restante', + }, transferAmountPage: { transfer: ({amount}: TransferParams) => `Transferir${amount ? ` ${amount}` : ''}`, instant: 'Instante', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index c5bb02354641..65439420811c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -536,6 +536,13 @@ const SettingsModalStackNavigator = createModalStackNavigator([ }, name: 'Settings_Wallet', }, + { + getComponent: () => { + const SettingsWalletExpensifyCardPage = require('../../../pages/settings/Wallet/ExpensifyCardPage').default; + return SettingsWalletExpensifyCardPage; + }, + name: 'Settings_Wallet_Expensify_Card', + }, { getComponent: () => { const TransferBalancePage = require('../../../pages/settings/Wallet/TransferBalancePage').default; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js new file mode 100644 index 000000000000..57b13cdb501f --- /dev/null +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -0,0 +1,60 @@ +import React from 'react'; +import {View, ScrollView} from 'react-native'; +import PropTypes from 'prop-types'; +import ROUTES from '../../../ROUTES'; +import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; +import ScreenWrapper from '../../../components/ScreenWrapper'; +import Navigation from '../../../libs/Navigation/Navigation'; +import styles from '../../../styles/styles'; +import * as CurrencyUtils from '../../../libs/CurrencyUtils'; +import MenuItemWithTopDescription from '../../../components/MenuItemWithTopDescription'; + +import CardPreview from '../../../components/CardPreview'; +import useLocalize from '../../../hooks/useLocalize'; + +const propTypes = { + /* Onyx Props */ + + spendLimit: PropTypes.number, +}; + +const defaultProps = { + spendLimit: 0, +}; + +function ExpensifyCardPage(props) { + const {translate} = useLocalize(); + + const formattedSpendLimitAmount = CurrencyUtils.convertToDisplayString(props.spendLimit); + + return ( + + {({safeAreaPaddingBottomStyle}) => ( + <> + Navigation.goBack(ROUTES.SETTINGS_WALLET)} + /> + + + + + + + + + )} + + ); +} + +ExpensifyCardPage.propTypes = propTypes; +ExpensifyCardPage.defaultProps = defaultProps; +ExpensifyCardPage.displayName = ExpensifyCardPage; + +export default ExpensifyCardPage; diff --git a/src/styles/styles.js b/src/styles/styles.js index ef69eed9c6b6..692bf5db51bb 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3986,6 +3986,21 @@ const styles = (theme) => ({ height: 30, width: '100%', }, + + + walletCard: { + borderRadius: variables.componentBorderRadiusLarge, + position: 'relative', + alignSelf: 'center', + overflow: 'hidden', + }, + + walletCardHolder: { + position: 'absolute', + left: 16, + bottom: 16, + width: 160, + }, }); // For now we need to export the styles function that takes the theme as an argument From 453e7470004143160c25b22d496452572db106e7 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 8 Sep 2023 17:09:02 +0100 Subject: [PATCH 02/25] feat(expensify card): add screen routing --- src/ROUTES.ts | 2 ++ src/libs/Navigation/AppNavigator/ModalStackNavigators.js | 2 +- src/libs/Navigation/linkingConfig.js | 4 ++++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 9459708c893b..c4ed1d625dc4 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -48,6 +48,8 @@ export default { SETTINGS_ABOUT: 'settings/about', SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', + SETTINGS_WALLET_DOMAINCARDS: '/settings/wallet/card/:domain', + getWalletCardRoute: (domain) => `/settings/wallet/card/${domain}`, SETTINGS_ADD_PAYPAL_ME: 'settings/wallet/add-paypal-me', SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card', SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js index 65439420811c..a2596c828420 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.js +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.js @@ -541,7 +541,7 @@ const SettingsModalStackNavigator = createModalStackNavigator([ const SettingsWalletExpensifyCardPage = require('../../../pages/settings/Wallet/ExpensifyCardPage').default; return SettingsWalletExpensifyCardPage; }, - name: 'Settings_Wallet_Expensify_Card', + name: 'Settings_Wallet_DomainCards', }, { getComponent: () => { diff --git a/src/libs/Navigation/linkingConfig.js b/src/libs/Navigation/linkingConfig.js index 14ee2b895831..657d70f8adad 100644 --- a/src/libs/Navigation/linkingConfig.js +++ b/src/libs/Navigation/linkingConfig.js @@ -73,6 +73,10 @@ export default { path: ROUTES.SETTINGS_WALLET, exact: true, }, + Settings_Wallet_DomainCards: { + path: ROUTES.SETTINGS_WALLET_DOMAINCARDS, + exact: true, + }, Settings_Wallet_EnablePayments: { path: ROUTES.SETTINGS_ENABLE_PAYMENTS, exact: true, From 8af98184fa1a00ab16e8dc3085c90a792a113138 Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 8 Sep 2023 17:09:47 +0100 Subject: [PATCH 03/25] fix(expensify card): card holder label style --- src/styles/styles.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/styles/styles.js b/src/styles/styles.js index 692bf5db51bb..679aa0aa928f 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -4000,6 +4000,9 @@ const styles = (theme) => ({ left: 16, bottom: 16, width: 160, + color: themeColors.text, + fontSize: variables.fontSizeLabel, + lineHeight: variables.lineHeightLarge, }, }); From e3c1050dc9581f56b481e737f6b0bb27728e80ad Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Fri, 8 Sep 2023 17:10:35 +0100 Subject: [PATCH 04/25] refactor(expensify card): apply code guidelines --- src/components/CardPreview.js | 11 +++-------- src/pages/settings/Wallet/ExpensifyCardPage.js | 3 +-- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/components/CardPreview.js b/src/components/CardPreview.js index 31549f8dc6d1..49afba8a1661 100644 --- a/src/components/CardPreview.js +++ b/src/components/CardPreview.js @@ -2,12 +2,10 @@ import React from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import PropTypes from 'prop-types'; - import styles from '../styles/styles'; import Text from './Text'; import usePrivatePersonalDetails from '../hooks/usePrivatePersonalDetails'; import ONYXKEYS from '../ONYXKEYS'; - import ExpensifyCardImage from '../../assets/images/expensify-card.svg'; const propTypes = { @@ -32,12 +30,9 @@ const defaultProps = { }, }; -function CardPreview(props) { +function CardPreview({privatePersonalDetails: {legalFirstName, legalLastName}, session: {email}}) { usePrivatePersonalDetails(); - const legalFirstName = props.privatePersonalDetails.legalFirstName || ''; - const legalLastName = props.privatePersonalDetails.legalLastName || ''; - - const cardHolder = legalFirstName && legalLastName ? `${legalFirstName} ${legalLastName}` : props.session.email; + const cardHolder = legalFirstName && legalLastName ? `${legalFirstName} ${legalLastName}` : email; return ( @@ -59,7 +54,7 @@ function CardPreview(props) { CardPreview.propTypes = propTypes; CardPreview.defaultProps = defaultProps; -CardPreview.displayName = CardPreview; +CardPreview.displayName = 'CardPreview'; export default withOnyx({ privatePersonalDetails: { diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 57b13cdb501f..6c39efd17681 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -8,7 +8,6 @@ import Navigation from '../../../libs/Navigation/Navigation'; import styles from '../../../styles/styles'; import * as CurrencyUtils from '../../../libs/CurrencyUtils'; import MenuItemWithTopDescription from '../../../components/MenuItemWithTopDescription'; - import CardPreview from '../../../components/CardPreview'; import useLocalize from '../../../hooks/useLocalize'; @@ -55,6 +54,6 @@ function ExpensifyCardPage(props) { ExpensifyCardPage.propTypes = propTypes; ExpensifyCardPage.defaultProps = defaultProps; -ExpensifyCardPage.displayName = ExpensifyCardPage; +ExpensifyCardPage.displayName = 'ExpensifyCardPage'; export default ExpensifyCardPage; From 20d12686b7ca81a62c5b0c3ad5f13fa4e10079ef Mon Sep 17 00:00:00 2001 From: Pedro Guerreiro Date: Thu, 14 Sep 2023 10:14:17 +0100 Subject: [PATCH 05/25] feat(expensify card): add virtual and physical card number rows --- src/languages/en.ts | 2 + src/languages/es.ts | 2 + src/libs/CardUtils.ts | 14 ++++- .../settings/Wallet/ExpensifyCardPage.js | 55 +++++++++++++++---- src/styles/styles.js | 5 ++ 5 files changed, 65 insertions(+), 13 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 006d2e1e9efa..555a472d5cf1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -809,6 +809,8 @@ export default { cardPage: { expensifyCard: 'Expensify Card', availableSpend: 'Remaining spending power', + virtualCardNumber: 'Virtual card number', + physicalCardNumber: 'Physical card number', }, transferAmountPage: { transfer: ({amount}: TransferParams) => `Transfer${amount ? ` ${amount}` : ''}`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 87d231b8e693..733b2605acd9 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -804,6 +804,8 @@ export default { cardPage: { expensifyCard: 'Tarjeta Expensify', availableSpend: 'Poder de gasto restante', + virtualCardNumber: 'Número de la tarjeta virtual', + physicalCardNumber: 'Número de la tarjeta física', }, transferAmountPage: { transfer: ({amount}: TransferParams) => `Transferir${amount ? ` ${amount}` : ''}`, diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index beb0ea800091..53c57e92523f 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -1,5 +1,7 @@ +import lodash from 'lodash'; import {Card} from '../types/onyx'; import CONST from '../CONST'; +import * as OnyxTypes from '../types/onyx'; /** * @returns string with a month in MM format @@ -25,4 +27,14 @@ function getCompanyCards(cardList: {string: Card}) { return Object.values(cardList).filter((card) => card.bank !== CONST.EXPENSIFY_CARD.BANK); } -export {getMonthFromExpirationDateString, getYearFromExpirationDateString, getCompanyCards}; +/** + * @param cardList - collection of assigned cards + * @returns collection of assigned cards grouped by domain + */ +function getDomainCards(cardList: Record) { + // eslint-disable-next-line you-dont-need-lodash-underscore/filter + const activeCards = lodash.filter(cardList, (card) => [2, 3, 4, 7].includes(card.state)); + return lodash.groupBy(activeCards, (card) => card.domainName); +} + +export {getDomainCards, getCompanyCards, getMonthFromExpirationDateString, getYearFromExpirationDateString}; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 6c39efd17681..dab43c8d8dcf 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -1,30 +1,45 @@ -import React from 'react'; -import {View, ScrollView} from 'react-native'; import PropTypes from 'prop-types'; +import React from 'react'; +import {ScrollView, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import _ from 'underscore'; +import ONYXKEYS from '../../../ONYXKEYS'; import ROUTES from '../../../ROUTES'; +import CardPreview from '../../../components/CardPreview'; import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; +import MenuItemWithTopDescription from '../../../components/MenuItemWithTopDescription'; import ScreenWrapper from '../../../components/ScreenWrapper'; +import cardPropTypes from '../../../components/cardPropTypes'; +import useLocalize from '../../../hooks/useLocalize'; +import * as CurrencyUtils from '../../../libs/CurrencyUtils'; import Navigation from '../../../libs/Navigation/Navigation'; import styles from '../../../styles/styles'; -import * as CurrencyUtils from '../../../libs/CurrencyUtils'; -import MenuItemWithTopDescription from '../../../components/MenuItemWithTopDescription'; -import CardPreview from '../../../components/CardPreview'; -import useLocalize from '../../../hooks/useLocalize'; +import * as CardUtils from '../../../libs/CardUtils'; const propTypes = { /* Onyx Props */ + cardList: PropTypes.objectOf(cardPropTypes), - spendLimit: PropTypes.number, + /** Navigation route context info provided by react navigation */ + route: PropTypes.shape({ + params: PropTypes.shape({ + /** domain passed via route /settings/wallet/card/:domain */ + domain: PropTypes.string, + }), + }).isRequired, }; const defaultProps = { - spendLimit: 0, + cardList: {}, }; -function ExpensifyCardPage(props) { +function ExpensifyCardPage({cardList, route: {params: {domain}}}) { const {translate} = useLocalize(); + const domainCards = CardUtils.getDomainCards(cardList)[domain]; + const virtualCard = _.find(domainCards, (card) => card.isVirtual); + const physicalCard = _.find(domainCards, (card) => !card.isVirtual); - const formattedSpendLimitAmount = CurrencyUtils.convertToDisplayString(props.spendLimit); + const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend); return ( @@ -41,10 +56,22 @@ function ExpensifyCardPage(props) { + + )} @@ -56,4 +83,8 @@ ExpensifyCardPage.propTypes = propTypes; ExpensifyCardPage.defaultProps = defaultProps; ExpensifyCardPage.displayName = 'ExpensifyCardPage'; -export default ExpensifyCardPage; +export default withOnyx({ + cardList: { + key: ONYXKEYS.CARD_LIST, + }, +})(ExpensifyCardPage); diff --git a/src/styles/styles.js b/src/styles/styles.js index 679aa0aa928f..cdabdb8a324c 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3995,6 +3995,11 @@ const styles = (theme) => ({ overflow: 'hidden', }, + walletCardNumber: { + color: themeColors.text, + fontSize: variables.fontSizeNormal, + }, + walletCardHolder: { position: 'absolute', left: 16, From 33cc565e4f97b2d5276c539c12056311e37910d3 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 14 Sep 2023 15:24:07 +0200 Subject: [PATCH 06/25] fix type for route --- src/ROUTES.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c4ed1d625dc4..3bd0941e1a46 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -49,7 +49,7 @@ export default { SETTINGS_APP_DOWNLOAD_LINKS: 'settings/about/app-download-links', SETTINGS_WALLET: 'settings/wallet', SETTINGS_WALLET_DOMAINCARDS: '/settings/wallet/card/:domain', - getWalletCardRoute: (domain) => `/settings/wallet/card/${domain}`, + getWalletCardRoute: (domain: string) => `/settings/wallet/card/${domain}`, SETTINGS_ADD_PAYPAL_ME: 'settings/wallet/add-paypal-me', SETTINGS_ADD_DEBIT_CARD: 'settings/wallet/add-debit-card', SETTINGS_ADD_BANK_ACCOUNT: 'settings/wallet/add-bank-account', From 597b3197357a9f47a7f7053d787929d72870d059 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 14 Sep 2023 15:24:36 +0200 Subject: [PATCH 07/25] update styles for card --- src/components/CardPreview.js | 2 +- src/styles/styles.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/CardPreview.js b/src/components/CardPreview.js index 49afba8a1661..2d1f7b0d053f 100644 --- a/src/components/CardPreview.js +++ b/src/components/CardPreview.js @@ -43,7 +43,7 @@ function CardPreview({privatePersonalDetails: {legalFirstName, legalLastName}, s /> {cardHolder} diff --git a/src/styles/styles.js b/src/styles/styles.js index cdabdb8a324c..cc8da446957f 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -4006,7 +4006,7 @@ const styles = (theme) => ({ bottom: 16, width: 160, color: themeColors.text, - fontSize: variables.fontSizeLabel, + fontSize: variables.fontSizeSmall, lineHeight: variables.lineHeightLarge, }, }); From 9a04cf08e55935b1ecb6f46bf5d676404a53adf2 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 14 Sep 2023 15:27:29 +0200 Subject: [PATCH 08/25] fix upper cased domain --- src/libs/CardUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 53c57e92523f..af2f3dfd93d2 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -34,7 +34,7 @@ function getCompanyCards(cardList: {string: Card}) { function getDomainCards(cardList: Record) { // eslint-disable-next-line you-dont-need-lodash-underscore/filter const activeCards = lodash.filter(cardList, (card) => [2, 3, 4, 7].includes(card.state)); - return lodash.groupBy(activeCards, (card) => card.domainName); + return lodash.groupBy(activeCards, (card) => card.domainName.toLowerCase()); } export {getDomainCards, getCompanyCards, getMonthFromExpirationDateString, getYearFromExpirationDateString}; From 1040f6bb06c5673dd60cd942f83b4a4f83e592d3 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 14 Sep 2023 15:28:33 +0200 Subject: [PATCH 09/25] adjust displayed card data --- .../settings/Wallet/ExpensifyCardPage.js | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index dab43c8d8dcf..9cae4122d030 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -33,7 +33,12 @@ const defaultProps = { cardList: {}, }; -function ExpensifyCardPage({cardList, route: {params: {domain}}}) { +function ExpensifyCardPage({ + cardList, + route: { + params: {domain}, + }, +}) { const {translate} = useLocalize(); const domainCards = CardUtils.getDomainCards(cardList)[domain]; const virtualCard = _.find(domainCards, (card) => card.isVirtual); @@ -62,16 +67,18 @@ function ExpensifyCardPage({cardList, route: {params: {domain}}}) { /> - + {physicalCard && ( + + )} )} From 27e242d8ac33c1538c24c66a7b212f1898898746 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 14 Sep 2023 15:37:50 +0200 Subject: [PATCH 10/25] fix styles file after rebase --- src/styles/styles.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/styles/styles.js b/src/styles/styles.js index cc8da446957f..22465ef28650 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3987,7 +3987,6 @@ const styles = (theme) => ({ width: '100%', }, - walletCard: { borderRadius: variables.componentBorderRadiusLarge, position: 'relative', @@ -3996,7 +3995,7 @@ const styles = (theme) => ({ }, walletCardNumber: { - color: themeColors.text, + color: theme.text, fontSize: variables.fontSizeNormal, }, @@ -4005,7 +4004,7 @@ const styles = (theme) => ({ left: 16, bottom: 16, width: 160, - color: themeColors.text, + color: theme.text, fontSize: variables.fontSizeSmall, lineHeight: variables.lineHeightLarge, }, From c57bb8659a1bab24b8cb0c6a17bd1867e6114b43 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 15 Sep 2023 09:37:26 +0200 Subject: [PATCH 11/25] fix conditional rendering to a boolean --- src/pages/settings/Wallet/ExpensifyCardPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 9cae4122d030..246114cbe8c3 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -71,7 +71,7 @@ function ExpensifyCardPage({ interactive={false} titleStyle={styles.walletCardNumber} /> - {physicalCard && ( + {!_.isEmpty(physicalCard) && ( Date: Fri, 15 Sep 2023 10:37:54 +0200 Subject: [PATCH 12/25] fix translation for card page --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 733b2605acd9..79c4aff5423f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -803,7 +803,7 @@ export default { }, cardPage: { expensifyCard: 'Tarjeta Expensify', - availableSpend: 'Poder de gasto restante', + availableSpend: 'Capacidad de gasto restante', virtualCardNumber: 'Número de la tarjeta virtual', physicalCardNumber: 'Número de la tarjeta física', }, From fc90f3def18d0732481958ba8506a149de72c2bb Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Fri, 15 Sep 2023 11:23:47 +0200 Subject: [PATCH 13/25] fix crash with empty card list state --- src/pages/settings/Wallet/ExpensifyCardPage.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 246114cbe8c3..b06164de7365 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -41,10 +41,12 @@ function ExpensifyCardPage({ }) { const {translate} = useLocalize(); const domainCards = CardUtils.getDomainCards(cardList)[domain]; - const virtualCard = _.find(domainCards, (card) => card.isVirtual); - const physicalCard = _.find(domainCards, (card) => !card.isVirtual); + const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; + const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; - const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend); + const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); + + const virtualMaskedPan = virtualCard.maskedPan || ''; return ( @@ -67,7 +69,7 @@ function ExpensifyCardPage({ /> From b043af1496c1843af2772658c31808ec9e822de5 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 18 Sep 2023 12:52:33 +0200 Subject: [PATCH 14/25] add showing not found page in case of empty cards --- src/pages/settings/Wallet/ExpensifyCardPage.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index b06164de7365..cbf6854e9f00 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -5,6 +5,7 @@ import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import ONYXKEYS from '../../../ONYXKEYS'; import ROUTES from '../../../ROUTES'; +import NotFoundPage from '../../ErrorPage/NotFoundPage'; import CardPreview from '../../../components/CardPreview'; import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; import MenuItemWithTopDescription from '../../../components/MenuItemWithTopDescription'; @@ -44,9 +45,11 @@ function ExpensifyCardPage({ const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; - const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); + if (_.isEmpty(virtualCard) || _.isEmpty(physicalCard)) { + return ; + } - const virtualMaskedPan = virtualCard.maskedPan || ''; + const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); return ( @@ -69,7 +72,7 @@ function ExpensifyCardPage({ /> From 33325ced11d1a3bb4f9680b5187023618651a57e Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 20 Sep 2023 08:45:28 +0200 Subject: [PATCH 15/25] update card holder name width --- src/styles/styles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/styles/styles.js b/src/styles/styles.js index cb8a22a76a85..31d17f87f816 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -4005,7 +4005,7 @@ const styles = (theme) => ({ position: 'absolute', left: 16, bottom: 16, - width: 160, + width: 156, color: theme.text, fontSize: variables.fontSizeSmall, lineHeight: variables.lineHeightLarge, From c864fe74a3ac0fffc526f9be8f8f48167d4987b1 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 20 Sep 2023 11:04:46 +0200 Subject: [PATCH 16/25] update prop types for card page --- src/pages/settings/Wallet/ExpensifyCardPage.js | 4 ++-- .../settings/Wallet/assignedCardPropTypes.js | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 src/pages/settings/Wallet/assignedCardPropTypes.js diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index cbf6854e9f00..ccfe8ecafe54 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -10,7 +10,7 @@ import CardPreview from '../../../components/CardPreview'; import HeaderWithBackButton from '../../../components/HeaderWithBackButton'; import MenuItemWithTopDescription from '../../../components/MenuItemWithTopDescription'; import ScreenWrapper from '../../../components/ScreenWrapper'; -import cardPropTypes from '../../../components/cardPropTypes'; +import assignedCardPropTypes from './assignedCardPropTypes'; import useLocalize from '../../../hooks/useLocalize'; import * as CurrencyUtils from '../../../libs/CurrencyUtils'; import Navigation from '../../../libs/Navigation/Navigation'; @@ -19,7 +19,7 @@ import * as CardUtils from '../../../libs/CardUtils'; const propTypes = { /* Onyx Props */ - cardList: PropTypes.objectOf(cardPropTypes), + cardList: PropTypes.objectOf(assignedCardPropTypes), /** Navigation route context info provided by react navigation */ route: PropTypes.shape({ diff --git a/src/pages/settings/Wallet/assignedCardPropTypes.js b/src/pages/settings/Wallet/assignedCardPropTypes.js new file mode 100644 index 000000000000..e45b57a05d31 --- /dev/null +++ b/src/pages/settings/Wallet/assignedCardPropTypes.js @@ -0,0 +1,18 @@ +import PropTypes from 'prop-types'; +import CONST from '../../../CONST'; + +/** Assigned Card props */ +const assignedCardPropTypes = PropTypes.shape({ + cardID: PropTypes.number, + state: PropTypes.number, + bank: PropTypes.string, + availableSpend: PropTypes.number, + domainName: PropTypes.string, + maskedPan: PropTypes.string, + isVirtual: PropTypes.bool, + fraud: PropTypes.oneOf([CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN, CONST.EXPENSIFY_CARD.FRAUD_TYPES.USER, CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE]), + cardholderFirstName: PropTypes.string, + cardholderLastName: PropTypes.string, +}); + +export default assignedCardPropTypes; From 8248196c7d93c56952664c9d6ae2b5290147bc87 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 21 Sep 2023 13:29:02 +0700 Subject: [PATCH 17/25] fix duplicate participant name and avatar --- src/pages/home/report/ReportActionItemSingle.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index bfbce8aed336..7d71ad00a0f3 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -114,14 +114,15 @@ function ReportActionItemSingle(props) { let secondaryAvatar = {}; const primaryDisplayName = displayName; if (displayAllActors) { - const secondaryUserDetails = props.personalDetailsList[props.iouReport.ownerAccountID] || {}; + const secondaryAccountId = props.iouReport.ownerAccountID === actorAccountID ? props.iouReport.managerID : props.iouReport.ownerAccountID; + const secondaryUserDetails = props.personalDetailsList[secondaryAccountId] || {}; const secondaryDisplayName = lodashGet(secondaryUserDetails, 'displayName', ''); displayName = `${primaryDisplayName} & ${secondaryDisplayName}`; secondaryAvatar = { - source: UserUtils.getAvatar(secondaryUserDetails.avatar, props.iouReport.ownerAccountID), + source: UserUtils.getAvatar(secondaryUserDetails.avatar, secondaryAccountId), type: CONST.ICON_TYPE_AVATAR, name: secondaryDisplayName, - id: props.iouReport.ownerAccountID, + id: secondaryAccountId, }; } else if (!isWorkspaceActor) { secondaryAvatar = ReportUtils.getIcons(props.report, {})[props.report.isOwnPolicyExpenseChat ? 0 : 1]; From 901301a95bbf991369d9a35925087163b6fb7f0d Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 21 Sep 2023 09:15:36 +0200 Subject: [PATCH 18/25] fix empty card handling --- src/pages/settings/Wallet/ExpensifyCardPage.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index ccfe8ecafe54..36ac1a83d7d0 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -45,7 +45,7 @@ function ExpensifyCardPage({ const virtualCard = _.find(domainCards, (card) => card.isVirtual) || {}; const physicalCard = _.find(domainCards, (card) => !card.isVirtual) || {}; - if (_.isEmpty(virtualCard) || _.isEmpty(physicalCard)) { + if (_.isEmpty(virtualCard) && _.isEmpty(physicalCard)) { return ; } @@ -70,12 +70,14 @@ function ExpensifyCardPage({ interactive={false} titleStyle={styles.newKansasLarge} /> - + {!_.isEmpty(physicalCard) && ( + + )} {!_.isEmpty(physicalCard) && ( Date: Thu, 21 Sep 2023 09:16:13 +0200 Subject: [PATCH 19/25] move style constants to variables --- src/components/CardPreview.js | 5 +++-- src/styles/styles.js | 2 +- src/styles/variables.ts | 4 ++++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/CardPreview.js b/src/components/CardPreview.js index 2d1f7b0d053f..5a5d34485d74 100644 --- a/src/components/CardPreview.js +++ b/src/components/CardPreview.js @@ -7,6 +7,7 @@ import Text from './Text'; import usePrivatePersonalDetails from '../hooks/usePrivatePersonalDetails'; import ONYXKEYS from '../ONYXKEYS'; import ExpensifyCardImage from '../../assets/images/expensify-card.svg'; +import variables from '../styles/variables'; const propTypes = { /** User's private personal details */ @@ -38,8 +39,8 @@ function CardPreview({privatePersonalDetails: {legalFirstName, legalLastName}, s ({ position: 'absolute', left: 16, bottom: 16, - width: 156, + width: variables.cardNameWidth, color: theme.text, fontSize: variables.fontSizeSmall, lineHeight: variables.lineHeightLarge, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 6291323cef0b..d3a39bbdb3a3 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -158,4 +158,8 @@ export default { moneyRequestSkeletonHeight: 107, distanceScrollEventThrottle: 16, + + cardPreviewHeight: 148, + cardPreviewWidth: 235, + cardNameWidth: 156, } as const; From 29c668587b5e73660bf1bc4d393f36d8074d64da Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 21 Sep 2023 18:25:56 +0200 Subject: [PATCH 20/25] update card types with new PAN --- src/pages/settings/Wallet/ExpensifyCardPage.js | 4 ++-- src/pages/settings/Wallet/assignedCardPropTypes.js | 4 +++- src/types/onyx/Card.ts | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index 36ac1a83d7d0..b144c95c6f19 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -73,7 +73,7 @@ function ExpensifyCardPage({ {!_.isEmpty(physicalCard) && ( @@ -81,7 +81,7 @@ function ExpensifyCardPage({ {!_.isEmpty(physicalCard) && ( diff --git a/src/pages/settings/Wallet/assignedCardPropTypes.js b/src/pages/settings/Wallet/assignedCardPropTypes.js index e45b57a05d31..2f0eb1ad4ec9 100644 --- a/src/pages/settings/Wallet/assignedCardPropTypes.js +++ b/src/pages/settings/Wallet/assignedCardPropTypes.js @@ -8,11 +8,13 @@ const assignedCardPropTypes = PropTypes.shape({ bank: PropTypes.string, availableSpend: PropTypes.number, domainName: PropTypes.string, - maskedPan: PropTypes.string, + lastFourPAN: PropTypes.string, + cardName: PropTypes.string, isVirtual: PropTypes.bool, fraud: PropTypes.oneOf([CONST.EXPENSIFY_CARD.FRAUD_TYPES.DOMAIN, CONST.EXPENSIFY_CARD.FRAUD_TYPES.USER, CONST.EXPENSIFY_CARD.FRAUD_TYPES.NONE]), cardholderFirstName: PropTypes.string, cardholderLastName: PropTypes.string, + errors: PropTypes.objectOf(PropTypes.string), }); export default assignedCardPropTypes; diff --git a/src/types/onyx/Card.ts b/src/types/onyx/Card.ts index e89c966d49da..08d16cf8494c 100644 --- a/src/types/onyx/Card.ts +++ b/src/types/onyx/Card.ts @@ -10,7 +10,7 @@ type Card = { bank: string; availableSpend: number; domainName: string; - maskedPan: string; + lastFourPAN?: string; cardName: string; isVirtual: boolean; fraud: ValueOf; From dfeabc8bfc329abd5f98c180474a68c93b2c3d89 Mon Sep 17 00:00:00 2001 From: Hans Date: Thu, 21 Sep 2023 23:45:15 +0700 Subject: [PATCH 21/25] add comment for secondaryAccountId --- src/pages/home/report/ReportActionItemSingle.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index 7d71ad00a0f3..a220f17d3e8b 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -114,6 +114,7 @@ function ReportActionItemSingle(props) { let secondaryAvatar = {}; const primaryDisplayName = displayName; if (displayAllActors) { + // When we merge IOUs into one, if ownerAccountID is also actorAccountID, we should use managerID instead to prevent duplicate information. const secondaryAccountId = props.iouReport.ownerAccountID === actorAccountID ? props.iouReport.managerID : props.iouReport.ownerAccountID; const secondaryUserDetails = props.personalDetailsList[secondaryAccountId] || {}; const secondaryDisplayName = lodashGet(secondaryUserDetails, 'displayName', ''); From 48debed2a863baee68c639c01b1670da1d751450 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Thu, 21 Sep 2023 19:04:23 +0200 Subject: [PATCH 22/25] add maskCard helper to create mask card with PAN --- src/libs/CardUtils.ts | 21 ++++++++++++++++++- .../settings/Wallet/ExpensifyCardPage.js | 4 ++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index af2f3dfd93d2..30bb17f3db52 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -37,4 +37,23 @@ function getDomainCards(cardList: Record) { return lodash.groupBy(activeCards, (card) => card.domainName.toLowerCase()); } -export {getDomainCards, getCompanyCards, getMonthFromExpirationDateString, getYearFromExpirationDateString}; +/** + * Returns a masked credit card string with spaces for every four symbols. + * If the last four digits are provided, all preceding digits will be masked. + * If not, the entire card string will be masked. + * + * @param [lastFour=""] - The last four digits of the card (optional). + * @returns - The masked card string. + */ +function maskCard(lastFour = ''): string { + const totalDigits = 16; + const maskedLength = totalDigits - lastFour.length; + + // Create a string with '•' repeated for the masked portion + const maskedString = '•'.repeat(maskedLength) + lastFour; + + // Insert space for every four symbols + return maskedString.replace(/(.{4})/g, '$1 ').trim(); +} + +export {getDomainCards, getCompanyCards, getMonthFromExpirationDateString, getYearFromExpirationDateString, maskCard}; diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index b144c95c6f19..af9f3b311210 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -73,7 +73,7 @@ function ExpensifyCardPage({ {!_.isEmpty(physicalCard) && ( @@ -81,7 +81,7 @@ function ExpensifyCardPage({ {!_.isEmpty(physicalCard) && ( From a22a2d69d8733e881acefad0413cf0664cd10725 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Mon, 25 Sep 2023 10:44:27 +0200 Subject: [PATCH 23/25] add testID for expensify card page --- src/pages/settings/Wallet/ExpensifyCardPage.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Wallet/ExpensifyCardPage.js b/src/pages/settings/Wallet/ExpensifyCardPage.js index af9f3b311210..bc49301e8d80 100644 --- a/src/pages/settings/Wallet/ExpensifyCardPage.js +++ b/src/pages/settings/Wallet/ExpensifyCardPage.js @@ -52,7 +52,10 @@ function ExpensifyCardPage({ const formattedAvailableSpendAmount = CurrencyUtils.convertToDisplayString(physicalCard.availableSpend || virtualCard.availableSpend || 0); return ( - + {({safeAreaPaddingBottomStyle}) => ( <> Date: Mon, 25 Sep 2023 10:46:26 +0200 Subject: [PATCH 24/25] add doc for session --- src/components/CardPreview.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/CardPreview.js b/src/components/CardPreview.js index 5a5d34485d74..4f774d67360c 100644 --- a/src/components/CardPreview.js +++ b/src/components/CardPreview.js @@ -15,6 +15,7 @@ const propTypes = { legalFirstName: PropTypes.string, legalLastName: PropTypes.string, }), + /** Session info for the currently logged in user. */ session: PropTypes.shape({ /** Currently logged-in user email */ email: PropTypes.string, From 67ed516e702d773f5eba5a1732e5a6f39ae9f56a Mon Sep 17 00:00:00 2001 From: Hans Date: Mon, 25 Sep 2023 22:18:12 +0700 Subject: [PATCH 25/25] Update src/pages/home/report/ReportActionItemSingle.js Co-authored-by: Joel Davies --- src/pages/home/report/ReportActionItemSingle.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index a220f17d3e8b..3bf25aacc2f9 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -114,7 +114,7 @@ function ReportActionItemSingle(props) { let secondaryAvatar = {}; const primaryDisplayName = displayName; if (displayAllActors) { - // When we merge IOUs into one, if ownerAccountID is also actorAccountID, we should use managerID instead to prevent duplicate information. + // The ownerAccountID and actorAccountID can be the same if the a user requests money back from the IOU's original creator, in that case we need to use managerID to avoid displaying the same user twice const secondaryAccountId = props.iouReport.ownerAccountID === actorAccountID ? props.iouReport.managerID : props.iouReport.ownerAccountID; const secondaryUserDetails = props.personalDetailsList[secondaryAccountId] || {}; const secondaryDisplayName = lodashGet(secondaryUserDetails, 'displayName', '');