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/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2498,6 +2498,10 @@ const ROUTES = {
return `workspaces/${policyID}/travel` as const;
},
},
WORKSPACE_TRAVEL_SETTINGS_ACCOUNT: {
route: 'workspaces/:policyID/travel/settings/account',
getRoute: (policyID: string) => `workspaces/${policyID}/travel/settings/account` as const,
},
WORKSPACE_CREATE_DISTANCE_RATE: {
route: 'workspaces/:policyID/distance-rates/new',
getRoute: (policyID: string, transactionID?: string, reportID?: string) =>
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,7 @@ const SCREENS = {
OWNER_CHANGE_ERROR: 'Workspace_Owner_Change_Error',
DISTANCE_RATES: 'Distance_Rates',
TRAVEL: 'Travel',
TRAVEL_SETTINGS_ACCOUNT: 'Workspace_Travel_Settings_Account',
CREATE_DISTANCE_RATE: 'Create_Distance_Rate',
CREATE_DISTANCE_RATE_UPGRADE: 'Create_Distance_Rate_Upgrade',
DISTANCE_RATES_SETTINGS: 'Distance_Rates_Settings',
Expand Down
114 changes: 114 additions & 0 deletions src/components/SettlementAccountSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import React from 'react';
import {View} from 'react-native';
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import type {BankName} from '@src/types/onyx/Bank';
import Icon from './Icon';
import getBankIcon from './Icon/BankIcons';
import MenuItem from './MenuItem';
import SelectionList from './SelectionList';
import RadioListItem from './SelectionList/ListItem/RadioListItem';
import type {ListItem} from './SelectionList/types';

type BankAccountListItem = ListItem & {value: number | undefined};

type SettlementAccountSelectorProps = {
/** Bank account list items to display */
listOptions: BankAccountListItem[];

/** Callback when an account is selected */
onSelectAccount: (value: number) => void;

/** Optional callback for "Add new bank account" - if provided, the option will be shown */
onAddNewBankAccount?: () => void;

/** Whether to show the "Add new bank account" option */
showAddNewAccountOption?: boolean;

/** Optional custom header content */
customHeaderContent?: React.ReactElement;

/** Initial key to focus */
initiallyFocusedItemKey?: string;
};

function BankAccountListItemLeftElement({bankName}: {bankName: BankName}) {
const styles = useThemeStyles();
const {icon, iconSize, iconStyles} = getBankIcon({bankName, styles});

return (
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mr3]}>
<Icon
src={icon}
width={iconSize}
height={iconSize}
additionalStyles={iconStyles}
/>
</View>
);
}

/**
* Abstract component for selecting a settlement bank account.
* Used by both Expensify Card and Travel Invoicing settlement account pages.
* Follows composition over configuration pattern - different pages compose this
* with their specific callbacks and content.
*/
function SettlementAccountSelector({
listOptions,
onSelectAccount,
onAddNewBankAccount,
showAddNewAccountOption = false,
customHeaderContent,
initiallyFocusedItemKey,
}: SettlementAccountSelectorProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const icons = useMemoizedLazyExpensifyIcons(['Plus']);

const handleSelectRow = ({value}: BankAccountListItem) => {
onSelectAccount(value ?? 0);
};

// Render "Add new bank account" as list footer when enabled
const listFooterContent =
showAddNewAccountOption && onAddNewBankAccount ? (
<MenuItem
icon={icons.Plus}
title={translate('workspace.expensifyCard.addNewBankAccount')}
onPress={onAddNewBankAccount}
/>
) : undefined;

// If no list options and we should show add new account, just show the menu item
if (listOptions.length === 0 && showAddNewAccountOption && onAddNewBankAccount) {
return (
<View style={styles.flex1}>
{customHeaderContent}
<MenuItem
icon={icons.Plus}
title={translate('workspace.expensifyCard.addNewBankAccount')}
onPress={onAddNewBankAccount}
/>
</View>
);
}

return (
<SelectionList
addBottomSafeAreaPadding
data={listOptions}
ListItem={RadioListItem}
onSelectRow={handleSelectRow}
shouldSingleExecuteRowSelect
initiallyFocusedItemKey={initiallyFocusedItemKey}
customListHeaderContent={customHeaderContent}
listFooterContent={listFooterContent}
/>
);
}

