Skip to content

Commit

Permalink
Merge pull request #45415 from koko57/feature/44311-card-settings-page
Browse files Browse the repository at this point in the history
[No QA] Card settings page
  • Loading branch information
MariaHCD authored Jul 22, 2024
2 parents 0673b9b + eb2323b commit 50ebe95
Show file tree
Hide file tree
Showing 26 changed files with 529 additions and 19 deletions.
6 changes: 6 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ const CONST = {

MERCHANT_NAME_MAX_LENGTH: 255,

MASKED_PAN_PREFIX: 'XXXXXXXXXXXX',

REQUEST_PREVIEW: {
MAX_LENGTH: 83,
},
Expand Down Expand Up @@ -2248,6 +2250,10 @@ const CONST = {
PHYSICAL: 'physical',
VIRTUAL: 'virtual',
},
FREQUENCY_SETTING: {
DAILY: 'daily',
MONTHLY: 'monthly',
},
},
AVATAR_ROW_SIZE: {
DEFAULT: 4,
Expand Down
6 changes: 3 additions & 3 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,10 +452,10 @@ const ONYXKEYS = {
*/
WORKSPACE_CARDS_LIST: 'card_',

/** The bank account that Expensify Card payments will be reconciled against */
/** Stores which connection is set up to use Continuous Reconciliation */
SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION: 'sharedNVP_expensifyCard_continuousReconciliationConnection_',

/** If continuous reconciliation is enabled */
/** The value that indicates whether Continuous Reconciliation should be used on the domain */
SHARED_NVP_EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION: 'sharedNVP_expensifyCard_useContinuousReconciliation_',
},

Expand Down Expand Up @@ -713,7 +713,7 @@ type OnyxCollectionValuesMapping = {
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_USER_BILLING_GRACE_PERIOD_END]: OnyxTypes.BillingGraceEndPeriod;
[ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS]: OnyxTypes.ExpensifyCardSettings;
[ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST]: OnyxTypes.WorkspaceCardsList;
[ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.BankAccount;
[ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION]: OnyxTypes.PolicyConnectionName;
[ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION]: boolean;
};

Expand Down
12 changes: 12 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -888,6 +888,18 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/expensify-card/choose-bank-account',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/choose-bank-account` as const,
},
WORKSPACE_EXPENSIFY_CARD_SETTINGS: {
route: 'settings/workspaces/:policyID/expensify-card/settings',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/settings` as const,
},
WORKSPACE_EXPENSIFY_CARD_SETTINGS_ACCOUNT: {
route: 'settings/workspaces/:policyID/expensify-card/settings/account',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/expensify-card/settings/account` as const,
},
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,
},
WORKSPACE_DISTANCE_RATES: {
route: 'settings/workspaces/:policyID/distance-rates',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/distance-rates` as const,
Expand Down
3 changes: 3 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,9 @@ const SCREENS = {
EXPENSIFY_CARD_ISSUE_NEW: 'Workspace_ExpensifyCard_New',
EXPENSIFY_CARD_NAME: 'Workspace_ExpensifyCard_Name',
EXPENSIFY_CARD_BANK_ACCOUNT: 'Workspace_ExpensifyCard_BankAccount',
EXPENSIFY_CARD_SETTINGS: 'Workspace_ExpensifyCard_Settings',
EXPENSIFY_CARD_SETTINGS_ACCOUNT: 'Workspace_ExpensifyCard_Settings_Account',
EXPENSIFY_CARD_SETTINGS_FREQUENCY: 'Workspace_ExpensifyCard_Settings_Frequency',
BILLS: 'Workspace_Bills',
INVOICES: 'Workspace_Invoices',
TRAVEL: 'Workspace_Travel',
Expand Down
2 changes: 1 addition & 1 deletion src/components/FormHelpMessage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Text from './Text';

type FormHelpMessageProps = {
/** Error or hint text. Ignored when children is not empty */
message?: string;
message?: string | React.ReactNode;

/** Children to render next to dot indicator */
children?: React.ReactNode;
Expand Down
2 changes: 1 addition & 1 deletion src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ type MenuItemBaseProps = {
errorTextStyle?: StyleProp<ViewStyle>;

/** Hint to display at the bottom of the component */
hintText?: string;
hintText?: string | ReactNode;

/** Should the error text red dot indicator be shown */
shouldShowRedDotIndicator?: boolean;
Expand Down
1 change: 1 addition & 0 deletions src/components/SelectionList/RadioListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ function RadioListItem<TItem extends ListItem>({
pendingAction={item.pendingAction}
>
<>
{!!item.leftElement && item.leftElement}
<View style={[styles.flex1, styles.alignItemsStart]}>
<TextWithTooltip
shouldShowTooltip={showTooltip}
Expand Down
3 changes: 3 additions & 0 deletions src/components/SelectionList/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ type ListItem = {
/** User login */
login?: string | null;

/** Element to show on the left side of the item */
leftElement?: ReactNode;

/** Element to show on the right side of the item */
rightElement?: ReactNode;

Expand Down
12 changes: 12 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2672,6 +2672,18 @@ export default {
chooseExistingBank: 'Choose an existing business bank account to pay your Expensify Card balance, or add a new bank account',
accountEndingIn: 'Account ending in',
addNewBankAccount: 'Add a new bank account',
settlementAccount: 'Settlement account',
settlementAccountDescription: 'Choose an account to pay your Expensify Card balance.',
settlementAccountInfoPt1: 'Make sure this account matches your',
settlementAccountInfoPt2: 'so Continuous Reconciliation works properly.',
reconciliationAccount: 'Reconciliation account',
settlementFrequency: 'Settlement frequency',
settlementFrequencyDescription: 'Choose how often you’ll pay your Expensify Card balance.',
settlementFrequencyInfo: 'If you’d like to switch to monthly settlement, you’ll need to connect your bank account via Plaid and have a positive 90-day balance history.',
frequency: {
daily: 'Daily',
monthly: 'Monthly',
},
cardDetails: 'Card details',
virtual: 'Virtual',
physical: 'Physical',
Expand Down
15 changes: 14 additions & 1 deletion src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2716,12 +2716,25 @@ export default {
'A la hora de calcular tu límite restante, tenemos en cuenta una serie de factores: su antigüedad como cliente, la información relacionada con tu negocio que nos facilitaste al darte de alta y el efectivo disponible en tu cuenta bancaria comercial. Tu límite restante puede fluctuar a diario.',
cashBack: 'Reembolso',
cashBackDescription: 'El saldo de devolución se basa en el gasto mensual realizado con la tarjeta Expensify en tu espacio de trabajo.',
issueNewCard: '',
issueNewCard: 'Emitir nueva tarjeta',
finishSetup: 'Terminar configuración',
chooseBankAccount: 'Elegir cuenta bancaria',
chooseExistingBank: 'Elige una cuenta bancaria comercial existente para pagar el saldo de su Tarjeta Expensify o añade una nueva cuenta bancaria.',
accountEndingIn: 'Cuenta terminada en',
addNewBankAccount: 'Añadir nueva cuenta bancaria',
settlementAccount: 'Cuenta de liquidación',
settlementAccountDescription: 'Elige una cuenta para pagar el saldo de tu Tarjeta Expensify.',
settlementAccountInfoPt1: 'Asegúrate de que esta cuenta coincide con tu',
settlementAccountInfoPt2: 'para que Reconciliación Continua funcione correctamente.',
reconciliationAccount: 'Cuenta de conciliación',
settlementFrequency: 'Frecuencia de liquidación',
settlementFrequencyDescription: 'Elige con qué frecuencia pagarás el saldo de tu Tarjeta Expensify',
settlementFrequencyInfo:
'Si deseas cambiar a la liquidación mensual, deberás conectar tu cuenta bancaria a través de Plaid y tener un historial de saldo positivo en los últimos 90 días.',
frequency: {
daily: 'Cada día',
monthly: 'Mensual',
},
cardDetails: 'Datos de la tarjeta',
virtual: 'Virtual',
physical: 'Física',
Expand Down
8 changes: 6 additions & 2 deletions src/libs/BankAccountUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,9 @@ import type * as OnyxTypes from '@src/types/onyx';
function getDefaultCompanyWebsite(session: OnyxEntry<OnyxTypes.Session>, user: OnyxEntry<OnyxTypes.User>): string {
return user?.isFromPublicDomain ? 'https://' : `https://www.${Str.extractEmailDomain(session?.email ?? '')}`;
}
// eslint-disable-next-line import/prefer-default-export
export {getDefaultCompanyWebsite};

