Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
9a93822
Allow $0 workspace distance rates and treat a $0 rate as a real rate
NicolasBonet Oct 2, 2026
f0474e4
Support splitting $0 distance expenses by distance
NicolasBonet Oct 2, 2026
c2fa242
Pass the full rate form values in the validateRateValue tests
NicolasBonet Oct 2, 2026
c4b37e1
Make split amounts read-only for $0 distance expenses and explain why
NicolasBonet Oct 2, 2026
23fc4a3
Merge branch 'main' into nicolas/allow-zero-distance-rate
NicolasBonet Oct 5, 2026
c3d79c6
Merge remote-tracking branch 'origin/main' into nicolas/allow-zero-di…
NicolasBonet Oct 5, 2026
6926c27
Remove the onInputBlur prop from the split input editability tests
NicolasBonet Oct 5, 2026
0900651
Only use a $0 rate for $0 distance expenses when splitting, and offer…
NicolasBonet Oct 5, 2026
274f787
Merge remote-tracking branch 'origin/main' into nicolas/allow-zero-di…
NicolasBonet Oct 7, 2026
d2ae35e
Merge remote-tracking branch 'origin/main' into nicolas/allow-zero-di…
NicolasBonet Oct 9, 2026
3a604b8
Restore GitHub Actions builds from main
NicolasBonet Oct 9, 2026
03e7847
Merge branch 'main' into nicolas/allow-zero-distance-rate
NicolasBonet Oct 12, 2026
847c75d
Merge remote-tracking branch 'origin/main' into nicolas/allow-zero-di…
NicolasBonet Oct 12, 2026
07f83b1
Merge remote-tracking branch 'origin/main' into nicolas/allow-zero-di…
NicolasBonet Oct 12, 2026
808fdfa
Split the zero-rate distance comment into plain sentences
NicolasBonet Oct 12, 2026
2245eba
Recalculate positive-rate splits when making the splits of a $0 dista…
NicolasBonet Oct 12, 2026
1f4ab34
Add the required transaction fields to the zero-total split test
NicolasBonet Oct 12, 2026
0d65a29
Merge remote-tracking branch 'origin/main' into nicolas/allow-zero-di…
NicolasBonet Oct 12, 2026
3a222fe
Stub on-mount policy category/tag loaders in CategoryTagPickerAutoFoc…
NicolasBonet Oct 12, 2026
3697787
Merge remote-tracking branch 'origin/main' into nicolas/allow-zero-di…
NicolasBonet Oct 13, 2026
1b77e8a
Keep a split's distance when its odometer readings are saved unchanged
NicolasBonet Oct 15, 2026
6df1c00
Make the amount of a split on a $0 rate read-only
NicolasBonet Oct 15, 2026
743e1a9
Fix $0 distance rate split bugs found in review
NicolasBonet Oct 16, 2026
2101809
Merge branch 'main' into nicolas/allow-zero-distance-rate
NicolasBonet Oct 19, 2026
a0bd406
Merge branch 'main' into nicolas/allow-zero-distance-rate
NicolasBonet Oct 20, 2026
bf8855f
Fix $0 distance split edge cases: rate changed from $0, Search eligib…
NicolasBonet Oct 21, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ function useDistanceRequestState({
});
const distanceRate = mileageRate.rate;
const distanceUnit = mileageRate.unit;
const calculateFromTransactionData = isMovingTransactionFromTrackExpense && !distanceRate;
const calculateFromTransactionData = isMovingTransactionFromTrackExpense && distanceRate === undefined;
const customUnit = transaction?.comment?.customUnit;
const unit = calculateFromTransactionData ? customUnit?.distanceUnit : distanceUnit;
const backCalculationQuantity = customUnit?.quantity;
Expand Down Expand Up @@ -124,7 +124,7 @@ function useDistanceRequestState({
!transaction?.errorFields?.route &&
!DistanceRequestUtils.hasCommuterExclusionPreviewForPolicy(transaction, policy);

const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || !rate || isCommuterExclusionPreviewPending) && !isMovingTransactionFromTrackExpense;
const isDistanceRequestWithPendingRoute = isDistanceRequest && (!hasRoute || rate === undefined || isCommuterExclusionPreviewPending) && !isMovingTransactionFromTrackExpense;

const distanceRequestAmount = DistanceRequestUtils.getDistanceRequestAmount(reimbursableDistanceInMeters, amountUnit, rate ?? 0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ function RateField({

const isTrackExpense = iouType === CONST.IOU.TYPE.TRACK;
const rate = mileageRate.rate;
const isRateInteractive = !!rate && !isReadOnly && iouType !== CONST.IOU.TYPE.SPLIT;
const isRateInteractive = rate !== undefined && !isReadOnly && iouType !== CONST.IOU.TYPE.SPLIT;

const {isSearchRouterDisplayed} = useSearchRouterState();

Expand Down
17 changes: 14 additions & 3 deletions src/components/MoneyRequestHeaderSecondaryActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import useReportTransactionsCollection from '@hooks/useReportTransactionsCollect
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useRestrictedActionPolicyID from '@hooks/useRestrictedActionPolicyID';
import useShouldDisplayButtonsInSeparateLine from '@hooks/useShouldDisplayButtonsInSeparateLine';
import useSplitEffectivePolicy from '@hooks/useSplitEffectivePolicy';
import useSplitEffectivePolicy, {findSplitPolicyForCustomUnit} from '@hooks/useSplitEffectivePolicy';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useThrottledButtonState from '@hooks/useThrottledButtonState';
Expand Down Expand Up @@ -81,8 +81,9 @@ import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type {Transaction} from '@src/types/onyx';
import type {Policy, Transaction} from '@src/types/onyx';

import type {OnyxCollection} from 'react-native-onyx';
import type {ValueOf} from 'type-fest';

import {useRoute} from '@react-navigation/native';
Expand All @@ -91,7 +92,7 @@ import {hasSeenTourSelector} from '@selectors/Onboarding';
import {loginSelector} from '@selectors/PersonalDetails';
import {billingRestrictionPolicySelector, createFilteredPoliciesInfoSelector, createHasWorkspaceToSubmitToSelector} from '@selectors/Policy';
import {validTransactionDraftsSelector} from '@selectors/TransactionDraft';
import React, {useMemo, useRef, useState} from 'react';
import React, {useCallback, useMemo, useRef, useState} from 'react';

import type {ButtonWithDropdownMenuRef, DropdownOption} from './ButtonWithDropdownMenu/types';

Expand Down Expand Up @@ -187,6 +188,15 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
const [conciergeChat] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${conciergeReportID}`);
const [selfDMReportID] = useOnyx(ONYXKEYS.SELF_DM_REPORT_ID);
const splitEffectivePolicy = useSplitEffectivePolicy(report, undefined, transaction);
// Only the workspace that owns the expense's distance rate is needed to tell whether the rate is $0
const distanceRatePoliciesSelector = useCallback(
(allPolicies: OnyxCollection<Policy>) => {
const distanceRatePolicy = findSplitPolicyForCustomUnit(allPolicies, transaction);
return distanceRatePolicy ? {[`${ONYXKEYS.COLLECTION.POLICY}${distanceRatePolicy.id}`]: distanceRatePolicy} : undefined;
},
[transaction],
);
const [distanceRatePolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: distanceRatePoliciesSelector});
const personalPolicy = usePersonalPolicy();
const restrictedActionPolicyID = useRestrictedActionPolicyID(policy);
const isTrackIntentUser = isTrackOnboardingChoice(introSelected?.choice);
Expand Down Expand Up @@ -397,6 +407,7 @@ function MoneyRequestHeaderSecondaryActions({reportID, onBackButtonPress}: Money
isRestrictedToPreferredPolicy,
rules,
cardList,
policies: distanceRatePolicies,
});
})();

Expand Down
4 changes: 2 additions & 2 deletions src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -639,8 +639,8 @@ function MoneyRequestView({
const currency = transactionCurrency ?? CONST.CURRENCY.USD;
const hasRequiredCompanyCardViolation = transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.COMPANY_CARD_REQUIRED);
const isCustomUnitOutOfPolicy =
(transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY) || (isDistanceRequest && !rate)) && !isTrackExpense;
const calculateFromTransactionData = isTrackExpense && !rate;
(transactionViolations.some((violation) => violation.name === CONST.VIOLATIONS.CUSTOM_UNIT_OUT_OF_POLICY) || (isDistanceRequest && rate === undefined)) && !isTrackExpense;
const calculateFromTransactionData = isTrackExpense && rate === undefined;
const distanceUnit = calculateFromTransactionData ? transaction?.comment?.customUnit?.distanceUnit : unit;
const backCalculationQuantity = transaction?.comment?.customUnit?.quantity;
const distanceRate = calculateFromTransactionData && !!backCalculationQuantity ? (transactionAmount ?? 0) / backCalculationQuantity : rate;
Expand Down
6 changes: 6 additions & 0 deletions src/components/Search/SearchWriteActionsProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ function useReconcileSelectionWithData({
const {selectedTransactions, excludedTransactions = getEmptyObject<SelectedTransactions>(), areAllMatchingItemsSelected} = useSearchSelectionContext();
const {applySelection} = useSearchSelectionActions();
const [rules] = useOnyx(ONYXKEYS.COLLECTION.RULE);
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);

useEffect(() => {
const shouldReconcileMovedExcludedTransaction =
Expand Down Expand Up @@ -277,6 +278,7 @@ function useReconcileSelectionWithData({
allowNegativeAmount: true,
parentReport: itemParentReport,
rules,
policies,
});

const liveSelectionEntry: SelectedTransactionInfo = {
Expand Down Expand Up @@ -362,6 +364,7 @@ function useReconcileSelectionWithData({
allowNegativeAmount: true,
parentReport: itemParentReport,
rules,
policies,
});

const liveSelectionEntry: SelectedTransactionInfo = {
Expand Down Expand Up @@ -537,6 +540,7 @@ function SearchWriteActionsProvider({
const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS);
const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID);
const [rules] = useOnyx(ONYXKEYS.COLLECTION.RULE);
const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY);
const {applySelection, getSelectedTransactions, getExcludedTransactions, getAreAllMatchingItemsSelected} = useSearchSelectionActions();

const {openGroupKeys, shiftRangeGroupsActions} = useOpenGroupsRegistry(searchHash);
Expand Down Expand Up @@ -584,6 +588,7 @@ function SearchWriteActionsProvider({
allowNegativeAmount: true,
parentReport,
rules,
policies,
});
};

Expand Down Expand Up @@ -739,6 +744,7 @@ function SearchWriteActionsProvider({
selfDMReport,
parentReport: itemParentReport,
rules,
policies,
});

if (areItemsGrouped && isGroupedItemArray(filteredData)) {
Expand Down
24 changes: 22 additions & 2 deletions src/components/Search/selectionBuilders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type {ShiftRangeBatch} from '@libs/shiftRangeSelection';
import {getOriginalTransactionWithSplitInfo, hasValidModifiedAmount, isExpenseUnreported, isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils';

import CONST from '@src/CONST';
import type {OutstandingReportsByPolicyIDDerivedValue, Report, ReportNameValuePairs, Rule, Transaction} from '@src/types/onyx';
import type {OutstandingReportsByPolicyIDDerivedValue, Policy, Report, ReportNameValuePairs, Rule, Transaction} from '@src/types/onyx';
import type {SearchGroupBase, SearchResultDataType} from '@src/types/onyx/SearchResults';

import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
Expand Down Expand Up @@ -133,6 +133,9 @@ type MapTransactionItemToSelectedEntryParams = {

/** Approval workflow rules, used for split eligibility */
rules: OnyxCollection<Rule>;

/** Workspaces to look up a distance rate in for split eligibility, when the row's policy doesn't own it */
policies?: OnyxCollection<Policy>;
};

/**
Expand All @@ -152,6 +155,7 @@ function mapTransactionItemToSelectedEntry({
allowNegativeAmount,
parentReport,
rules,
policies,
}: MapTransactionItemToSelectedEntryParams): [string, SelectedTransactionInfo] {
const {canHoldRequest, canUnholdRequest} = canHoldUnholdReportAction(item.report, item.reportAction, item.holdReportAction, item, item.policy, currentUserAccountID, rules);
const canRejectRequest = item.report ? canRejectReportAction(item.report, currentUserAccountID, item.policy) : false;
Expand All @@ -170,7 +174,18 @@ function mapTransactionItemToSelectedEntry({
canUnhold: canUnholdRequest,
// TODO: Pass reportOwnerLogin in PR 4d. This runs once per search result, so it needs a precomputed map of accountID to login rather than a hook.
// isAwaitingFirstLevelApproval falls back to the personal details store until then. See https://github.com/Expensify/App/issues/66413.
canSplit: isSplitAction(reportForSplit, [itemTransaction], originalItemTransaction, currentUserLogin, currentUserAccountID, rules, undefined, item.policy, parentReport),
canSplit: isSplitAction(
reportForSplit,
[itemTransaction],
originalItemTransaction,
currentUserLogin,
currentUserAccountID,
rules,
undefined,
item.policy,
parentReport,
policies,
),
hasBeenSplit: getOriginalTransactionWithSplitInfo(itemTransaction, originalItemTransaction).isExpenseSplit,
canChangeReport: canEditFieldOfMoneyRequest({
reportAction: item.reportAction,
Expand Down Expand Up @@ -283,6 +298,9 @@ type PrepareTransactionsListParams = {

/** Approval workflow rules, used for split eligibility */
rules: OnyxCollection<Rule>;

/** Workspaces to look up a distance rate in for split eligibility, when the row's policy doesn't own it */
policies?: OnyxCollection<Policy>;
};

/**
Expand All @@ -301,6 +319,7 @@ function prepareTransactionsList({
selfDMReport,
parentReport,
rules,
policies,
}: PrepareTransactionsListParams) {
if (selectedTransactions[item.keyForList]?.isSelected) {
const {[item.keyForList]: omittedTransaction, ...transactions} = selectedTransactions;
Expand All @@ -320,6 +339,7 @@ function prepareTransactionsList({
allowNegativeAmount: false,
parentReport,
rules,
policies,
});

return {
Expand Down
2 changes: 1 addition & 1 deletion src/components/SelectionList/ListItem/SplitListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ function SplitListItem<TItem extends SplitListItemType>({item, isFocused, showTo
// cleanup can cancel any pending focus task when the screen starts closing, preventing
// the focused input from interfering with the close animation.
const inputCallbackRef: (ref: BaseTextInputRef | null) => void = (ref) => {
if (!item.isSelected || !item.isEditable) {
if (!item.isSelected || !item.isEditable || item.isAmountEditable === false) {
return;
}
(autoFocusCallbackRef as unknown as (ref: BaseTextInputRef | null) => void)(ref);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function SplitAmountInput({splitItem, formattedOriginalAmount, contentWidth, onS
return convertToFrontendAmountAsString(amountAsInt, decimals);
};

if (splitItem.isEditable) {
if (splitItem.isEditable && splitItem.isAmountEditable !== false) {
return (
<MoneyRequestAmountInput
ref={inputCallbackRef}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ function SplitPercentageInput({splitItem, contentWidth, percentageDraft, onSplit

const inputValue = percentageDraft ?? String(splitItem.percentage ?? 0);

if (splitItem.isEditable) {
// Any percentage of a $0 total is $0, so a percentage typed in would be discarded
if (splitItem.isEditable && splitItem.isAmountEditable !== false && splitItem.originalAmount !== 0) {
return (
<PercentageForm
onInputChange={(value) => {
Expand Down
6 changes: 6 additions & 0 deletions src/components/SelectionList/ListItem/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,12 @@ type SplitListItemType = ListItem &
/** Indicates whether a split wasn't approved, paid etc. when report.statusNum < CONST.REPORT.STATUS_NUM.CLOSED */
isEditable: boolean;

/**
* Whether the split's amount or percentage can be typed in. The split can still be opened to edit its other fields
* when this is false, e.g. a split of a $0 rate distance expense is always $0 but its distance can change.
*/
isAmountEditable?: boolean;

/** Current mode for the split editor: amount or percentage */
mode: ValueOf<typeof CONST.TAB.SPLIT>;

Expand Down
1 change: 1 addition & 0 deletions src/hooks/useFrozenSplitTransactionIDs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ function useFrozenSplitTransactionIDs({
getLoginByAccountID(itemReport?.ownerAccountID, personalDetails),
itemPolicy,
parentReport,
allPolicies,
);
if (isSplitExpenseFrozen(itemReport) || !isEditable) {
frozenIDs.add(item.transactionID);
Expand Down
1 change: 1 addition & 0 deletions src/hooks/useSelectedTransactionsActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,7 @@ function useSelectedTransactionsActions({
getLoginByAccountID(report?.ownerAccountID, personalDetails),
policy,
parentReport,
allPolicies,
);

if (canSplitTransaction) {
Expand Down
4 changes: 3 additions & 1 deletion src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ const translations: TranslationDeepObject<typeof en> = {
pleaseCompleteForm: 'Bitte fülle das obige Formular aus, um fortzufahren',
pleaseSelectOne: 'Bitte wähle oben eine Option aus',
invalidRateError: 'Bitte einen gültigen Satz eingeben',
lowRateError: 'Der Satz muss größer als 0 sein',
lowRateError: 'Der Satz darf nicht negativ sein',
email: 'Bitte gib eine gültige E-Mail-Adresse ein',
login: 'Beim Anmelden ist ein Fehler aufgetreten. Bitte versuche es erneut.',
},
Expand Down Expand Up @@ -1392,6 +1392,8 @@ const translations: TranslationDeepObject<typeof en> = {
totalAmountGreaterThanOriginal: (amount: string) => `Der Gesamtbetrag ist um ${amount} höher als die ursprüngliche Ausgabe.`,
totalAmountLessThanOriginal: (amount: string) => `Der Gesamtbetrag ist um ${amount} geringer als die ursprüngliche Ausgabe.`,
splitExpenseZeroAmount: 'Bitte gib einen gültigen Betrag ein, bevor du fortfährst.',
splitExpenseZeroDistance: 'Bitte gib eine gültige Entfernung ein, bevor du fortfährst.',
splitExpenseZeroTotalAmountNotEditable: (amount: string) => `Der Betrag kann nicht bearbeitet werden, wenn die ursprüngliche Summe ${amount} beträgt.`,
splitExpenseOneMoreSplit: 'Keine Aufteilungen hinzugefügt. Füge mindestens eine hinzu, um zu speichern.',
splitExpenseEditTitle: (amount: string, merchant: string) => `${amount} für ${merchant} bearbeiten`,
removeSplit: 'Aufteilung entfernen',
Expand Down
4 changes: 3 additions & 1 deletion src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ const translations: TranslationDeepObject<typeof en> = {
pleaseCompleteForm: 'Συμπληρώστε τη φόρμα παραπάνω για να συνεχίσετε',
pleaseSelectOne: 'Παρακαλώ επιλέξτε μια από τις παραπάνω επιλογές',
invalidRateError: 'Παρακαλούμε εισαγάγετε έγκυρο συντελεστή',
lowRateError: 'Το ποσοστό πρέπει να είναι μεγαλύτερο από 0',
lowRateError: 'Το ποσοστό δεν μπορεί να είναι αρνητικό',
email: 'Παρακαλούμε εισαγάγετε μια έγκυρη διεύθυνση email',
login: 'Προέκυψε σφάλμα κατά τη σύνδεση. Παρακαλούμε δοκιμάστε ξανά.',
},
Expand Down Expand Up @@ -1439,6 +1439,8 @@ const translations: TranslationDeepObject<typeof en> = {
totalAmountGreaterThanOriginal: (amount: string) => `Το συνολικό ποσό είναι κατά ${amount} μεγαλύτερο από την αρχική δαπάνη.`,
totalAmountLessThanOriginal: (amount: string) => `Το συνολικό ποσό είναι κατά ${amount} λιγότερο από την αρχική δαπάνη.`,
splitExpenseZeroAmount: 'Παρακαλώ εισαγάγετε ένα έγκυρο ποσό πριν συνεχίσετε.',
splitExpenseZeroDistance: 'Παρακαλώ εισαγάγετε μια έγκυρη απόσταση πριν συνεχίσετε.',
splitExpenseZeroTotalAmountNotEditable: (amount: string) => `Το ποσό δεν μπορεί να επεξεργαστεί όταν το αρχικό σύνολο είναι ${amount}.`,
splitExpenseOneMoreSplit: 'Δεν προστέθηκαν κατανομές. Προσθέστε τουλάχιστον μία για να αποθηκεύσετε.',
splitExpenseEditTitle: (amount: string, merchant: string) => `Επεξεργαστείτε ${amount} για ${merchant}`,
removeSplit: 'Αφαίρεση διαχωρισμού',
Expand Down
4 changes: 3 additions & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ const translations = {
pleaseCompleteForm: 'Please complete the form above to continue',
pleaseSelectOne: 'Please select an option above',
invalidRateError: 'Please enter a valid rate',
lowRateError: 'Rate must be greater than 0',
lowRateError: "Rate can't be negative",
email: 'Please enter a valid email address',
login: 'An error occurred while logging in. Please try again.',
},
Expand Down Expand Up @@ -1523,6 +1523,8 @@ const translations = {
totalAmountGreaterThanOriginal: (amount: string) => `Total amount is ${amount} greater than the original expense.`,
totalAmountLessThanOriginal: (amount: string) => `Total amount is ${amount} less than the original expense.`,
splitExpenseZeroAmount: 'Please enter a valid amount before continuing.',
splitExpenseZeroDistance: 'Please enter a valid distance before continuing.',
splitExpenseZeroTotalAmountNotEditable: (amount: string) => `Amount can't be edited when the original total is ${amount}.`,
splitExpenseOneMoreSplit: 'No splits added. Add at least one to save.',
splitExpenseEditTitle: (amount: string, merchant: string) => `Edit ${amount} for ${merchant}`,
removeSplit: 'Remove split',
Expand Down
Loading
Loading