export default SettlementAccountSelector;
export {BankAccountListItemLeftElement};
export type {BankAccountListItem, SettlementAccountSelectorProps};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type SetTravelInvoicingSettlementAccountParams = {
policyID: string;
settlementBankAccountID: number;
};

export default SetTravelInvoicingSettlementAccountParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ export type {default as ExportTagsSpreadsheetParams} from './ExportTagsSpreadshe
export type {default as UpdateXeroGenericTypeParams} from './UpdateXeroGenericTypeParams';
export type {default as UpdateCardSettlementFrequencyParams} from './UpdateCardSettlementFrequencyParams';
export type {default as UpdateCardSettlementAccountParams} from './UpdateCardSettlementAccountParams';
export type {default as SetTravelInvoicingSettlementAccountParams} from './SetTravelInvoicingSettlementAccountParams';
export type {default as SetCompanyCardFeedName} from './SetCompanyCardFeedName';
export type {default as DeleteCompanyCardFeed} from './DeleteCompanyCardFeed';
export type {default as SetCompanyCardTransactionLiability} from './SetCompanyCardTransactionLiability';
Expand Down
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,7 @@ const WRITE_COMMANDS = {
DELETE_SAVED_SEARCH: 'DeleteSavedSearch',
UPDATE_CARD_SETTLEMENT_FREQUENCY: 'UpdateCardSettlementFrequency',
UPDATE_CARD_SETTLEMENT_ACCOUNT: 'UpdateCardSettlementAccount',
SET_TRAVEL_INVOICING_SETTLEMENT_ACCOUNT: 'SetTravelInvoicingSettlementAccount',
UPDATE_XERO_IMPORT_TRACKING_CATEGORIES: 'UpdateXeroImportTrackingCategories',
UPDATE_XERO_IMPORT_TAX_RATES: 'UpdateXeroImportTaxRates',
UPDATE_XERO_TENANT_ID: 'UpdateXeroTenantID',
Expand Down Expand Up @@ -1045,6 +1046,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.DELETE_SAVED_SEARCH]: Parameters.DeleteSavedSearchParams;
[WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_FREQUENCY]: Parameters.UpdateCardSettlementFrequencyParams;
[WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_ACCOUNT]: Parameters.UpdateCardSettlementAccountParams;
[WRITE_COMMANDS.SET_TRAVEL_INVOICING_SETTLEMENT_ACCOUNT]: Parameters.SetTravelInvoicingSettlementAccountParams;
[WRITE_COMMANDS.SET_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARDS]: Parameters.SetPersonalDetailsAndShipExpensifyCardsParams;
[WRITE_COMMANDS.SELF_TOUR_VIEWED]: null;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -758,6 +758,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.TRAVEL_SETTINGS_ACCOUNT]: () => require<ReactComponentModule>('../../../../pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage').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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ const WORKSPACE_TO_RHP: Partial<Record<keyof WorkspaceSplitNavigatorParamList, s
SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS,
SCREENS.WORKSPACE.CREATE_DISTANCE_RATE,
],
[SCREENS.WORKSPACE.TRAVEL]: [],
[SCREENS.WORKSPACE.TRAVEL]: [SCREENS.WORKSPACE.TRAVEL_SETTINGS_ACCOUNT],
[SCREENS.WORKSPACE.REPORTS]: [
SCREENS.WORKSPACE.REPORTS_DEFAULT_TITLE,
SCREENS.WORKSPACE.REPORT_FIELDS_CREATE,
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -789,6 +789,9 @@ const config: LinkingOptions<RootNavigatorParamList>['config'] = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SELECT_FEED]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_SELECT_FEED.route,
},
[SCREENS.WORKSPACE.TRAVEL_SETTINGS_ACCOUNT]: {
path: ROUTES.WORKSPACE_TRAVEL_SETTINGS_ACCOUNT.route,
},
[SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS]: {
path: ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS.route,
},
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1258,6 +1258,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_FREQUENCY]: {
policyID: string;
};
[SCREENS.WORKSPACE.TRAVEL_SETTINGS_ACCOUNT]: {
policyID: string;
};
[SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS]: {
policyID: string;
};
Expand Down
19 changes: 18 additions & 1 deletion src/libs/TravelInvoicingUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {OnyxEntry} from 'react-native-onyx';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {BankAccountList} from '@src/types/onyx';
import type ExpensifyCardSettings from '@src/types/onyx/ExpensifyCardSettings';
import {getLastFourDigits} from './BankAccountUtils';
Expand Down Expand Up @@ -86,6 +87,22 @@ function getTravelSettlementFrequency(cardSettings: OnyxEntry<ExpensifyCardSetti
return cardSettings.monthlySettlementDate ? CONST.EXPENSIFY_CARD.FREQUENCY_SETTING.MONTHLY : CONST.EXPENSIFY_CARD.FREQUENCY_SETTING.DAILY;
}

