Skip to content
68 changes: 68 additions & 0 deletions src/hooks/useCardFeeds.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {useMemo} from 'react';
import type {OnyxCollection, ResultMetadata} from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';
import type {CardFeeds, CompanyCardFeed} from '@src/types/onyx';
import useOnyx from './useOnyx';
import useWorkspaceAccountID from './useWorkspaceAccountID';

/**
* This is a custom hook that combines workspace and domain card feeds for a given policy.
*
* This hook:
* - Gets all available feeds (ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER) from Onyx.
* - Extracts and compiles card feeds data including only feeds where the `preferredPolicy` matches the `policyID`.
* - Merges a workspace feed with relevant domain feeds.
*
* @param policyID - The workspace policyID to filter and construct card feeds for.
* @returns -
* A tuple containing:
* 1. Card feeds specific to the given policyID (or `undefined` if unavailable).
* 2. The result metadata from the Onyx collection fetch.
*/
const useCardFeeds = (policyID: string | undefined): [CardFeeds | undefined, ResultMetadata<OnyxCollection<CardFeeds>>] => {
const workspaceAccountID = useWorkspaceAccountID(policyID);
const [allFeeds, allFeedsResult] = useOnyx(ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER, {canBeMissing: true});

const workspaceFeeds = useMemo(() => {
if (!policyID || !allFeeds) {
return undefined;
}

const defaultFeed = allFeeds?.[`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`];
const {companyCards = {}, companyCardNicknames = {}, oAuthAccountDetails = {}} = defaultFeed?.settings ?? {};

return Object.values(allFeeds).reduce<CardFeeds & {settings: Required<CardFeeds['settings']>}>(
(acc, feed) => {
if (!feed?.settings?.companyCards) {
return acc;
}

Object.entries(feed.settings.companyCards).forEach(([key, feedSettings]) => {
const feedName = key as CompanyCardFeed;
const feedOAuthAccountDetails = feed.settings.oAuthAccountDetails?.[feedName];
const feedCompanyCardNicknames = feed.settings.companyCardNicknames?.[feedName];

if (feedSettings.preferredPolicy !== policyID || acc.settings.companyCards[feedName]) {
return;
}

acc.settings.companyCards[feedName] = feedSettings;

if (feedOAuthAccountDetails) {
acc.settings.oAuthAccountDetails[feedName] = feedOAuthAccountDetails;
}
if (feedCompanyCardNicknames) {
acc.settings.companyCardNicknames[feedName] = feedCompanyCardNicknames;
}
});

return acc;
},
{settings: {companyCards, companyCardNicknames, oAuthAccountDetails}},
);
}, [allFeeds, policyID, workspaceAccountID]);

return [workspaceFeeds, allFeedsResult];
};

