diff --git a/src/languages/de.ts b/src/languages/de.ts index 38028268cb76..f4b59a2b4d74 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5387,6 +5387,8 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU settlementFrequencyDescription: 'Wählen Sie, wie oft Sie den Saldo Ihrer Expensify Karte begleichen.', settlementFrequencyInfo: 'Wenn du zur monatlichen Abrechnung wechseln möchtest, musst du dein Bankkonto über Plaid verbinden und eine positive Kontohistorie der letzten 90 Tage haben.', + applyCashbackToBill: 'Cashback auf meine Expensify-Rechnung anwenden', + applyCashbackToBillDescription: 'Das Cashback von der Expensify-Karte wird zur Begleichung deiner Expensify-Rechnung verwendet.', frequency: { daily: 'Täglich', monthly: 'Monatlich', diff --git a/src/languages/en.ts b/src/languages/en.ts index d39ea41e7884..27842af7ce6d 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5395,6 +5395,8 @@ const translations = { 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.', + applyCashbackToBill: 'Apply cash back to my Expensify bill', + applyCashbackToBillDescription: 'Cash back from the Expensify Card will be used towards payment for your Expensify bill.', frequency: { daily: 'Daily', monthly: 'Monthly', diff --git a/src/languages/es.ts b/src/languages/es.ts index 07f3e2b3c4f2..d4591c4247f1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5258,6 +5258,8 @@ ${amount} para ${merchant} - ${date}`, 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.', + applyCashbackToBill: 'Aplicar reembolso a mi factura de Expensify', + applyCashbackToBillDescription: 'El reembolso de la Tarjeta Expensify se utilizará para el pago de tu factura de Expensify.', frequency: { daily: 'Cada día', monthly: 'Mensual', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 1227ae10f6bd..2195ee76b403 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5396,6 +5396,8 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. settlementFrequencyDescription: 'Choisissez la fréquence à laquelle vous réglerez le solde de votre Carte Expensify.', settlementFrequencyInfo: 'Si vous souhaitez passer à un règlement mensuel, vous devrez connecter votre compte bancaire via Plaid et disposer d’un historique de solde positif sur 90 jours.', + applyCashbackToBill: 'Appliquer le cashback à ma facture Expensify', + applyCashbackToBillDescription: 'Le cashback de la carte Expensify sera utilisé pour le paiement de votre facture Expensify.', frequency: { daily: 'Quotidien', monthly: 'Mensuel', diff --git a/src/languages/it.ts b/src/languages/it.ts index 90a640d1af79..ef0b5f226490 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5367,6 +5367,8 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. settlementFrequencyDescription: 'Scegli con quale frequenza pagherai il saldo della tua Carta Expensify.', settlementFrequencyInfo: 'Se desideri passare alla liquidazione mensile, dovrai collegare il tuo conto bancario tramite Plaid e avere uno storico del saldo positivo di 90 giorni.', + applyCashbackToBill: 'Applica il cashback alla mia fattura Expensify', + applyCashbackToBillDescription: 'Il cashback della Carta Expensify verrà utilizzato per il pagamento della tua fattura Expensify.', frequency: { daily: 'Quotidiano', monthly: 'Mensile', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 9d9e41855cdd..fde1bd7dd169 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5317,6 +5317,8 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO settlementFrequency: '清算頻度', settlementFrequencyDescription: 'Expensify カードの残高を支払う頻度を選択してください。', settlementFrequencyInfo: '月次清算に切り替えるには、Plaid を通じて銀行口座を連携し、直近90日間の残高履歴がプラスである必要があります。', + applyCashbackToBill: 'キャッシュバックを Expensify 請求書に適用する', + applyCashbackToBillDescription: 'Expensify カードのキャッシュバックは、Expensify 請求書の支払いに使用されます。', frequency: { daily: '毎日', monthly: '毎月', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 4d8278651bf1..ae040665bd48 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5359,6 +5359,8 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ settlementFrequency: 'Uitbetalingsfrequentie', settlementFrequencyDescription: 'Kies hoe vaak je het saldo van je Expensify Kaart betaalt.', settlementFrequencyInfo: 'Als je wilt overschakelen naar maandelijkse afrekening, moet je je bankrekening koppelen via Plaid en een positieve saldohistorie van 90 dagen hebben.', + applyCashbackToBill: 'Cashback toepassen op mijn Expensify-factuur', + applyCashbackToBillDescription: 'De cashback van de Expensify Kaart wordt gebruikt voor het betalen van je Expensify-factuur.', frequency: { daily: 'Dagelijks', monthly: 'Maandelijks', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 7728ba667da3..2b3198a40d9f 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5347,6 +5347,8 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy settlementFrequency: 'Częstotliwość rozliczeń', settlementFrequencyDescription: 'Wybierz, jak często będziesz spłacać saldo swojej Karty Expensify.', settlementFrequencyInfo: 'Jeśli chcesz przejść na miesięczne rozliczenie, musisz podłączyć swoje konto bankowe przez Plaid i mieć dodatnią historię salda z ostatnich 90 dni.', + applyCashbackToBill: 'Zastosuj zwrot gotówki do mojego rachunku Expensify', + applyCashbackToBillDescription: 'Zwrot gotówki z Karty Expensify zostanie wykorzystany do opłacenia Twojego rachunku Expensify.', frequency: { daily: 'Codziennie', monthly: 'Miesięcznie', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index de032b2af682..ad3d211334b1 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5350,6 +5350,8 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS settlementFrequency: 'Frequência de liquidação', settlementFrequencyDescription: 'Escolha com que frequência você vai pagar o saldo do seu Cartão Expensify.', settlementFrequencyInfo: 'Se quiser mudar para liquidação mensal, você precisará conectar sua conta bancária via Plaid e ter um histórico de saldo positivo de 90 dias.', + applyCashbackToBill: 'Aplicar reembolso à minha fatura do Expensify', + applyCashbackToBillDescription: 'O reembolso do Cartão Expensify será usado para o pagamento da sua fatura do Expensify.', frequency: { daily: 'Diariamente', monthly: 'Mensal', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index b52517901f68..9b12cfe7810a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5232,6 +5232,8 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM settlementFrequency: '结算频率', settlementFrequencyDescription: '选择支付 Expensify 卡余额的频率。', settlementFrequencyInfo: '如果你想切换为按月结算,你需要通过 Plaid 连接你的银行账户,并且拥有过去 90 天为正数的余额记录。', + applyCashbackToBill: '将返现用于抵扣我的 Expensify 账单', + applyCashbackToBillDescription: 'Expensify 卡的返现将用于支付你的 Expensify 账单。', frequency: { daily: '每日', monthly: '每月', diff --git a/src/libs/API/parameters/ToggleCardCashbackToBillParams.ts b/src/libs/API/parameters/ToggleCardCashbackToBillParams.ts new file mode 100644 index 000000000000..36bc94604d9c --- /dev/null +++ b/src/libs/API/parameters/ToggleCardCashbackToBillParams.ts @@ -0,0 +1,6 @@ +type ToggleCardCashbackToBillParams = { + workspaceAccountID: number; + shouldApplyCashbackToBill: boolean; +}; + +export default ToggleCardCashbackToBillParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 37bc18336998..76e81cbf93f0 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -375,6 +375,7 @@ export type {default as SetPolicyCategoryTaxParams} from './SetPolicyCategoryTax export type {default as SetPolicyCategoryMaxAmountParams} from './SetPolicyCategoryMaxAmountParams'; export type {default as EnablePolicyCompanyCardsParams} from './EnablePolicyCompanyCardsParams'; export type {default as ToggleCardContinuousReconciliationParams} from './ToggleCardContinuousReconciliationParams'; +export type {default as ToggleCardCashbackToBillParams} from './ToggleCardCashbackToBillParams'; export type {default as SetCardReconciliationBankAccountParams} from './SetCardReconciliationBankAccountParams'; export type {default as CardDeactivateParams} from './CardDeactivateParams'; export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index a5461e424cf5..84f0ca501342 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -483,6 +483,7 @@ const WRITE_COMMANDS = { REMOVE_DELEGATE: 'RemoveDelegate', UPDATE_DELEGATE_ROLE: 'UpdateDelegateRole', TOGGLE_CARD_CONTINUOUS_RECONCILIATION: 'ToggleCardContinuousReconciliation', + TOGGLE_CARD_CASHBACK_TO_BILL: 'ToggleCardCashbackToBill', SET_CARD_RECONCILIATION_BANK_ACCOUNT: 'SetCardReconciliationBankAccount', SET_POLICY_TAG_APPROVER: 'SetPolicyTagApprover', SAVE_SEARCH: 'SaveSearch', @@ -1105,6 +1106,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_DELEGATE_ROLE]: Parameters.UpdateDelegateRoleParams; [WRITE_COMMANDS.REMOVE_DELEGATE]: Parameters.RemoveDelegateParams; [WRITE_COMMANDS.TOGGLE_CARD_CONTINUOUS_RECONCILIATION]: Parameters.ToggleCardContinuousReconciliationParams; + [WRITE_COMMANDS.TOGGLE_CARD_CASHBACK_TO_BILL]: Parameters.ToggleCardCashbackToBillParams; [WRITE_COMMANDS.SET_CARD_RECONCILIATION_BANK_ACCOUNT]: Parameters.SetCardReconciliationBankAccountParams; [WRITE_COMMANDS.SAVE_SEARCH]: Parameters.SaveSearchParams; [WRITE_COMMANDS.DELETE_SAVED_SEARCH]: Parameters.DeleteSavedSearchParams; diff --git a/src/libs/CardUtils.ts b/src/libs/CardUtils.ts index 40fd4f524a3a..a4ec8e1f4bc5 100644 --- a/src/libs/CardUtils.ts +++ b/src/libs/CardUtils.ts @@ -20,6 +20,7 @@ import type { CurrencyList, ExpensifyCardSettings, ExpensifyCardSettingsBase, + NestedExpensifyCardSettings, PersonalDetailsList, Policy, PolicyConnectionName, @@ -1257,17 +1258,17 @@ function getCardProgramKey(cardSettings: OnyxEntry): Card }); } -function getCardSettings(cardSettings: OnyxEntry, programKey?: CardProgramKey): ExpensifyCardSettingsBase | undefined { +function getCardSettings(cardSettings: OnyxEntry, programKey?: CardProgramKey): NestedExpensifyCardSettings | undefined { if (!cardSettings) { return undefined; } - const getMergedProgramSettings = (key: CardProgramKey): ExpensifyCardSettingsBase | undefined => { + const getMergedProgramSettings = (key: CardProgramKey): NestedExpensifyCardSettings | undefined => { const programSettings = cardSettings[key]; if (programSettings && typeof programSettings === 'object' && !Array.isArray(programSettings)) { // Nested program values take precedence — they are the authoritative source for // program-specific fields (e.g. paymentBankAccountID, monthlySettlementDate). - return {...cardSettings, ...programSettings} as ExpensifyCardSettingsBase; + return {...cardSettings, ...programSettings} as NestedExpensifyCardSettings; } return undefined; }; @@ -1285,7 +1286,7 @@ function getCardSettings(cardSettings: OnyxEntry, program getMergedProgramSettings(CONST.COUNTRY.US) ?? getMergedProgramSettings(CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT) ?? getMergedProgramSettings(CONST.COUNTRY.GB) ?? - (cardSettings as ExpensifyCardSettingsBase) + (cardSettings as NestedExpensifyCardSettings) ); } diff --git a/src/libs/actions/Card.ts b/src/libs/actions/Card.ts index b5ebc6fbddab..aa3384467e67 100644 --- a/src/libs/actions/Card.ts +++ b/src/libs/actions/Card.ts @@ -691,6 +691,52 @@ function updateSettlementFrequency( API.write(WRITE_COMMANDS.UPDATE_CARD_SETTLEMENT_FREQUENCY, parameters, {optimisticData, successData, failureData}); } +function toggleCashbackToBill(workspaceAccountID: number, programKey: CardProgramKey, shouldApplyCashbackToBill: boolean, currentValue?: boolean) { + const optimisticValue = { + [programKey]: {shouldApplyCashbackToBill, pendingFields: {shouldApplyCashbackToBill: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}}, + }; + const successValue = {[programKey]: {shouldApplyCashbackToBill, pendingFields: {shouldApplyCashbackToBill: null}}}; + const failureValue = { + [programKey]: {shouldApplyCashbackToBill: currentValue ?? true, pendingFields: {shouldApplyCashbackToBill: null}}, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }; + + const optimisticData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`, + value: optimisticValue, + }, + ]; + + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`, + value: successValue, + }, + ]; + + const failureData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`, + value: failureValue, + }, + ]; + + const parameters = { + workspaceAccountID, + shouldApplyCashbackToBill, + }; + + API.write(WRITE_COMMANDS.TOGGLE_CARD_CASHBACK_TO_BILL, parameters, {optimisticData, successData, failureData}); +} + +function clearCashbackToBillError(workspaceAccountID: number) { + Onyx.merge(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${workspaceAccountID}`, {errors: null}); +} + function updateSettlementAccount( domainName: string, workspaceAccountID: number, @@ -1860,6 +1906,8 @@ export { unassignCard, updateAssignedCardName, updateAssignedCardTransactionStartDate, + toggleCashbackToBill, + clearCashbackToBillError, toggleContinuousReconciliation, setCardReconciliationAccount, updateExpensifyCardLimitType, diff --git a/src/pages/workspace/expensifyCard/WorkspaceCardSettingsPage.tsx b/src/pages/workspace/expensifyCard/WorkspaceCardSettingsPage.tsx index 4c314f971fa8..f1e65c7bcfad 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceCardSettingsPage.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceCardSettingsPage.tsx @@ -9,13 +9,17 @@ import TextLink from '@components/TextLink'; import useDefaultFundID from '@hooks/useDefaultFundID'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePrivateSubscription from '@hooks/usePrivateSubscription'; import useThemeStyles from '@hooks/useThemeStyles'; +import {clearCashbackToBillError, toggleCashbackToBill} from '@libs/actions/Card'; import {getLastFourDigits} from '@libs/BankAccountUtils'; -import {getCardSettings} from '@libs/CardUtils'; +import {getCardProgramKey, getCardSettings} from '@libs/CardUtils'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import {isSubscriptionTypeOfInvoicing} from '@libs/SubscriptionUtils'; import Navigation from '@navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -32,6 +36,11 @@ function WorkspaceCardSettingsPage({route}: WorkspaceCardSettingsPageProps) { const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); const [cardSettings] = useOnyx(`${ONYXKEYS.COLLECTION.PRIVATE_EXPENSIFY_CARD_SETTINGS}${defaultFundID}`); const settings = getCardSettings(cardSettings); + const programKey = getCardProgramKey(cardSettings); + const privateSubscription = usePrivateSubscription(); + const isUSProgram = programKey === CONST.COUNTRY.US || programKey === CONST.EXPENSIFY_CARD.CARD_PROGRAM.CURRENT; + const shouldShowCashbackToggle = isUSProgram && !isSubscriptionTypeOfInvoicing(privateSubscription?.type); + const shouldApplyCashbackToBill = settings?.shouldApplyCashbackToBill ?? true; const paymentBankAccountID = settings?.paymentBankAccountID; const paymentBankAccountNumber = settings?.paymentBankAccountNumber; @@ -88,6 +97,25 @@ function WorkspaceCardSettingsPage({route}: WorkspaceCardSettingsPageProps) { } /> + {shouldShowCashbackToggle && ( + clearCashbackToBillError(defaultFundID)} + onToggle={(isEnabled: boolean) => { + if (!programKey) { + return; + } + toggleCashbackToBill(defaultFundID, programKey, isEnabled, settings?.shouldApplyCashbackToBill); + }} + /> + )} diff --git a/src/types/onyx/ExpensifyCardSettings.ts b/src/types/onyx/ExpensifyCardSettings.ts index 05785190452d..bbb104de8481 100644 --- a/src/types/onyx/ExpensifyCardSettings.ts +++ b/src/types/onyx/ExpensifyCardSettings.ts @@ -61,6 +61,9 @@ type ExpensifyCardSettingsBase = { /** Number of the bank account used for the card settlement */ paymentBankAccountNumber?: string; + /** Whether Expensify Card cash back should be applied toward payment of the Expensify bill */ + shouldApplyCashbackToBill?: boolean; + /** Collections of form field errors */ errorFields?: OnyxCommon.ErrorFields; @@ -104,6 +107,9 @@ type ExpensifyCardRule = OnyxCommon.OnyxValueWithOfflineFeedback<{ action: ValueOf; }>; +/** Nested program settings with offline feedback support for optimistic updates */ +type NestedExpensifyCardSettings = OnyxCommon.OnyxValueWithOfflineFeedback; + /** Model of Expensify card settings for a workspace - can have nested feed types from backend */ type ExpensifyCardSettings = OnyxCommon.OnyxValueWithOfflineFeedback< ExpensifyCardSettingsBase & { @@ -112,25 +118,25 @@ type ExpensifyCardSettings = OnyxCommon.OnyxValueWithOfflineFeedback< /** * */ - US?: ExpensifyCardSettingsBase; + US?: NestedExpensifyCardSettings; /** Nested settings for pre-2024 US card program from backend */ /** * */ - CURRENT?: ExpensifyCardSettingsBase; + CURRENT?: NestedExpensifyCardSettings; /** Nested settings for UK/EU card program from backend */ /** * */ - GB?: ExpensifyCardSettingsBase; + GB?: NestedExpensifyCardSettings; /** Nested Travel Invoicing settings from backend */ /** * */ - TRAVEL_US?: ExpensifyCardSettingsBase; + TRAVEL_US?: NestedExpensifyCardSettings; /** Spend rules for the feed keyed by rule ID - stringified JSON of ExpensifyCardRule */ cardRules?: Record; @@ -141,4 +147,4 @@ type ExpensifyCardSettings = OnyxCommon.OnyxValueWithOfflineFeedback< >; export default ExpensifyCardSettings; -export type {ExpensifyCardSettingsBase, ExpensifyCardRule, ExpensifyCardRuleFilter}; +export type {ExpensifyCardSettingsBase, NestedExpensifyCardSettings, ExpensifyCardRule, ExpensifyCardRuleFilter}; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index b80b19122e79..85706a28f503 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -68,7 +68,7 @@ import type DuplicateWorkspace from './DuplicateWorkspace'; import type ExpenseRule from './ExpenseRule'; import type ExpensifyCardBankAccountMetadata from './ExpensifyCardBankAccountMetadata'; import type ExpensifyCardSettings from './ExpensifyCardSettings'; -import type {ExpensifyCardSettingsBase} from './ExpensifyCardSettings'; +import type {ExpensifyCardSettingsBase, NestedExpensifyCardSettings} from './ExpensifyCardSettings'; import type ExportTemplate from './ExportTemplate'; import type FrequentlyUsedEmoji from './FrequentlyUsedEmoji'; import type {FundList} from './Fund'; @@ -224,6 +224,7 @@ export type { ExpenseRule, ExpensifyCardSettings, ExpensifyCardSettingsBase, + NestedExpensifyCardSettings, ExpensifyCardBankAccountMetadata, FrequentlyUsedEmoji, Fund,