diff --git a/src/CONST/index.ts b/src/CONST/index.ts index c3eb2a757fbe..695ea1ad3c03 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3283,9 +3283,9 @@ const CONST = { ALLOW: 'personal', }, STATEMENT_CLOSE_DATE: { - LAST_DAY_OF_MONTH: 'lastDayOfMonth', - LAST_BUSINESS_DAY_OF_MONTH: 'lastBusinessDayOfMonth', - CUSTOM_DAY_OF_MONTH: 'customDayOfMonth', + LAST_DAY_OF_MONTH: 'LAST_DAY_OF_MONTH', + LAST_BUSINESS_DAY_OF_MONTH: 'LAST_BUSINESS_DAY_OF_MONTH', + CUSTOM_DAY_OF_MONTH: 'CUSTOM_DAY_OF_MONTH', }, CARD_LIST_THRESHOLD: 8, DEFAULT_EXPORT_TYPE: 'default', diff --git a/src/hooks/useImportPlaidAccounts.ts b/src/hooks/useImportPlaidAccounts.ts index f0dc26d4b7fc..4a4de355374a 100644 --- a/src/hooks/useImportPlaidAccounts.ts +++ b/src/hooks/useImportPlaidAccounts.ts @@ -13,11 +13,13 @@ export default function useImportPlaidAccounts(policyID?: string) { const plaidFeedName = addNewCard?.data?.plaidConnectedFeedName ?? assignCard?.data?.plaidConnectedFeedName; const plaidAccounts = addNewCard?.data?.plaidAccounts ?? assignCard?.data?.plaidAccounts; const country = addNewCard?.data?.selectedCountry; + const statementPeriodEnd = addNewCard?.data?.statementPeriodEnd; + const statementPeriodEndDay = addNewCard?.data?.statementPeriodEndDay; return useCallback(() => { if (!policyID || !plaidToken || !plaidFeed || !plaidFeedName || !country || !plaidAccounts?.length) { return; } - importPlaidAccounts(plaidToken, plaidFeed, plaidFeedName, country, getDomainNameForPolicy(policyID), JSON.stringify(plaidAccounts)); - }, [country, plaidAccounts, plaidFeed, plaidFeedName, plaidToken, policyID]); + importPlaidAccounts(plaidToken, plaidFeed, plaidFeedName, country, getDomainNameForPolicy(policyID), JSON.stringify(plaidAccounts), statementPeriodEnd, statementPeriodEndDay); + }, [statementPeriodEnd, statementPeriodEndDay, country, plaidAccounts, plaidFeed, plaidFeedName, plaidToken, policyID]); } diff --git a/src/libs/API/parameters/ImportPlaidAccountsParams.ts b/src/libs/API/parameters/ImportPlaidAccountsParams.ts index 7fa34843dcf5..a03a1cc541f5 100644 --- a/src/libs/API/parameters/ImportPlaidAccountsParams.ts +++ b/src/libs/API/parameters/ImportPlaidAccountsParams.ts @@ -1,3 +1,5 @@ +import type {StatementPeriodEnd, StatementPeriodEndDay} from '@src/types/onyx/CardFeeds'; + type ImportPlaidAccountsParams = { publicToken: string; feed: string; @@ -5,6 +7,8 @@ type ImportPlaidAccountsParams = { country: string; domainName: string; plaidAccounts: string; + statementPeriodEnd?: StatementPeriodEnd; + statementPeriodEndDay?: StatementPeriodEndDay; }; export default ImportPlaidAccountsParams; diff --git a/src/libs/API/parameters/RequestFeedSetupParams.ts b/src/libs/API/parameters/RequestFeedSetupParams.ts index 98e22c611efd..3474e7b89dd9 100644 --- a/src/libs/API/parameters/RequestFeedSetupParams.ts +++ b/src/libs/API/parameters/RequestFeedSetupParams.ts @@ -1,8 +1,12 @@ +import type {StatementPeriodEnd, StatementPeriodEndDay} from '@src/types/onyx/CardFeeds'; + type RequestFeedSetupParams = { authToken: string; policyID: string; feedDetails: string; feedType: string; + statementPeriodEnd?: StatementPeriodEnd; + statementPeriodEndDay?: StatementPeriodEndDay; }; export default RequestFeedSetupParams; diff --git a/src/libs/API/parameters/SetFeedStatementPeriodEndDayParams.ts b/src/libs/API/parameters/SetFeedStatementPeriodEndDayParams.ts new file mode 100644 index 000000000000..90af69922877 --- /dev/null +++ b/src/libs/API/parameters/SetFeedStatementPeriodEndDayParams.ts @@ -0,0 +1,12 @@ +import type {StatementPeriodEnd, StatementPeriodEndDay} from '@src/types/onyx/CardFeeds'; + +type SetFeedStatementPeriodEndDayParams = { + authToken: string | null | undefined; + policyID: string; + bankName: string; + domainAccountID: number; + statementPeriodEnd: StatementPeriodEnd | undefined; + statementPeriodEndDay: StatementPeriodEndDay | undefined; +}; + +export default SetFeedStatementPeriodEndDayParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index ac632b550a16..ecb171909c28 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -351,6 +351,7 @@ export type {default as DeleteCompanyCardFeed} from './DeleteCompanyCardFeed'; export type {default as SetCompanyCardTransactionLiability} from './SetCompanyCardTransactionLiability'; export type {default as OpenPolicyCompanyCardsFeedParams} from './OpenPolicyCompanyCardsFeedParams'; export type {default as OpenPolicyAddCardFeedPageParams} from './OpenPolicyAddCardFeedPageParams'; +export type {default as SetFeedStatementPeriodEndDayParams} from './SetFeedStatementPeriodEndDayParams'; export type {default as AssignCompanyCardParams} from './AssignCompanyCardParams'; export type {default as UnassignCompanyCard} from './UnassignCompanyCard'; export type {default as UpdateCompanyCard} from './UpdateCompanyCard'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c4d096d30276..4aab78dfb284 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -455,6 +455,7 @@ const WRITE_COMMANDS = { DELETE_COMPANY_CARD_FEED: 'RemoveFeed', SET_COMPANY_CARD_TRANSACTION_LIABILITY: 'SetFeedTransactionLiability', OPEN_POLICY_ADD_CARD_FEED_PAGE: 'OpenPolicyAddCardFeedPage', + SET_FEED_STATEMENT_PERIOD_END_DAY: 'SetFeedStatementPeriodEndDay', ASSIGN_COMPANY_CARD: 'AssignCard', UNASSIGN_COMPANY_CARD: 'UnassignCard', UPDATE_COMPANY_CARD: 'SyncCard', @@ -562,6 +563,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_CARD_EXPORT_ACCOUNT]: Parameters.SetCompanyCardExportAccountParams; [WRITE_COMMANDS.SET_COMPANY_CARD_TRANSACTION_LIABILITY]: Parameters.SetCompanyCardTransactionLiability; [WRITE_COMMANDS.OPEN_POLICY_ADD_CARD_FEED_PAGE]: Parameters.OpenPolicyAddCardFeedPageParams; + [WRITE_COMMANDS.SET_FEED_STATEMENT_PERIOD_END_DAY]: Parameters.SetFeedStatementPeriodEndDayParams; [WRITE_COMMANDS.VERIFY_IDENTITY]: Parameters.VerifyIdentityParams; [WRITE_COMMANDS.ACCEPT_WALLET_TERMS]: Parameters.AcceptWalletTermsParams; [WRITE_COMMANDS.ANSWER_QUESTIONS_FOR_WALLET]: Parameters.AnswerQuestionsForWalletParams; diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts index 7409a4ee8e72..e478265339f5 100644 --- a/src/libs/actions/CompanyCards.ts +++ b/src/libs/actions/CompanyCards.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; +import type {NullishDeep, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type { @@ -8,6 +8,7 @@ import type { OpenPolicyExpensifyCardsPageParams, RequestFeedSetupParams, SetCompanyCardExportAccountParams, + SetFeedStatementPeriodEndDayParams, UpdateCompanyCardNameParams, } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; @@ -21,7 +22,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Card, CardFeeds} from '@src/types/onyx'; import type {AssignCard, AssignCardData} from '@src/types/onyx/AssignCard'; -import type {AddNewCardFeedData, AddNewCardFeedStep, CardFeedDetails, CompanyCardFeed} from '@src/types/onyx/CardFeeds'; +import type {AddNewCardFeedData, AddNewCardFeedStep, CardFeedData, CardFeedDetails, CompanyCardFeed, StatementPeriodEnd, StatementPeriodEndDay} from '@src/types/onyx/CardFeeds'; import type {OnyxData} from '@src/types/onyx/Request'; type AddNewCompanyCardFlowData = { @@ -47,7 +48,7 @@ function clearAssignCardStepAndData() { Onyx.set(ONYXKEYS.ASSIGN_CARD, {}); } -function setAddNewCompanyCardStepAndData({data, isEditing, step}: AddNewCompanyCardFlowData) { +function setAddNewCompanyCardStepAndData({data, isEditing, step}: NullishDeep) { Onyx.merge(ONYXKEYS.ADD_NEW_COMPANY_CARD, {data, isEditing, currentStep: step}); } @@ -58,7 +59,15 @@ function clearAddNewCardFlow() { }); } -function addNewCompanyCardsFeed(policyID: string | undefined, cardFeed: CompanyCardFeed, feedDetails: CardFeedDetails, cardFeeds: OnyxEntry, lastSelectedFeed?: CompanyCardFeed) { +function addNewCompanyCardsFeed( + policyID: string | undefined, + cardFeed: CompanyCardFeed, + feedDetails: CardFeedDetails, + cardFeeds: OnyxEntry, + statementPeriodEnd: StatementPeriodEnd | undefined, + statementPeriodEndDay: StatementPeriodEndDay | undefined, + lastSelectedFeed?: CompanyCardFeed, +) { const authToken = NetworkStore.getAuthToken(); const workspaceAccountID = PolicyUtils.getWorkspaceAccountID(policyID); @@ -82,6 +91,7 @@ function addNewCompanyCardsFeed(policyID: string | undefined, cardFeed: CompanyC settings: { companyCards: { [feedType]: { + statementPeriodEndDay: statementPeriodEndDay ?? statementPeriodEnd ?? null, errors: null, }, }, @@ -134,6 +144,8 @@ function addNewCompanyCardsFeed(policyID: string | undefined, cardFeed: CompanyC feedDetails: Object.entries(feedDetails) .map(([key, value]) => `${key}: ${value}`) .join(', '), + statementPeriodEnd, + statementPeriodEndDay, }; API.write(WRITE_COMMANDS.REQUEST_FEED_SETUP, parameters, {optimisticData, failureData, successData, finallyData}); @@ -811,6 +823,104 @@ function openPolicyAddCardFeedPage(policyID: string | undefined) { API.write(WRITE_COMMANDS.OPEN_POLICY_ADD_CARD_FEED_PAGE, parameters); } +function setFeedStatementPeriodEndDay( + policyID: string, + bankName: string, + domainAccountID: number, + newStatementPeriodEnd: StatementPeriodEnd | undefined, + newStatementPeriodEndDay: StatementPeriodEndDay | undefined, + oldStatementPeriodEndDay: StatementPeriodEnd | StatementPeriodEndDay | undefined, +) { + const authToken = NetworkStore.getAuthToken(); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, + value: { + settings: { + companyCards: { + [bankName]: { + statementPeriodEndDay: newStatementPeriodEndDay ?? newStatementPeriodEnd ?? null, + pendingFields: { + statementPeriodEndDay: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + errorFields: { + statementPeriodEndDay: null, + }, + }, + }, + }, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, + value: { + settings: { + companyCards: { + [bankName]: { + pendingFields: { + statementPeriodEndDay: null, + }, + }, + }, + }, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, + value: { + settings: { + companyCards: { + [bankName]: { + statementPeriodEndDay: oldStatementPeriodEndDay ?? null, + pendingFields: { + statementPeriodEndDay: null, + }, + errorFields: { + statementPeriodEndDay: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + }, + }, + }, + ]; + + const parameters: SetFeedStatementPeriodEndDayParams = { + authToken, + policyID, + bankName, + domainAccountID, + statementPeriodEnd: newStatementPeriodEnd, + statementPeriodEndDay: newStatementPeriodEndDay, + }; + + API.write(WRITE_COMMANDS.SET_FEED_STATEMENT_PERIOD_END_DAY, parameters, {optimisticData, successData, failureData}); +} + +function clearErrorField(bankName: string, domainAccountID: number, fieldName: keyof CardFeedData) { + Onyx.merge(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_DOMAIN_MEMBER}${domainAccountID}`, { + settings: { + companyCards: { + [bankName]: { + errorFields: { + [fieldName]: null, + }, + }, + }, + }, + }); +} + export { setWorkspaceCompanyCardFeedName, deleteWorkspaceCompanyCardFeed, @@ -831,4 +941,6 @@ export { openAssignFeedCardPage, openPolicyAddCardFeedPage, setTransactionStartDate, + setFeedStatementPeriodEndDay, + clearErrorField, }; diff --git a/src/libs/actions/Plaid.ts b/src/libs/actions/Plaid.ts index 8190a1fcf9af..b9dd3df186db 100644 --- a/src/libs/actions/Plaid.ts +++ b/src/libs/actions/Plaid.ts @@ -6,6 +6,7 @@ import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import getPlaidLinkTokenParameters from '@libs/getPlaidLinkTokenParameters'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {StatementPeriodEnd, StatementPeriodEndDay} from '@src/types/onyx/CardFeeds'; /** * Gets the Plaid Link token used to initialize the Plaid SDK @@ -116,7 +117,16 @@ function openPlaidBankAccountSelector(publicToken: string, bankName: string, all }); } -function importPlaidAccounts(publicToken: string, feed: string, feedName: string, country: string, domainName: string, plaidAccounts: string) { +function importPlaidAccounts( + publicToken: string, + feed: string, + feedName: string, + country: string, + domainName: string, + plaidAccounts: string, + statementPeriodEnd: StatementPeriodEnd | undefined, + statementPeriodEndDay: StatementPeriodEndDay | undefined, +) { const parameters: ImportPlaidAccountsParams = { publicToken, feed, @@ -124,6 +134,8 @@ function importPlaidAccounts(publicToken: string, feed: string, feedName: string country, domainName, plaidAccounts, + statementPeriodEnd, + statementPeriodEndDay, }; API.write(WRITE_COMMANDS.IMPORT_PLAID_ACCOUNTS, parameters); diff --git a/src/pages/workspace/companyCards/BankConnection/index.native.tsx b/src/pages/workspace/companyCards/BankConnection/index.native.tsx index d8f953ee808f..2d0119f01cdd 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.native.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.native.tsx @@ -114,7 +114,15 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti if (newFeed) { updateSelectedFeed(newFeed, policyID); } - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + + // Direct feeds (except those added via Plaid) are created with default statement period end date. + // Redirect the user to set a custom date. + if (policyID && !isPlaid) { + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS_STATEMENT_CLOSE_DATE.getRoute(policyID)); + } else { + Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); + } } if (isPlaid) { onImportPlaidAccounts(); diff --git a/src/pages/workspace/companyCards/BankConnection/index.tsx b/src/pages/workspace/companyCards/BankConnection/index.tsx index 6ae17dd42b04..3fbeb4a0b485 100644 --- a/src/pages/workspace/companyCards/BankConnection/index.tsx +++ b/src/pages/workspace/companyCards/BankConnection/index.tsx @@ -145,8 +145,16 @@ function BankConnection({policyID: policyIDFromProps, feed, route}: BankConnecti if (newFeed) { updateSelectedFeed(newFeed, policyID); } - Navigation.closeRHPFlow(); - Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + + // Direct feeds (except those added via Plaid) are created with default statement period end date. + // Redirect the user to set a custom date. + if (policyID && !isPlaid) { + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS_STATEMENT_CLOSE_DATE.getRoute(policyID)); + } else { + Navigation.closeRHPFlow(); + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID), {forceReplace: true}); + } return; } if (!shouldBlockWindowOpen) { diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardStatementCloseDatePage.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardStatementCloseDatePage.tsx index 5d5ff1b8f0f5..d3b793456fc4 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardStatementCloseDatePage.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardStatementCloseDatePage.tsx @@ -1,9 +1,11 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useCardFeeds from '@hooks/useCardFeeds'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import {getSelectedFeed} from '@libs/CardUtils'; +import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID'; +import {clearErrorField, setFeedStatementPeriodEndDay} from '@libs/actions/CompanyCards'; +import {getCompanyFeeds, getDomainOrWorkspaceAccountID, getSelectedFeed} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -12,7 +14,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {CompanyCardStatementCloseDate} from '@src/types/onyx/CardFeeds'; +import type {StatementPeriodEnd, StatementPeriodEndDay} from '@src/types/onyx/CardFeeds'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import WorkspaceCompanyCardStatementCloseDateSelectionList from './WorkspaceCompanyCardStatementCloseDateSelectionList'; @@ -27,24 +29,48 @@ function WorkspaceCompanyCardStatementCloseDatePage({ const [lastSelectedFeed, lastSelectedFeedResult] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, {canBeMissing: true}); const [cardFeeds, cardFeedsResult] = useCardFeeds(policyID); const selectedFeed = getSelectedFeed(lastSelectedFeed, cardFeeds); + const workspaceAccountID = useWorkspaceAccountID(policyID); + const companyFeeds = getCompanyFeeds(cardFeeds); + const selectedFeedData = selectedFeed ? companyFeeds[selectedFeed] : undefined; + const domainOrWorkspaceAccountID = getDomainOrWorkspaceAccountID(workspaceAccountID, selectedFeedData); + const statementPeriodEndDay = selectedFeedData?.statementPeriodEndDay; + + const [defaultStatementPeriodEnd, defaultStatementPeriodEndDay] = useMemo(() => { + if (!statementPeriodEndDay) { + return [undefined, undefined]; + } + + if (typeof statementPeriodEndDay === 'number') { + return [undefined, statementPeriodEndDay]; + } + + return [statementPeriodEndDay, undefined]; + }, [statementPeriodEndDay]); const submit = useCallback( - // s77rt make use of statementCloseDate / statementCustomCloseDate and remove disable lint rule - // eslint-disable-next-line @typescript-eslint/no-unused-vars - (statementCloseDate: CompanyCardStatementCloseDate, statementCustomCloseDate: number | undefined) => { - if (selectedFeed) { - // s77rt call API command + (newStatementPeriodEnd: StatementPeriodEnd | undefined, newStatementPeriodEndDay: StatementPeriodEndDay | undefined) => { + const isChangedValue = (newStatementPeriodEndDay ?? newStatementPeriodEnd) !== statementPeriodEndDay; + if (selectedFeed && isChangedValue) { + setFeedStatementPeriodEndDay(policyID, selectedFeed, domainOrWorkspaceAccountID, newStatementPeriodEnd, newStatementPeriodEndDay, statementPeriodEndDay); } Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS.getRoute(policyID)); }, - [policyID, selectedFeed], + [policyID, selectedFeed, statementPeriodEndDay, domainOrWorkspaceAccountID], ); const goBack = useCallback(() => { Navigation.goBack(); }, []); + const clearError = useCallback(() => { + if (!selectedFeed) { + return; + } + + clearErrorField(selectedFeed, domainOrWorkspaceAccountID, 'statementPeriodEndDay'); + }, [selectedFeed, domainOrWorkspaceAccountID]); + if (isLoadingOnyxValue(cardFeedsResult) || isLoadingOnyxValue(lastSelectedFeedResult)) { return ; } @@ -60,6 +86,11 @@ function WorkspaceCompanyCardStatementCloseDatePage({ onSubmit={submit} onBackButtonPress={goBack} enabledWhenOffline + defaultStatementPeriodEnd={defaultStatementPeriodEnd} + defaultStatementPeriodEndDay={defaultStatementPeriodEndDay} + pendingAction={selectedFeedData?.pendingFields?.statementPeriodEndDay} + errors={selectedFeedData?.errorFields?.statementPeriodEndDay} + onCloseError={clearError} /> ); diff --git a/src/pages/workspace/companyCards/WorkspaceCompanyCardStatementCloseDateSelectionList/index.tsx b/src/pages/workspace/companyCards/WorkspaceCompanyCardStatementCloseDateSelectionList/index.tsx index 7d56439f97e2..c47c410ddb63 100644 --- a/src/pages/workspace/companyCards/WorkspaceCompanyCardStatementCloseDateSelectionList/index.tsx +++ b/src/pages/workspace/companyCards/WorkspaceCompanyCardStatementCloseDateSelectionList/index.tsx @@ -1,10 +1,12 @@ import React, {useCallback, useMemo, useState} from 'react'; import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; import FixedFooter from '@components/FixedFooter'; import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; import FormHelpMessage from '@components/FormHelpMessage'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import SingleSelectListItem from '@components/SelectionList/SingleSelectListItem'; @@ -13,19 +15,26 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -import type {CompanyCardStatementCloseDate} from '@src/types/onyx/CardFeeds'; +import type {StatementPeriodEnd, StatementPeriodEndDay} from '@src/types/onyx/CardFeeds'; +import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import CustomCloseDateSelectionList from './CustomCloseDateSelectionList'; +type CompanyCardStatementCloseDate = ValueOf; + type StatementCloseDateListItem = ListItem & { value: CompanyCardStatementCloseDate; }; type WorkspaceCompanyCardStatementCloseDateSelectionListProps = { confirmText: string; - onSubmit: (statementCloseDate: CompanyCardStatementCloseDate, statementCustomCloseDate: number | undefined) => void; + onSubmit: (statementPeriodEnd: StatementPeriodEnd | undefined, statementPeriodEndDay: StatementPeriodEndDay | undefined) => void; onBackButtonPress: () => void; enabledWhenOffline: boolean; - defaultDate?: CompanyCardStatementCloseDate; + defaultStatementPeriodEnd?: StatementPeriodEnd; + defaultStatementPeriodEndDay?: StatementPeriodEndDay; + pendingAction?: PendingAction; + errors?: Errors | null; + onCloseError?: () => void; }; function WorkspaceCompanyCardStatementCloseDateSelectionList({ @@ -33,16 +42,24 @@ function WorkspaceCompanyCardStatementCloseDateSelectionList({ onSubmit, onBackButtonPress, enabledWhenOffline, - defaultDate, + defaultStatementPeriodEnd, + defaultStatementPeriodEndDay, + pendingAction, + errors, + onCloseError, }: WorkspaceCompanyCardStatementCloseDateSelectionListProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); - const [selectedDate, setSelectedDate] = useState(defaultDate); - const [selectedCustomDate, setSelectedCustomDate] = useState(undefined); - const [error, setError] = useState(undefined); - + const [selectedDate, setSelectedDate] = useState(() => { + if (defaultStatementPeriodEndDay) { + return CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.CUSTOM_DAY_OF_MONTH; + } + return defaultStatementPeriodEnd; + }); + const [selectedCustomDate, setSelectedCustomDate] = useState(defaultStatementPeriodEndDay); const [isChoosingCustomDate, setIsChoosingCustomDate] = useState(false); + const [error, setError] = useState(undefined); const title = useMemo( () => (isChoosingCustomDate ? translate('workspace.companyCards.customCloseDate') : translate('workspace.moreFeatures.companyCards.statementCloseDateTitle')), @@ -73,12 +90,22 @@ function WorkspaceCompanyCardStatementCloseDateSelectionList({ ); const submit = useCallback(() => { - if (!selectedDate || (selectedDate === CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.CUSTOM_DAY_OF_MONTH && !selectedCustomDate)) { + if (!selectedDate) { setError(translate('workspace.moreFeatures.companyCards.error.statementCloseDateRequired')); return; } - onSubmit(selectedDate, selectedDate === CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.CUSTOM_DAY_OF_MONTH ? selectedCustomDate : undefined); + if (selectedDate === CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.CUSTOM_DAY_OF_MONTH) { + if (!selectedCustomDate) { + setError(translate('workspace.moreFeatures.companyCards.error.statementCloseDateRequired')); + return; + } + + onSubmit(undefined, selectedCustomDate); + return; + } + + onSubmit(selectedDate, undefined); }, [selectedDate, selectedCustomDate, onSubmit, translate]); return ( @@ -101,31 +128,38 @@ function WorkspaceCompanyCardStatementCloseDateSelectionList({ <> {translate('workspace.moreFeatures.companyCards.statementCloseDateDescription')} - - {Object.values(CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE)?.map((option) => ( - - ))} - {selectedDate === CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.CUSTOM_DAY_OF_MONTH && ( - setIsChoosingCustomDate(true)} - viewMode={CONST.OPTION_MODE.COMPACT} - /> - )} - + + + {Object.values(CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE)?.map((option) => ( + + ))} + {selectedDate === CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.CUSTOM_DAY_OF_MONTH && ( + setIsChoosingCustomDate(true)} + viewMode={CONST.OPTION_MODE.COMPACT} + /> + )} + + { + if (!selectedFeedData?.statementPeriodEndDay) { + return undefined; + } + + if (typeof selectedFeedData?.statementPeriodEndDay === 'number') { + return selectedFeedData.statementPeriodEndDay; + } - // s77rt remove DEV lock - const shouldShowStatementCloseDate = isDevelopment; + return translate(`workspace.companyCards.statementCloseDate.${selectedFeedData.statementPeriodEndDay}`); + }, [translate, selectedFeedData?.statementPeriodEndDay]); const navigateToChangeFeedName = () => { Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS_SETTINGS_FEED_NAME.getRoute(policyID)); @@ -118,16 +124,17 @@ function WorkspaceCompanyCardsSettingsPage({ titleStyle={styles.flex1} onPress={navigateToChangeFeedName} /> - {shouldShowStatementCloseDate && ( + - )} + ; case CONST.COMPANY_CARDS.STEP.CARD_DETAILS: - return ; + return ; case CONST.COMPANY_CARDS.STEP.AMEX_CUSTOM_FEED: return ; case CONST.COMPANY_CARDS.STEP.PLAID_CONNECTION: diff --git a/src/pages/workspace/companyCards/addNew/DetailsStep.tsx b/src/pages/workspace/companyCards/addNew/DetailsStep.tsx index 9d98f557ddf3..beb78e58876f 100644 --- a/src/pages/workspace/companyCards/addNew/DetailsStep.tsx +++ b/src/pages/workspace/companyCards/addNew/DetailsStep.tsx @@ -11,64 +11,45 @@ import Text from '@components/Text'; import TextInput from '@components/TextInput'; import TextLink from '@components/TextLink'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; -import useCardFeeds from '@hooks/useCardFeeds'; -import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {getFieldRequiredErrors} from '@libs/ValidationUtils'; -import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; -import {addNewCompanyCardsFeed, setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; +import {setAddNewCompanyCardStepAndData} from '@userActions/CompanyCards'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import INPUT_IDS from '@src/types/form/AddNewCardFeedForm'; -type DetailsStepProps = { - /** ID of the current policy */ - policyID: string | undefined; -}; - -function DetailsStep({policyID}: DetailsStepProps) { +function DetailsStep() { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); const {inputCallbackRef} = useAutoFocusInput(); - const {isDevelopment} = useEnvironment(); const [addNewCard] = useOnyx(ONYXKEYS.ADD_NEW_COMPANY_CARD, {canBeMissing: false}); - const [lastSelectedFeed] = useOnyx(`${ONYXKEYS.COLLECTION.LAST_SELECTED_FEED}${policyID}`, {canBeMissing: true}); - - const [cardFeeds] = useCardFeeds(policyID); const feedProvider = addNewCard?.data?.feedType; const isStripeFeedProvider = feedProvider === CONST.COMPANY_CARD.FEED_BANK_NAME.STRIPE; const bank = addNewCard?.data?.selectedBank; const isOtherBankSelected = bank === CONST.COMPANY_CARDS.BANKS.OTHER; - // s77rt remove DEV lock - const shouldSelectStatementCloseDate = isDevelopment; - - const submit = (values: FormOnyxValues) => { - if (!addNewCard?.data) { - return; - } + const submit = useCallback( + (values: FormOnyxValues) => { + if (!addNewCard?.data) { + return; + } - const feedDetails = { - ...values, - bankName: addNewCard.data.bankName ?? 'Amex', - }; + const feedDetails = { + ...values, + bankName: addNewCard.data.bankName ?? 'Amex', + }; - if (shouldSelectStatementCloseDate) { setAddNewCompanyCardStepAndData({step: CONST.COMPANY_CARDS.STEP.SELECT_STATEMENT_CLOSE_DATE, data: {feedDetails}}); - return; - } - - addNewCompanyCardsFeed(policyID, addNewCard.data.feedType, feedDetails, cardFeeds, lastSelectedFeed); - Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); - }; + }, + [addNewCard?.data], + ); const handleBackButtonPress = () => { if (isOtherBankSelected) { @@ -211,7 +192,7 @@ function DetailsStep({policyID}: DetailsStepProps) { /> !!plaidData?.bankAccounts?.length || !isEmptyObject(plaidData?.errors), [plaidData]); /** @@ -134,6 +129,8 @@ function PlaidConnectionStep({feed, policyID}: {feed?: CompanyCardFeed; policyID addNewCard.data.selectedCountry, getDomainNameForPolicy(policyID), JSON.stringify(metadata?.accounts), + addNewCard.data.statementPeriodEnd, + addNewCard.data.statementPeriodEndDay, ); InteractionManager.runAfterInteractions(() => { setAssignCardStepAndData({ @@ -161,7 +158,7 @@ function PlaidConnectionStep({feed, policyID}: {feed?: CompanyCardFeed; policyID } setAddNewCompanyCardStepAndData({ - step: shouldSelectStatementCloseDate ? CONST.COMPANY_CARDS.STEP.SELECT_STATEMENT_CLOSE_DATE : CONST.COMPANY_CARDS.STEP.BANK_CONNECTION, + step: CONST.COMPANY_CARDS.STEP.SELECT_STATEMENT_CLOSE_DATE, data: { publicToken, plaidConnectedFeed, diff --git a/src/pages/workspace/companyCards/addNew/StatementCloseDateStep.tsx b/src/pages/workspace/companyCards/addNew/StatementCloseDateStep.tsx index c4d041af0884..045b784c01d5 100644 --- a/src/pages/workspace/companyCards/addNew/StatementCloseDateStep.tsx +++ b/src/pages/workspace/companyCards/addNew/StatementCloseDateStep.tsx @@ -9,7 +9,7 @@ import WorkspaceCompanyCardStatementCloseDateSelectionList from '@pages/workspac import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {CompanyCardStatementCloseDate} from '@src/types/onyx/CardFeeds'; +import type {StatementPeriodEnd, StatementPeriodEndDay} from '@src/types/onyx/CardFeeds'; type StatementCloseDateStepProps = { /** ID of the current policy */ @@ -26,18 +26,18 @@ function StatementCloseDateStep({policyID}: StatementCloseDateStepProps) { const isPlaid = isBetaEnabled(CONST.BETAS.PLAID_COMPANY_CARDS) && !!addNewCard?.data?.publicToken; const submit = useCallback( - // s77rt make use of statementCloseDate / statementCustomCloseDate and remove disable lint rule - // eslint-disable-next-line @typescript-eslint/no-unused-vars - (statementCloseDate: CompanyCardStatementCloseDate, statementCustomCloseDate: number | undefined) => { + (statementPeriodEnd: StatementPeriodEnd | undefined, statementPeriodEndDay: StatementPeriodEndDay | undefined) => { if (isPlaid) { setAddNewCompanyCardStepAndData({ step: CONST.COMPANY_CARDS.STEP.BANK_CONNECTION, + // Fallback to null to clear old value (if any) because `undefined` is a no-op in Onyx.merge + data: {statementPeriodEnd: statementPeriodEnd ?? null, statementPeriodEndDay: statementPeriodEndDay ?? null}, }); return; } if (addNewCard?.data.feedDetails) { - addNewCompanyCardsFeed(policyID, addNewCard.data.feedType, addNewCard.data.feedDetails, cardFeeds, lastSelectedFeed); + addNewCompanyCardsFeed(policyID, addNewCard.data.feedType, addNewCard.data.feedDetails, cardFeeds, statementPeriodEnd, statementPeriodEndDay, lastSelectedFeed); Navigation.goBack(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policyID)); } }, @@ -54,7 +54,7 @@ function StatementCloseDateStep({policyID}: StatementCloseDateStepProps) { onSubmit={submit} onBackButtonPress={goBack} enabledWhenOffline={false} - defaultDate={CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH} + defaultStatementPeriodEnd={CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.LAST_DAY_OF_MONTH} /> ); } diff --git a/src/types/onyx/CardFeeds.ts b/src/types/onyx/CardFeeds.ts index 88ecfb9093c3..e73e95a6ce84 100644 --- a/src/types/onyx/CardFeeds.ts +++ b/src/types/onyx/CardFeeds.ts @@ -4,15 +4,18 @@ import type {ValueOf} from 'type-fest'; import type CONST from '@src/CONST'; import type * as OnyxCommon from './OnyxCommon'; -/** Card statement close date */ -type CompanyCardStatementCloseDate = ValueOf; - /** Card feed */ type CompanyCardFeed = ValueOf; /** Custom card feed with a number */ type CompanyCardFeedWithNumber = CompanyCardFeed | `${CompanyCardFeed}${number}`; +/** Statement period end */ +type StatementPeriodEnd = Exclude, typeof CONST.COMPANY_CARDS.STATEMENT_CLOSE_DATE.CUSTOM_DAY_OF_MONTH>; + +/** Statement period end day */ +type StatementPeriodEndDay = number; + /** Card feed provider */ type CardFeedProvider = | typeof CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD @@ -64,11 +67,16 @@ type CustomCardFeedData = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Specifies the format for the report title related to this card */ reportTitleFormat?: string; - /** Indicates the day when the statement period for this card ends */ - statementPeriodEndDay?: string; + /** Indicates the day when the statement period for this card ends. + * The BE returns a unified key which may hold either a preset value (string) or a custom day (integer) + */ + statementPeriodEndDay?: StatementPeriodEnd | StatementPeriodEndDay; /** Plaid access token */ plaidAccessToken?: string; + + /** Field-specific error messages */ + errorFields?: OnyxCommon.ErrorFields<'statementPeriodEndDay'>; }>; /** Direct card feed data */ @@ -91,11 +99,16 @@ type DirectCardFeedData = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Whether any actions are pending */ pending?: boolean; - /** Indicates the day when the statement period for this card ends */ - statementPeriodEndDay?: string; + /** Indicates the day when the statement period for this card ends. + * The BE returns a unified key which may hold either a preset value (string) or a custom day (integer) + */ + statementPeriodEndDay?: StatementPeriodEnd | StatementPeriodEndDay; /** Plaid access token */ plaidAccessToken?: string; + + /** Field-specific error messages */ + errorFields?: OnyxCommon.ErrorFields<'statementPeriodEndDay'>; }>; /** Card feed data */ @@ -136,6 +149,12 @@ type AddNewCardFeedData = { /** Name of the card */ cardTitle: string; + /** Indicates the day (preset value) when the statement period for this card ends */ + statementPeriodEnd?: StatementPeriodEnd; + + /** Indicates the day (custom day) when the statement period for this card ends */ + statementPeriodEndDay?: StatementPeriodEndDay; + /** Selected bank */ selectedBank: ValueOf | null; @@ -196,5 +215,6 @@ export type { CompanyCardNicknames, CompanyCardFeedWithNumber, FundID, - CompanyCardStatementCloseDate, + StatementPeriodEnd, + StatementPeriodEndDay, };