export default useCardFeeds;
10 changes: 7 additions & 3 deletions src/pages/workspace/WorkspaceMoreFeaturesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Section from '@components/Section';
import Text from '@components/Text';
import useCardFeeds from '@hooks/useCardFeeds';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand Down Expand Up @@ -84,8 +85,11 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro
!!policy?.connections?.netsuite?.options?.config?.syncOptions?.syncTax;
const policyID = policy?.id;
const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID.toString()}_${CONST.EXPENSIFY_CARD.BANK}`, {selector: filterInactiveCards});
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID.toString()}`);
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID.toString()}_${CONST.EXPENSIFY_CARD.BANK}`, {
selector: filterInactiveCards,
canBeMissing: true,
});
const [cardFeeds] = useCardFeeds(policyID);
const [isOrganizeWarningModalOpen, setIsOrganizeWarningModalOpen] = useState(false);
const [isIntegrateWarningModalOpen, setIsIntegrateWarningModalOpen] = useState(false);
const [isReportFieldsWarningModalOpen, setIsReportFieldsWarningModalOpen] = useState(false);
Expand All @@ -95,7 +99,7 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro

const perDiemCustomUnit = getPerDiemCustomUnit(policy);

const [cardList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`);
const [cardList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`, {canBeMissing: true});
const workspaceCards = getAllCardsForWorkspace(workspaceAccountID, cardList);
const isSmartLimitEnabled = isSmartLimitEnabledUtil(workspaceCards);

Expand Down
3 changes: 2 additions & 1 deletion src/pages/workspace/WorkspaceOverviewPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import Section from '@components/Section';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useCardFeeds from '@hooks/useCardFeeds';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePayAndDowngrade from '@hooks/usePayAndDowngrade';
Expand Down Expand Up @@ -78,7 +79,7 @@ function WorkspaceOverviewPage({policyDraft, policy: policyProp, route}: Workspa

// We need this to update translation for deleting a workspace when it has third party card feeds or expensify card assigned.
const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, {canBeMissing: true});
const [cardFeeds] = useCardFeeds(policy?.id);
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`, {
selector: filterInactiveCards,
canBeMissing: true,
Expand Down
3 changes: 2 additions & 1 deletion src/pages/workspace/WorkspacesListPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import ScrollView from '@components/ScrollView';
import SupportalActionRestrictedModal from '@components/SupportalActionRestrictedModal';
import Text from '@components/Text';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useCardFeeds from '@hooks/useCardFeeds';
import useHandleBackButton from '@hooks/useHandleBackButton';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
Expand Down Expand Up @@ -150,7 +151,7 @@ function WorkspacesListPage() {

// We need this to update translation for deleting a workspace when it has third party card feeds or expensify card assigned.
const workspaceAccountID = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyIDToDelete}`]?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`, {canBeMissing: true});
const [cardFeeds] = useCardFeeds(policyIDToDelete);
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${CONST.EXPENSIFY_CARD.BANK}`, {
selector: filterInactiveCards,
canBeMissing: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOffli
import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator';
import HeaderWithBackButton from '@components/HeaderWithBackButton';
import ScreenWrapper from '@components/ScreenWrapper';
import useCardFeeds from '@hooks/useCardFeeds';
import useLocalize from '@hooks/useLocalize';
import usePrevious from '@hooks/usePrevious';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID';
import {updateSelectedFeed} from '@libs/actions/Card';
import {setAssignCardStepAndData} from '@libs/actions/CompanyCards';
import {checkIfNewFeedConnected, getBankName, isSelectedFeedExpired} from '@libs/CardUtils';
Expand Down Expand Up @@ -43,16 +43,15 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti
const {translate} = useLocalize();
const theme = useTheme();
const webViewRef = useRef<WebView>(null);
const [session] = useOnyx(ONYXKEYS.SESSION);
const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD);
const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false});
const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true});
const authToken = session?.authToken ?? null;
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD);
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true});
const {bankName: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {};
const policyID = policyIDFromProps ?? policyIDFromRoute;
const bankName = feed ? getBankName(feed) : bankNameFromRoute ?? addNewCard?.data?.selectedBank;
const url = getCompanyCardBankConnection(policyID, bankName);
const workspaceAccountID = useWorkspaceAccountID(policyID);
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [cardFeeds] = useCardFeeds(policyID);
const [isConnectionCompleted, setConnectionCompleted] = useState(false);
const prevFeedsData = usePrevious(cardFeeds?.settings?.oAuthAccountDetails);
const isFeedExpired = feed ? isSelectedFeedExpired(cardFeeds?.settings?.oAuthAccountDetails?.[feed]) : false;
Expand Down
9 changes: 4 additions & 5 deletions src/pages/workspace/companyCards/BankConnection/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import {PendingBank} from '@components/Icon/Illustrations';
import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useCardFeeds from '@hooks/useCardFeeds';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePrevious from '@hooks/usePrevious';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID';
import {setAssignCardStepAndData} from '@libs/actions/CompanyCards';
import {checkIfNewFeedConnected, getBankName, isSelectedFeedExpired} from '@libs/CardUtils';
import Navigation from '@libs/Navigation/Navigation';
Expand Down Expand Up @@ -43,12 +43,11 @@ type BankConnectionProps = {
function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnectionProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD);
const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD);
const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: true});
const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD, {canBeMissing: true});
const {bankName: bankNameFromRoute, backTo, policyID: policyIDFromRoute} = route?.params ?? {};
const policyID = policyIDFromProps ?? policyIDFromRoute;
const workspaceAccountID = useWorkspaceAccountID(policyID);
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [cardFeeds] = useCardFeeds(policyID);
const prevFeedsData = usePrevious(cardFeeds?.settings?.oAuthAccountDetails);
const [shouldBlockWindowOpen, setShouldBlockWindowOpen] = useState(false);
const bankName = feed ? getBankName(feed) : bankNameFromRoute ?? addNewCard?.data?.selectedBank;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import type {ListItem} from '@components/SelectionList/types';
import useCardFeeds from '@hooks/useCardFeeds';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
import useThemeIllustrations from '@hooks/useThemeIllustrations';
Expand Down Expand Up @@ -42,9 +43,9 @@ function WorkspaceCompanyCardFeedSelectorPage({route}: WorkspaceCompanyCardFeedS
const {translate} = useLocalize();
const styles = useThemeStyles();
const illustrations = useThemeIllustrations();
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [allFeedsCards] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`);
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
const [cardFeeds] = useCardFeeds(policyID);
const [allFeedsCards] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`, {canBeMissing: false});
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, {canBeMissing: true});
const selectedFeed = getSelectedFeed(lastSelectedFeed, cardFeeds);
const companyFeeds = getCompanyFeeds(cardFeeds);
const isCollect = isCollectPolicy(policy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Icon from '@components/Icon';
import * as Expensicons from '@components/Icon/Expensicons';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useCardFeeds from '@hooks/useCardFeeds';
import useLocalize from '@hooks/useLocalize';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
Expand Down Expand Up @@ -51,8 +52,8 @@ function WorkspaceCompanyCardsListHeaderButtons({policyID, selectedFeed, shouldS
const illustrations = useThemeIllustrations();
const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
const workspaceAccountID = useWorkspaceAccountID(policyID);
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [allFeedsCards] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`);
const [cardFeeds] = useCardFeeds(policyID);
const [allFeedsCards] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}`, {canBeMissing: false});
const shouldChangeLayout = isMediumScreenWidth || shouldUseNarrowLayout;
const formattedFeedName = getCustomOrFormattedFeedName(selectedFeed, cardFeeds?.settings?.companyCardNicknames);
const isCommercialFeed = isCustomFeed(selectedFeed);
Expand Down
11 changes: 6 additions & 5 deletions src/pages/workspace/companyCards/WorkspaceCompanyCardsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {useOnyx} from 'react-native-onyx';
import DecisionModal from '@components/DecisionModal';
import DelegateNoAccessModal from '@components/DelegateNoAccessModal';
import * as Illustrations from '@components/Icon/Illustrations';
import useCardFeeds from '@hooks/useCardFeeds';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand Down Expand Up @@ -45,16 +46,16 @@ function WorkspaceCompanyCardsPage({route}: WorkspaceCompanyCardsPageProps) {
const styles = useThemeStyles();
const theme = useTheme();
const policyID = route.params.policyID;
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`);
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: false});
const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, {canBeMissing: true});
const [cardFeeds] = useCardFeeds(policyID);
const selectedFeed = getSelectedFeed(lastSelectedFeed, cardFeeds);
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`, {selector: filterInactiveCards});
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`, {selector: filterInactiveCards, canBeMissing: true});

