Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,9 @@ const ONYXKEYS = {
/** Currently displaying feed */
LAST_SELECTED_FEED: 'lastSelectedFeed_',

/** Currently displaying Expensify Card feed */
LAST_SELECTED_EXPENSIFY_CARD_FEED: 'lastSelectedExpensifyCardFeed_',

/** Whether the bank account chosen for Expensify Card in on verification waitlist */
NVP_EXPENSIFY_ON_CARD_WAITLIST: 'nvp_expensify_onCardWaitlist_',

Expand Down Expand Up @@ -946,6 +949,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName;
[ONYXKEYS.COLLECTION.EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean;
[ONYXKEYS.COLLECTION.LAST_SELECTED_FEED]: OnyxTypes.CompanyCardFeed;
[ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED]: OnyxTypes.FundID;
[ONYXKEYS.COLLECTION.NVP_EXPENSIFY_ON_CARD_WAITLIST]: OnyxTypes.CardOnWaitlist;
[ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard;
};
Expand Down
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/expensify-card/settings/account',
getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/settings/account`, backTo),
},
WORKSPACE_EXPENSIFY_CARD_SELECT_FEED: {
route: 'settings/workspaces/:policyID/expensify-card/select-feed',
getRoute: (policyID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/select-feed`, backTo),
},
WORKSPACE_EXPENSIFY_CARD_SETTINGS_FREQUENCY: {
route: 'settings/workspaces/:policyID/expensify-card/settings/frequency',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/settings/frequency` 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 @@ -505,6 +505,7 @@ const SCREENS = {
EXPENSIFY_CARD_LIMIT: 'Workspace_ExpensifyCard_Limit',
EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New',
EXPENSIFY_CARD_NAME: 'Workspace_ExpensifyCard_Name',
EXPENSIFY_CARD_SELECT_FEED: 'Workspace_ExpensifyCard_Select_Feed',
EXPENSIFY_CARD_LIMIT_TYPE: 'Workspace_ExpensifyCard_LimitType',
EXPENSIFY_CARD_BANK_ACCOUNT: 'Workspace_ExpensifyCard_BankAccount',
EXPENSIFY_CARD_SETTINGS: 'Workspace_ExpensifyCard_Settings',
Expand Down
67 changes: 67 additions & 0 deletions src/components/FeedSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React from 'react';
import {View} from 'react-native';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import variables from '@styles/variables';
import type IconAsset from '@src/types/utils/IconAsset';
import CaretWrapper from './CaretWrapper';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import {PressableWithFeedback} from './Pressable';
import Text from './Text';

type Props = {
/** Function to call when the feed is selected */
onFeedSelect: () => void;

/** Icon for the card */
cardIcon: IconAsset;

/** Whether to show assign card button */
shouldChangeLayout?: boolean;

/** Feed name */
feedName?: string;

/** Supporting text */
supportingText?: string;

/** Whether the RBR indicator should be shown */
shouldShowRBR?: boolean;
};

function FeedSelector({onFeedSelect, cardIcon, shouldChangeLayout, feedName, supportingText, shouldShowRBR = false}: Props) {
const styles = useThemeStyles();
const theme = useTheme();

return (
<PressableWithFeedback
onPress={onFeedSelect}
style={[styles.flexRow, styles.alignItemsCenter, styles.gap3, shouldChangeLayout && styles.mb3]}
accessibilityLabel={feedName ?? ''}
>
<Icon
src={cardIcon}
height={variables.cardIconHeight}
width={variables.cardIconWidth}
additionalStyles={styles.cardIcon}
/>
<View style={styles.flex1}>
<View style={[styles.flexRow, styles.gap1]}>
<CaretWrapper style={styles.flex1}>
<Text style={[styles.textStrong, styles.flexShrink1]}>{feedName}</Text>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Came from this issue
We had an issue where the Card feed name is displayed partially with a blank space on Android 16
More context here

</CaretWrapper>
{shouldShowRBR && (
<Icon
src={Expensicons.DotIndicator}
fill={theme.danger}
/>
)}
</View>
<Text style={styles.textLabelSupporting}>{supportingText}</Text>
</View>
</PressableWithFeedback>
);
}

export default FeedSelector;
37 changes: 37 additions & 0 deletions src/hooks/useDefaultFundID.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {useOnyx} from 'react-native-onyx';
import {getFundIdFromSettingsKey} from '@libs/CardUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import useWorkspaceAccountID from './useWorkspaceAccountID';

/**
* Hook to get the default fundID for a given policyID. This is used to get the settings and cards for each of the feeds.
* It will always return lastSelectedExpensifyCardFeed if it exists or fallback to the workspaceAccountID or domainFundID.
*/
function useDefaultFundID(policyID: string | undefined) {
Comment thread
puneetlath marked this conversation as resolved.
const workspaceAccountID = useWorkspaceAccountID(policyID);
const [lastSelectedExpensifyCardFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED}${policyID}`);