export {getIsTravelInvoicingEnabled, hasTravelInvoicingSettlementAccount, getTravelLimit, getTravelSpend, getTravelSettlementAccount, getTravelSettlementFrequency};
/**
* Gets the Onyx key for Travel Invoicing card settings.
* This function returns a properly typed key without requiring type assertions.
*/
function getTravelInvoicingCardSettingsKey(workspaceAccountID: number): `${typeof ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${number}_${typeof CONST.TRAVEL.PROGRAM_TRAVEL_US}` {
return `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}_${CONST.TRAVEL.PROGRAM_TRAVEL_US}`;
}

export {
getIsTravelInvoicingEnabled,
hasTravelInvoicingSettlementAccount,
getTravelLimit,
getTravelSpend,
getTravelSettlementAccount,
getTravelSettlementFrequency,
getTravelInvoicingCardSettingsKey,
};

export type {TravelSettlementAccountInfo};
81 changes: 75 additions & 6 deletions src/libs/actions/TravelInvoicing.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import Onyx from 'react-native-onyx';
import type {OnyxUpdate} from 'react-native-onyx';
import * as API from '@libs/API';
import type {OpenPolicyTravelPageParams} from '@libs/API/parameters';
import {READ_COMMANDS} from '@libs/API/types';
import type {OpenPolicyTravelPageParams, SetTravelInvoicingSettlementAccountParams} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as ErrorUtils from '@libs/ErrorUtils';
import {getTravelInvoicingCardSettingsKey} from '@libs/TravelInvoicingUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';

/**
Expand Down Expand Up @@ -47,7 +50,73 @@ function openPolicyTravelPage(policyID: string, workspaceAccountID: number) {
API.read(READ_COMMANDS.OPEN_POLICY_TRAVEL_PAGE, params, {optimisticData, successData, failureData});
}

export {
// eslint-disable-next-line import/prefer-default-export
openPolicyTravelPage,
};
/**
* Sets the settlement account for Travel Invoicing.
* Updates the paymentBankAccountID in the Travel Invoicing card settings.
*/
function setTravelInvoicingSettlementAccount(policyID: string, workspaceAccountID: number, settlementBankAccountID: number, previousPaymentBankAccountID?: number) {
const cardSettingsKey = getTravelInvoicingCardSettingsKey(workspaceAccountID);

const optimisticData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: cardSettingsKey,
value: {
paymentBankAccountID: settlementBankAccountID,
previousPaymentBankAccountID,
isLoading: true,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
},
},
];

const successData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: cardSettingsKey,
value: {
paymentBankAccountID: settlementBankAccountID,
previousPaymentBankAccountID: null,
isLoading: false,
pendingAction: null,
},
},
];

const failureData: Array<OnyxUpdate<typeof ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS>> = [
{
onyxMethod: Onyx.METHOD.MERGE,
key: cardSettingsKey,
value: {
// Keep the attempted value visible (grayed out) until error is dismissed
paymentBankAccountID: settlementBankAccountID,
previousPaymentBankAccountID,
isLoading: false,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE,
errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'),
},
},
];

const params: SetTravelInvoicingSettlementAccountParams = {
policyID,
settlementBankAccountID,
};

API.write(WRITE_COMMANDS.SET_TRAVEL_INVOICING_SETTLEMENT_ACCOUNT, params, {optimisticData, successData, failureData});
}

/**
* Clears any errors from the Travel Invoicing settlement account settings.
* Also resets the paymentBankAccountID to the previous valid value (or null if none existed).
*/
function clearTravelInvoicingSettlementAccountErrors(workspaceAccountID: number, paymentBankAccountID: number | null) {
Onyx.merge(getTravelInvoicingCardSettingsKey(workspaceAccountID), {
errors: null,
pendingAction: null,
paymentBankAccountID,
previousPaymentBankAccountID: null,
});
}

export {openPolicyTravelPage, setTravelInvoicingSettlementAccount, clearTravelInvoicingSettlementAccountErrors};
Loading
Loading