Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7fca4f0
Add country selection for EUR auto-update government rates
ShridharGoel Sep 29, 2026
e2bc3ed
Extract shared GovernmentRateCountrySelector component
ShridharGoel Sep 29, 2026
5972c25
Fix review items and merge auto-update into the currency change request
ShridharGoel Sep 29, 2026
5d2dff9
Fix test types and remove unused export
ShridharGoel Sep 29, 2026
0a20428
Fix government rate failure restore and apply review fixes
ShridharGoel Sep 30, 2026
526f52e
Merge branch 'main' into government-rates-eur-country
ShridharGoel Sep 30, 2026
58b1e77
Merge remote-tracking branch 'origin/main' into government-rates-eur-…
ShridharGoel Sep 30, 2026
40b8bad
Fix lint and typecheck errors from PR review
ShridharGoel Sep 30, 2026
6e4cbed
Merge remote-tracking branch 'fork/government-rates-eur-country' into…
ShridharGoel Sep 30, 2026
cd371df
Merge remote-tracking branch 'origin/main' into government-rates-eur-…
ShridharGoel Sep 30, 2026
a5355a9
Sync CI action bundles with main
ShridharGoel Sep 30, 2026
2e4719d
Merge remote-tracking branch 'origin/main' into government-rates-eur-…
ShridharGoel Oct 1, 2026
39f3f04
Fix lint errors in PolicyTest
ShridharGoel Oct 1, 2026
59e97c0
Fix PolicyTest import and currency goBack callback
ShridharGoel Oct 2, 2026
d21fd1a
Merge branch 'main' into government-rates-eur-country
ShridharGoel Oct 2, 2026
82802c2
Update automatic rates section per Figma design
ShridharGoel Oct 5, 2026
710a614
Merge remote-tracking branch 'origin/main' into government-rates-eur-…
ShridharGoel Oct 5, 2026
fdee630
Show bottom divider whenever auto-update section renders
ShridharGoel Oct 5, 2026
06123c7
Add spacing between top divider and heading
ShridharGoel Oct 5, 2026
04e67c9
Merge branch 'main' of github.com:Expensify/App into government-rates…
ShridharGoel Oct 5, 2026
a44eed5
Sync merged main content into the branch tree
ShridharGoel Oct 5, 2026
7d0c854
Fix stale translations and currency type in currency change hook
ShridharGoel Oct 5, 2026
67adb98
Sync branch tree with latest main
ShridharGoel Oct 5, 2026
88776b4
Simplify government rate country option types and imports
ShridharGoel Oct 5, 2026
cf95b49
Fix currency-to-EUR unit rollback and lost card enrollment flag
ShridharGoel Oct 5, 2026
6b80d0c
Merge branch 'main' into government-rates-eur-country
ShridharGoel Oct 5, 2026
8709ec8
Merge branch 'main' of github.com:Expensify/App into government-rates…
ShridharGoel Oct 5, 2026
e1d1c37
Open country picker after EUR gov-rates upgrade and keep API error text
ShridharGoel Oct 5, 2026
532773c
Restore back button on distance rates settings header
ShridharGoel Oct 6, 2026
4ce5f60
Merge branch 'main' into government-rates-eur-country
ShridharGoel Oct 6, 2026
d7a25aa
Address Neil review on gov rate country navigation and errors
ShridharGoel Oct 8, 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
12 changes: 11 additions & 1 deletion src/CONST/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5108,12 +5108,21 @@ const CONST = {
SEK: 'SE',
ZAR: 'ZA',
},
// EUR is shared by several supported countries, so an EUR workspace picks the country itself
GOVERNMENT_RATE_SUPPORTED_EUR_COUNTRIES: ['AT', 'BE', 'FI', 'DE', 'NL', 'PT', 'ES'] as const,
// Unit each country publishes its rates in
GOVERNMENT_RATE_COUNTRY_TO_UNIT: {
US: 'mi',
GB: 'mi',
CA: 'km',
AU: 'km',
AT: 'km',
BE: 'km',
FI: 'km',
DE: 'km',
NL: 'km',
PT: 'km',
ES: 'km',
NO: 'km',
SE: 'km',
ZA: 'km',
Expand Down Expand Up @@ -9889,6 +9898,7 @@ const CONST = {
MORE_DROPDOWN: 'WorkspaceDistanceRates-MoreDropdown',
BULK_ACTIONS_DROPDOWN: 'WorkspaceDistanceRates-BulkActionsDropdown',
UNIT_SELECTOR: 'WorkspaceDistanceRates-UnitSelector',
COUNTRY_SELECTOR: 'WorkspaceDistanceRates-CountrySelector',
},
WORKFLOWS: {
AUTO_REPORTING_FREQUENCY: 'WorkspaceWorkflows-AutoReportingFrequency',
Expand Down Expand Up @@ -10384,7 +10394,7 @@ const COUNTRIES_US_BANK_FLOW: string[] = [CONST.COUNTRY.US, CONST.COUNTRY.PR, CO
type Country = keyof typeof CONST.ALL_COUNTRIES;

/** A country whose government mileage rates Expensify can auto-update */
type GovernmentRateCountry = ValueOf<typeof CONST.CUSTOM_UNITS.GOVERNMENT_RATE_CURRENCY_TO_COUNTRY>;
type GovernmentRateCountry = ValueOf<typeof CONST.CUSTOM_UNITS.GOVERNMENT_RATE_CURRENCY_TO_COUNTRY> | TupleToUnion<typeof CONST.CUSTOM_UNITS.GOVERNMENT_RATE_SUPPORTED_EUR_COUNTRIES>;

type IOUType = ValueOf<typeof CONST.IOU.TYPE>;
type IOUAction = ValueOf<typeof CONST.IOU.ACTION>;
Expand Down
22 changes: 22 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3070,6 +3070,24 @@ const ROUTES = {
return `workspaces/${policyID}/overview/currency${query ? `?${query}` : ''}` as const;
},
},
WORKSPACE_OVERVIEW_CURRENCY_GOVERNMENT_RATE_COUNTRY: {
route: 'workspaces/:policyID/overview/currency/government-rate-country/:currencyCode',
getRoute: (
policyID: string,
currencyCode: string,
{isForcedToChangeCurrency, shouldStartExpensifyCardEnrollment}: {isForcedToChangeCurrency?: boolean; shouldStartExpensifyCardEnrollment?: boolean} = {},
) => {
const params = new URLSearchParams();
if (isForcedToChangeCurrency) {
params.set('isForcedToChangeCurrency', 'true');
}
if (shouldStartExpensifyCardEnrollment) {
params.set('shouldStartExpensifyCardEnrollment', 'true');
}
const query = params.toString();
return `workspaces/${policyID}/overview/currency/government-rate-country/${currencyCode}${query ? `?${query}` : ''}` as const;
},
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_EXPORT: {
route: 'workspaces/:policyID/accounting/quickbooks-online/export',
getRoute: (policyID: string | undefined) => {
Expand Down Expand Up @@ -3922,6 +3940,10 @@ const ROUTES = {
route: 'workspaces/:policyID/distance-rates/settings/unit',
getRoute: (policyID: string) => `workspaces/${policyID}/distance-rates/settings/unit` as const,
},
WORKSPACE_DISTANCE_RATES_GOVERNMENT_RATE_COUNTRY: {
route: 'workspaces/:policyID/distance-rates/settings/government-rate-country',
getRoute: (policyID: string) => `workspaces/${policyID}/distance-rates/settings/government-rate-country` as const,
},
WORKSPACE_DISTANCE_RATES_COMMUTER_EXCLUSIONS: {
route: 'workspaces/:policyID/distance-rates/settings/commuter-exclusions',
getRoute: (policyID: string) => `workspaces/${policyID}/distance-rates/settings/commuter-exclusions` as const,
Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -893,6 +893,7 @@ const SCREENS = {
DYNAMIC_TAG_SETTINGS: 'Dynamic_Tag_Settings',
DYNAMIC_TAG_LIST_VIEW: 'Dynamic_Tag_List_View',
CURRENCY: 'Workspace_Overview_Currency',
CURRENCY_GOVERNMENT_RATE_COUNTRY: 'Workspace_Overview_Currency_Government_Rate_Country',
DYNAMIC_WORKSPACE_OVERVIEW_ADDRESS: 'Dynamic_Workspace_Overview_Address',
DYNAMIC_WORKSPACE_OVERVIEW_PLAN: 'Dynamic_Workspace_Overview_Plan_Type',
WORKFLOWS: 'Workspace_Workflows',
Expand Down Expand Up @@ -964,6 +965,7 @@ const SCREENS = {
CREATE_DISTANCE_RATE_UPGRADE: 'Create_Distance_Rate_Upgrade',
DISTANCE_RATES_SETTINGS: 'Distance_Rates_Settings',
DISTANCE_RATES_UNIT: 'Distance_Rates_Unit',
DISTANCE_RATES_GOVERNMENT_RATE_COUNTRY: 'Distance_Rates_Government_Rate_Country',
DISTANCE_RATES_COMMUTER_EXCLUSIONS: 'Distance_Rates_Commuter_Exclusions',
DISTANCE_RATE_DETAILS: 'Distance_Rate_Details',
DISTANCE_RATE_EDIT: 'Distance_Rate_Edit',
Expand Down
97 changes: 97 additions & 0 deletions src/components/GovernmentRateCountrySelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Shared country picker for the government distance rate auto-update flow, used by the distance rates settings page and the currency change page.

import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';

import {getGovernmentRateCountryOptions} from '@libs/PolicyDistanceRatesUtils';
import type {Option} from '@libs/searchOptions';

import React, {useState} from 'react';
import {View} from 'react-native';

import FullPageOfflineBlockingView from './BlockingViews/FullPageOfflineBlockingView';
import FormAlertWithSubmitButton from './FormAlertWithSubmitButton';
import Header from './Header';
import ScreenWrapper from './ScreenWrapper';
import SelectionList from './SelectionList';
import SingleSelectListItem from './SelectionList/ListItem/SingleSelectListItem';
import Text from './Text';

type GovernmentRateCountrySelectorProps = {
/** Header title shown in the composed Header */
headerTitle: string;

/** Country that is already stored, shown as selected before the user picks anything */
initialSelectedCountry?: string;

/** Renders the offline blocking view instead of the list when the page data is missing */
isBlocked: boolean;

onBackButtonPress: () => void;

/** Called with the picked country when the user saves */
onSave: (selectedCountry: string) => void;

/** Optional intro paragraph shown above the list */
introText?: string;

testID: string;
};

function GovernmentRateCountrySelector({headerTitle, initialSelectedCountry, isBlocked, onBackButtonPress, onSave, introText, testID}: GovernmentRateCountrySelectorProps) {
const styles = useThemeStyles();
const {translate, localeCompare} = useLocalize();

// The stored country can load after first render, so only an explicit pick overrides it
const [pickedCountry, setPickedCountry] = useState<string>();
const selectedCountry = pickedCountry ?? initialSelectedCountry;

const countries = getGovernmentRateCountryOptions(translate, localeCompare, selectedCountry);

const saveCountry = () => {
if (!selectedCountry) {
return;
}
onSave(selectedCountry);
};

const FullPageBlockingView = isBlocked ? FullPageOfflineBlockingView : View;

return (
<ScreenWrapper
style={styles.pb0}
enableEdgeToEdgeBottomSafeAreaPadding
testID={testID}
>
<Header>
<Header.BackButton onPress={onBackButtonPress} />
<Header.Title title={headerTitle} />
</Header>
<FullPageBlockingView style={isBlocked ? [] : styles.flexGrow1}>
{!isBlocked && (
<>
{!!introText && <Text style={[styles.textNormal, styles.mh5, styles.mb3]}>{introText}</Text>}
<SelectionList
data={countries}
ListItem={SingleSelectListItem}
onSelectRow={(option: Option) => setPickedCountry(option.value)}
shouldSingleExecuteRowSelect
addBottomSafeAreaPadding
footerContent={
<FormAlertWithSubmitButton
buttonText={translate('common.save')}
onSubmit={saveCountry}
isDisabled={!selectedCountry}
enabledWhenOffline
containerStyles={[styles.flexReset, styles.flexGrow0, styles.flexShrink0, styles.flexBasisAuto]}
/>
}
/>
</>
)}
</FullPageBlockingView>
</ScreenWrapper>
);
}

export default GovernmentRateCountrySelector;
96 changes: 96 additions & 0 deletions src/hooks/useApplyWorkspaceCurrencyChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {getExpensifyCardEnrollmentRoute, isCurrencySupportedForECards} from '@libs/CardUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getEligibleExistingBusinessBankAccounts} from '@libs/WorkflowUtils';

import {clearCorpayBankAccountFields} from '@userActions/BankAccounts';
import {clearDraftValues} from '@userActions/FormActions';
import {isCurrencySupportedForGlobalReimbursement, updateGeneralSettings} from '@userActions/Policy/Policy';
import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
import type {Policy} from '@src/types/onyx';

import type {OnyxEntry} from 'react-native-onyx';

import useOnyx from './useOnyx';
import useReviewWorkspaceSettingsTaskCompletion from './useReviewWorkspaceSettingsTaskCompletion';

type ApplyWorkspaceCurrencyChangeOptions = {
/** Whether the currency change is forced by the global reimbursement setup, which continues to bank account connection */
isForcedToChangeCurrency?: boolean;
/** Whether the currency change should proceed to Expensify Card enrollment */
shouldStartExpensifyCardEnrollment?: boolean;
/** Country whose government mileage rates should auto-update, needed when several countries share the new currency */
governmentRateCountry?: string;
/** Where to go back to after the change, when the caller sits deeper than one screen in the stack */
backTo?: Route;
};

/**
* Returns a function that applies a workspace currency change: clears bank account drafts, writes the new currency,
* and re-enables government rate auto-update when a country was chosen for a shared (EUR) currency.
*/
function useApplyWorkspaceCurrencyChange(policy: OnyxEntry<Policy>) {
const [bankAccountList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST);
const [supportedCountriesByCurrency] = useOnyx(ONYXKEYS.CARD_SUPPORTED_COUNTRIES);
const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT);
const [governmentMileageRates] = useOnyx(ONYXKEYS.GOVERNMENT_MILEAGE_RATES);
const getReviewWorkspaceSettingsTaskCompletion = useReviewWorkspaceSettingsTaskCompletion();

return (
currencyCode: string,
{isForcedToChangeCurrency = false, shouldStartExpensifyCardEnrollment = false, governmentRateCountry, backTo}: ApplyWorkspaceCurrencyChangeOptions = {},
) => {
if (!policy) {
return;
}
clearDraftValues(ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM);
updateGeneralSettings(policy, policy.name ?? '', currencyCode, getReviewWorkspaceSettingsTaskCompletion(), {
governmentRateCountry,
governmentMileageRates: governmentMileageRates ?? [],
});
clearCorpayBankAccountFields();

const isUkEuCurrencySupported = isCurrencySupportedForECards(currencyCode);
const canEnrollNewCardProgram = currencyCode === CONST.CURRENCY.USD || isUkEuCurrencySupported;
if (shouldStartExpensifyCardEnrollment && canEnrollNewCardProgram) {
Navigation.navigate(
getExpensifyCardEnrollmentRoute({
policyID: policy.id,
currencyCode,
isUkEuCurrencySupported,
bankAccountsList: bankAccountList,
supportedCountriesByCurrency,
achData: reimbursementAccount?.achData,
}),
{forceReplace: true},
);
return;
}

const isSupportedForGlobalReimbursement = isCurrencySupportedForGlobalReimbursement(currencyCode);
if (isForcedToChangeCurrency && isSupportedForGlobalReimbursement) {
const hasValidExistingAccounts = getEligibleExistingBusinessBankAccounts(bankAccountList, currencyCode, true).length > 0;
if (hasValidExistingAccounts) {
Navigation.navigate(ROUTES.BANK_ACCOUNT_CONNECT_EXISTING_BUSINESS_BANK_ACCOUNT.getRoute(policy.id, ROUTES.WORKSPACE_WORKFLOWS.getRoute(policy.id)));
return;
}
navigateToBankAccountRoute({
policyID: policy.id,
backTo: ROUTES.WORKSPACE_WORKFLOWS.getRoute(policy.id),
navigationOptions: {forceReplace: true},
});
return;
}
if (backTo) {
Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(backTo));
return;
}
Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack);
};
}

export default useApplyWorkspaceCurrencyChange;
27 changes: 25 additions & 2 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7678,9 +7678,32 @@ Der Control-Tarif beginnt bei 9 $ pro aktivem Mitglied und Monat.`,
startDate: 'Startdatum',
endDate: 'Enddatum',
autoGeneratedRateTooltip: 'Dieser Satz wurde automatisch erzeugt.',
automaticRates: 'Automatische Sätze',
autoUpdateGovernmentRate: 'Regierungssätze automatisch aktualisieren',
autoUpdateGovernmentRateDescription: (countryPhrase: string) => `Neue Sätze automatisch erstellen, wenn ${countryPhrase} neue Richtlinien veröffentlicht.`,
governmentRateCountries: {US: 'die Vereinigten Staaten', CA: 'Kanada', GB: 'Großbritannien', AU: 'Australien', NO: 'Norwegen', SE: 'Schweden', ZA: 'Südafrika'},
autoUpdateGovernmentRateDescription: (countryPhrase: string) =>
`Neue Sätze automatisch erstellen, wenn ${countryPhrase} neue Richtlinien veröffentlicht. Bestehende Sätze werden nicht geändert.`,
governmentRateCountries: {
US: 'die Vereinigten Staaten',
CA: 'Kanada',
GB: 'Großbritannien',
AU: 'Australien',
AT: 'Österreich',
BE: 'Belgien',
FI: 'Finnland',
DE: 'Deutschland',
NL: 'die Niederlande',
PT: 'Portugal',
ES: 'Spanien',
NO: 'Norwegen',
SE: 'Schweden',
ZA: 'Südafrika',
},
governmentRateCountryGeneric: 'Ihr Land',
governmentRateSourceCountry: 'Quellland der Entfernungssätze',
governmentRateCountrySelectionPrompt:
'Um die Funktion zur automatischen Aktualisierung staatlicher Entfernungssätze weiterhin zu nutzen, bestätigen Sie bitte, welches Land Sie künftig verwenden möchten.',
currencyChangeGovernmentRateWarning: (currency: string, countryPhrase: string) =>
`Achtung! Wenn Sie die Währung des Arbeitsbereichs auf ${currency} ändern, werden stattdessen die staatlichen Kilometersätze verwendet, die ${countryPhrase} veröffentlicht. Möchten Sie wirklich fortfahren?`,
},
editor: {
descriptionInputLabel: 'Beschreibung',
Expand Down
27 changes: 25 additions & 2 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7967,9 +7967,32 @@ _Για πιο αναλυτικές οδηγίες, [επισκεφθείτε τ
startDateMustBeBeforeEndDate: 'Η ημερομηνία έναρξης πρέπει να είναι πριν από την ημερομηνία λήξης',
},
autoGeneratedRateTooltip: 'Αυτός ο συντελεστής δημιουργείται αυτόματα.',
automaticRates: 'Αυτόματες τιμές',
autoUpdateGovernmentRate: 'Αυτόματη ενημέρωση κρατικών συντελεστών',
autoUpdateGovernmentRateDescription: (countryPhrase: string) => `Δημιουργείτε αυτόματα νέες τιμές όταν η ${countryPhrase} δημοσιεύει νέες οδηγίες.`,
governmentRateCountries: {US: 'οι Ηνωμένες Πολιτείες', CA: 'Καναδάς', GB: 'Μεγάλη Βρετανία', AU: 'Αυστραλία', NO: 'Νορβηγία', SE: 'Σουηδία', ZA: 'Νότια Αφρική'},
autoUpdateGovernmentRateDescription: (countryPhrase: string) =>
`Δημιουργείτε αυτόματα νέες τιμές όταν η ${countryPhrase} δημοσιεύει νέες οδηγίες. Δεν τροποποιεί υπάρχουσες τιμές.`,
governmentRateCountries: {
US: 'οι Ηνωμένες Πολιτείες',
CA: 'Καναδάς',
GB: 'Μεγάλη Βρετανία',
AU: 'Αυστραλία',
AT: 'Αυστρία',
BE: 'Βέλγιο',
FI: 'Φινλανδία',
DE: 'Γερμανία',
NL: 'Ολλανδία',
PT: 'Πορτογαλία',
ES: 'Ισπανία',
NO: 'Νορβηγία',
SE: 'Σουηδία',
ZA: 'Νότια Αφρική',
},
governmentRateCountryGeneric: 'τη χώρα σας',
governmentRateSourceCountry: 'Χώρα προέλευσης τιμών απόστασης',
governmentRateCountrySelectionPrompt:
'Για να συνεχίσετε να χρησιμοποιείτε τη λειτουργία αυτόματης ενημέρωσης κρατικών τιμών απόστασης, επιβεβαιώστε ποια χώρα θέλετε να χρησιμοποιείτε στο εξής.',
currencyChangeGovernmentRateWarning: (currency: string, countryPhrase: string) =>
`Προσοχή! Η αλλαγή του νομίσματος του χώρου εργασίας σε ${currency} θα χρησιμοποιεί πλέον τις κρατικές χιλιομετρικές τιμές που δημοσιεύει ${countryPhrase}. Είστε βέβαιοι ότι θέλετε να συνεχίσετε;`,
},
editor: {
descriptionInputLabel: 'Περιγραφή',
Expand Down
Loading
Loading