const [domainFundID] = useOnyx(ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS, {
selector: (cardSettings) => {
const matchingKey = Object.entries(cardSettings ?? {}).find(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([key, settings]) => settings?.preferredPolicy && settings.preferredPolicy === policyID && !key.includes(workspaceAccountID.toString()),
);

return getFundIdFromSettingsKey(matchingKey?.[0] ?? '');
},
});

if (lastSelectedExpensifyCardFeed) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should also consider lastSelectedCardSettings availability here because if the lastSelectedExpensifyCardFeed value workspace is deleted then it doesn't make sense to return that feed number. Issue #65592

return lastSelectedExpensifyCardFeed;
}

if (workspaceAccountID) {
return workspaceAccountID;
}

return domainFundID ?? CONST.DEFAULT_NUMBER_ID;
}

export default useDefaultFundID;
34 changes: 0 additions & 34 deletions src/hooks/useDomainFundID.ts

This file was deleted.

19 changes: 19 additions & 0 deletions src/hooks/useExpensifyCardFeeds.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {useOnyx} from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';

function useExpensifyCardFeeds(policyID: string | undefined) {
const [allExpensifyCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS, {
selector: (cardSettings) => {
const matchingEntries = Object.entries(cardSettings ?? {}).filter(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
([_, settings]) => settings?.preferredPolicy && settings.preferredPolicy === policyID,
);

return Object.fromEntries(matchingEntries);
},
});

return allExpensifyCardFeeds;
}

export default useExpensifyCardFeeds;
12 changes: 12 additions & 0 deletions src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,17 @@ function isExpensifyCardFullySetUp(policy?: OnyxEntry<Policy>, cardSettings?: On
return !!(policy?.areExpensifyCardsEnabled && cardSettings?.paymentBankAccountID);
}

function getFundIdFromSettingsKey(key: string) {
const prefix = ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS;
if (!key?.startsWith(prefix)) {
return CONST.DEFAULT_NUMBER_ID;
}
const fundIDStr = key.substring(prefix.length);

const fundID = Number(fundIDStr);
return Number.isNaN(fundID) ? CONST.DEFAULT_NUMBER_ID : fundID;
}