function getLastFourDigits(bankAccountNumber: string): string {
return bankAccountNumber ? bankAccountNumber.slice(-4) : '';
}

export {getDefaultCompanyWebsite, getLastFourDigits};
11 changes: 10 additions & 1 deletion src/libs/CardUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import CONST from '@src/CONST';
import type {TranslationPaths} from '@src/languages/types';
import type {OnyxValues} from '@src/ONYXKEYS';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Card, CardList} from '@src/types/onyx';
import type {BankAccountList, Card, CardList} from '@src/types/onyx';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import * as Localize from './Localize';

let allCards: OnyxValues[typeof ONYXKEYS.CARD_LIST] = {};
Expand Down Expand Up @@ -158,6 +159,13 @@ function getTranslationKeyForLimitType(limitType: ValueOf<typeof CONST.EXPENSIFY
}
}

function getEligibleBankAccountsForCard(bankAccountsList: OnyxEntry<BankAccountList>) {
if (!bankAccountsList || isEmptyObject(bankAccountsList)) {
return [];
}
return Object.values(bankAccountsList).filter((bankAccount) => bankAccount?.accountData?.type === CONST.BANK_ACCOUNT.TYPE.BUSINESS && bankAccount?.accountData?.allowDebit);
}

export {
isExpensifyCard,
isCorporateCard,
Expand All @@ -171,4 +179,5 @@ export {
hasDetectedFraud,
getMCardNumberString,
getTranslationKeyForLimitType,
getEligibleBankAccountsForCard,
};
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,9 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.TAX_CREATE]: () => require<ReactComponentModule>('../../../../pages/workspace/taxes/WorkspaceCreateTaxPage').default,
[SCREENS.WORKSPACE.TAX_CODE]: () => require<ReactComponentModule>('../../../../pages/workspace/taxes/WorkspaceTaxCodePage').default,
[SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require<ReactComponentModule>('../../../../pages/workspace/card/issueNew/IssueNewCardPage').default,
[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_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 @@ -159,6 +159,9 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial<Record<FullScreenName, string[]>> = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: [
SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW,
SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT,
SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS,
SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT,
SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_FREQUENCY,
SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS,
SCREENS.WORKSPACE.EXPENSIFY_CARD_NAME,
SCREENS.WORKSPACE.EXPENSIFY_CARD_LIMIT,
Expand Down
9 changes: 9 additions & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,15 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_BANK_ACCOUNT.route,
},
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS.route,
},
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_FREQUENCY]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS_FREQUENCY.route,
},
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_SETTINGS_ACCOUNT.route,
},
[SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: {
path: ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.route,
},
Expand Down
12 changes: 9 additions & 3 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -671,6 +671,15 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT]: {
policyID: string;
};
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS]: {
policyID: string;
};
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_ACCOUNT]: {
policyID: string;
};
[SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS_FREQUENCY]: {
policyID: string;
};
[SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: {
policyID: string;
cardID: string;
Expand Down Expand Up @@ -1094,9 +1103,6 @@ type FullScreenNavigatorParamList = {
[SCREENS.WORKSPACE.WORKFLOWS]: {
policyID: string;
};
[SCREENS.WORKSPACE.EXPENSIFY_CARD]: {
policyID: string;
};
[SCREENS.WORKSPACE.WORKFLOWS_APPROVER]: {
policyID: string;
};
Expand Down
99 changes: 99 additions & 0 deletions src/libs/actions/Card.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Onyx from 'react-native-onyx';
import type {OnyxUpdate} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
import * as API from '@libs/API';
import type {
ActivatePhysicalExpensifyCardParams,
Expand Down Expand Up @@ -204,6 +205,102 @@ function revealVirtualCardDetails(cardID: number): Promise<ExpensifyCardDetails>
});
}

function updateSettlementFrequency(policyID: string, frequency: ValueOf<typeof CONST.EXPENSIFY_CARD.FREQUENCY_SETTING>) {
// TODO: remove this code when the API is ready
if (frequency === CONST.EXPENSIFY_CARD.FREQUENCY_SETTING.DAILY) {
Onyx.merge(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policyID}`, {
monthlySettlementDate: null,
});
} else {
Onyx.merge(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policyID}`, {
monthlySettlementDate: new Date(),
});
}

// TODO: uncomment this code when the API is ready
// const optimisticData: OnyxUpdate[] = [
// {
// onyxMethod: Onyx.METHOD.MERGE,
// key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policyID}`,
// value: {
// monthlySettlementDate: '',
// },
// },
// ];
//
// const successData: OnyxUpdate[] = [
// {
// onyxMethod: Onyx.METHOD.MERGE,
// key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policyID}`,
// value: {
// monthlySettlementDate: '',
// },
// },
// ];
//
// const failureData: OnyxUpdate[] = [
// {
// onyxMethod: Onyx.METHOD.MERGE,
// key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policyID}`,
// value: {
// monthlySettlementDate: null,
// },
// },
// ];
//
// const parameters = {
// workspaceAccountID: policyID,
// settlementFrequency: frequency,
// };
//
// API.write(WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_FREQUENCY, parameters, {optimisticData, successData, failureData});
}