const {cardList, ...cards} = cardsList ?? {};

const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => !!account?.delegatedAccess?.delegate});
const [isActingAsDelegate] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account) => !!account?.delegatedAccess?.delegate, canBeMissing: false});
const [isNoDelegateAccessMenuVisible, setIsNoDelegateAccessMenuVisible] = useState(false);

const filteredCardList = getFilteredCardList(cardsList, selectedFeed ? cardFeeds?.settings?.oAuthAccountDetails?.[selectedFeed] : undefined);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
import useAutoFocusInput from '@hooks/useAutoFocusInput';
import useCardFeeds from '@hooks/useCardFeeds';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -40,8 +41,8 @@ function WorkspaceCompanyCardsSettingsFeedNamePage({
const {inputCallbackRef} = useAutoFocusInput();
const policy = usePolicy(policyID);
const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
const [lastSelectedFeed, lastSelectedFeedResult] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
const [cardFeeds, cardFeedsResult] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [lastSelectedFeed, lastSelectedFeedResult] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, {canBeMissing: true});
const [cardFeeds, cardFeedsResult] = useCardFeeds(policyID);
const selectedFeed = getSelectedFeed(lastSelectedFeed, cardFeeds);
const feedName = getCustomOrFormattedFeedName(selectedFeed, cardFeeds?.settings?.companyCardNicknames);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useCardFeeds from '@hooks/useCardFeeds';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
import useThemeStyles from '@hooks/useThemeStyles';
Expand Down Expand Up @@ -38,12 +39,12 @@ function WorkspaceCompanyCardsSettingsPage({
const workspaceAccountID = policy?.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
const [deleteCompanyCardConfirmModalVisible, setDeleteCompanyCardConfirmModalVisible] = useState(false);

const [cardFeeds] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${workspaceAccountID}`);
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`);
const [cardFeeds] = useCardFeeds(policyID);
const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, {canBeMissing: true});

const selectedFeed = useMemo(() => getSelectedFeed(lastSelectedFeed, cardFeeds), [cardFeeds, lastSelectedFeed]);

const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`, {selector: filterInactiveCards});
const [cardsList] = useOnyx(`${ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST}${workspaceAccountID}_${selectedFeed}`, {selector: filterInactiveCards, canBeMissing: false});
const feedName = getCustomOrFormattedFeedName(selectedFeed, cardFeeds?.settings?.companyCardNicknames);
const companyFeeds = getCompanyFeeds(cardFeeds);
const liabilityType = selectedFeed && companyFeeds[selectedFeed]?.liabilityType;
Expand Down
Loading