export {
isExpensifyCard,
isCorporateCard,
Expand Down Expand Up @@ -641,4 +652,5 @@ export {
hasCardListObject,
isExpensifyCardFullySetUp,
filterInactiveCards,
getFundIdFromSettingsKey,
};
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceCardSettingsPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceSettlementAccountPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_FREQUENCY]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SELECT_FEED]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardSelectorPage').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.WORKSPACE.EXPENSIFY_CARD_NAME]: () => require<ReactComponentModule>('../../../../pages/workspace/expensifyCard/WorkspaceEditCardNamePage').default,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ const WORKSPACE_TO_RHP: Partial<Record<keyof WorkspaceSplitNavigatorParamList, s
SCREENS.WORKSPACE.EXPENSIFY_CARD_NAME,
SCREENS.WORKSPACE.EXPENSIFY_CARD_LIMIT,
SCREENS.WORKSPACE.EXPENSIFY_CARD_LIMIT_TYPE,
SCREENS.WORKSPACE.EXPENSIFY_CARD_SELECT_FEED,
],
[SCREENS.WORKSPACE.RULES]: [
SCREENS.WORKSPACE.RULES_CUSTOM_NAME,
Expand Down
8 changes: 7 additions & 1 deletion src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -613,6 +613,9 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS_ACCOUNT.route,
},
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SELECT_FEED]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_SELECT_FEED.route,
},
[SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS]: {
path: ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS.route,
},
Expand Down Expand Up @@ -1283,7 +1286,10 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.MONEY_REQUEST.STEP_WAYPOINT]: ROUTES.MONEY_REQUEST_STEP_WAYPOINT.route,
[SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.route,
[SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: ROUTES.MONEY_REQUEST_STEP_TAX_RATE.route,
[SCREENS.MONEY_REQUEST.STATE_SELECTOR]: {path: ROUTES.MONEY_REQUEST_STATE_SELECTOR.route, exact: true},
[SCREENS.MONEY_REQUEST.STATE_SELECTOR]: {
path: ROUTES.MONEY_REQUEST_STATE_SELECTOR.route,
exact: true,
},
[SCREENS.MONEY_REQUEST.STEP_SPLIT_PAYER]: ROUTES.MONEY_REQUEST_STEP_SPLIT_PAYER.route,
[SCREENS.MONEY_REQUEST.STEP_ATTENDEES]: ROUTES.MONEY_REQUEST_ATTENDEE.route,
[SCREENS.MONEY_REQUEST.STEP_UPGRADE]: ROUTES.MONEY_REQUEST_UPGRADE.route,
Expand Down
25 changes: 22 additions & 3 deletions src/libs/actions/Card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,10 @@ function updateSettlementFrequency(workspaceAccountID: number, settlementFrequen
API.write(WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_FREQUENCY, parameters, {optimisticData, successData, failureData});
}

function updateSettlementAccount(workspaceAccountID: number, policyID: string, settlementBankAccountID?: number, currentSettlementBankAccountID?: number) {
function updateSettlementAccount(domainName: string, workspaceAccountID: number, policyID: string, settlementBankAccountID?: number, currentSettlementBankAccountID?: number) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to change this function?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because prevously we generated the domainName inside function from the policy - now we cannot get it from the policyID like previously
Screenshot 2025-04-14 at 09 40 09

if (!settlementBankAccountID) {
return;
}
const domainName = PolicyUtils.getDomainNameForPolicy(policyID);

const optimisticData: OnyxUpdate[] = [
{
Expand Down Expand Up @@ -390,7 +389,12 @@ function getCardDefaultName(userName?: string) {
}

function setIssueNewCardStepAndData({data, isEditing, step, policyID}: IssueNewCardFlowData) {
Onyx.merge(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {data, isEditing, currentStep: step, errors: null});
Onyx.merge(`${ONYXKEYS.COLLECTION.ISSUE_NEW_EXPENSIFY_CARD}${policyID}`, {
data,
isEditing,
currentStep: step,
errors: null,
});
}

function clearIssueNewCardFlow(policyID: string | undefined) {
Expand Down Expand Up @@ -918,6 +922,20 @@ function updateSelectedFeed(feed: CompanyCardFeed, policyID: string | undefined)
]);
}

function updateSelectedExpensifyCardFeed(feed: number, policyID: string | undefined) {
if (!policyID) {
return;
}

Onyx.update([
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.LAST_SELECTED_EXPENSIFY_CARD_FEED}${policyID}`,
value: feed,
},
]);
}

function queueExpensifyCardForBilling(feedCountry: string, domainAccountID: number) {
const parameters = {
feedCountry,
Expand Down Expand Up @@ -948,6 +966,7 @@ export {
toggleContinuousReconciliation,
updateExpensifyCardLimitType,
updateSelectedFeed,
updateSelectedExpensifyCardFeed,
deactivateCard,
getCardDefaultName,
queueExpensifyCardForBilling,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ import SelectionList from '@components/SelectionList';
import RadioListItem from '@components/SelectionList/RadioListItem';
import Text from '@components/Text';
import TextLink from '@components/TextLink';
import useDefaultFundID from '@hooks/useDefaultFundID';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID';
import {getConnectionNameFromRouteParam} from '@libs/AccountingUtils';
import {getLastFourDigits} from '@libs/BankAccountUtils';
import {getEligibleBankAccountsForCard} from '@libs/CardUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import {getDomainNameForPolicy} from '@libs/PolicyUtils';
import Navigation from '@navigation/Navigation';
import type {SettingsNavigatorParamList} from '@navigation/types';
import {updateSettlementAccount} from '@userActions/Card';
Expand All @@ -29,17 +30,19 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting
const styles = useThemeStyles();
const {translate} = useLocalize();

const workspaceAccountID = useWorkspaceAccountID(policyID);
const connectionName = getConnectionNameFromRouteParam(connection);
const defaultFundID = useDefaultFundID(policyID);

const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`);
const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`);
const paymentBankAccountID = cardSettings?.paymentBankAccountID;

const selectedBankAccount = useMemo(() => bankAccountList?.[paymentBankAccountID?.toString() ?? ''], [paymentBankAccountID, bankAccountList]);
const bankAccountNumber = useMemo(() => selectedBankAccount?.accountData?.accountNumber ?? '', [selectedBankAccount]);
const settlementAccountEnding = getLastFourDigits(bankAccountNumber);

const domainName = cardSettings?.domainName ?? getDomainNameForPolicy(policyID);

const sections = useMemo(() => {
if (!bankAccountList || isEmptyObject(bankAccountList)) {
return [];
Expand All @@ -56,7 +59,7 @@ function ReconciliationAccountSettingsPage({route}: ReconciliationAccountSetting
}, [bankAccountList, paymentBankAccountID]);

const selectBankAccount = (newBankAccountID?: number) => {
updateSettlementAccount(workspaceAccountID, policyID, newBankAccountID, paymentBankAccountID);
updateSettlementAccount(domainName, defaultFundID, policyID, newBankAccountID, paymentBankAccountID);
Navigation.goBack(ROUTES.WORKSPACE_ACCOUNTING_CARD_RECONCILIATION.getRoute(policyID, connection));
};

Expand Down
Loading