function updateSettlementAccount(policyID: string, accountID: number) {
// TODO: remove this code when the API is ready
Onyx.merge(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policyID}`, {
paymentBankAccountID: accountID,
});

// TODO: uncomment this code when the API is ready
// const optimisticData: OnyxUpdate[] = [
// {
// onyxMethod: Onyx.METHOD.MERGE,
// key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policyID}`,
// value: {
// paymentBankAccountID: accountID,
// },
// },
// ];
//
// const successData: OnyxUpdate[] = [
// {
// onyxMethod: Onyx.METHOD.MERGE,
// key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policyID}`,
// value: {
// paymentBankAccountID: accountID,
// },
// },
// ];
//
// const failureData: OnyxUpdate[] = [
// {
// onyxMethod: Onyx.METHOD.MERGE,
// key: `${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policyID}`,
// value: {
// paymentBankAccountID: null,
// },
// },
// ];
//
// const parameters = {
// workspaceAccountID: policyID,
// settlementBankAccountID: accountID,
// };
//
// API.write(WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_ACCOUNT, parameters, {optimisticData, successData, failureData});
}

function setIssueNewCardStepAndData({data, isEditing, step}: IssueNewCardFlowData) {
Onyx.merge(ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD, {data, isEditing, currentStep: step});
}
Expand Down Expand Up @@ -281,6 +378,8 @@ export {
clearCardListErrors,
reportVirtualExpensifyCardFraud,
revealVirtualCardDetails,
updateSettlementFrequency,
updateSettlementAccount,
setIssueNewCardStepAndData,
clearIssueNewCardFlow,
updateExpensifyCardLimit,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ function CardReconciliationPage({policy, route}: CardReconciliationPageProps) {

const [reconciliationConnection] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_CONTINUOUS_RECONCILIATION_CONNECTION}${policy?.id}`);
const [isContinuousReconciliationOn] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_EXPENSIFY_CARD_USE_CONTINUOUS_RECONCILIATION}${policy?.id}`);
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_PRIVATE_EXPENSIFY_CARD_SETTINGS}${policy?.id}`);

const paymentBankAccountID = cardSettings?.paymentBankAccountID ?? -1;
const bankAccountTitle = bankAccountList?.[paymentBankAccountID]?.title ?? '';

const policyID = policy?.id ?? '-1';
const {connection} = route.params;
Expand Down Expand Up @@ -99,7 +104,7 @@ function CardReconciliationPage({policy, route}: CardReconciliationPageProps) {
{!!reconciliationConnection && (
<MenuItemWithTopDescription
style={styles.mt5}
title={reconciliationConnection?.title}
title={bankAccountTitle}
description={translate('workspace.accounting.reconciliationAccount')}
shouldShowRightIcon
/>
Expand Down
Loading

0 comments on commit 50ebe95

Please sign in to comment.