Tax Rates page is empty when creating a new expense while offline
If you haven’t already, check out our contributing guidelines for onboarding and email [email protected] to request to join our Slack channel!
Version Number: 9.2.75-0 Reproducible in staging?: Y Reproducible in production?: Y If this was caught during regression testing, add the test name, ID and link from BrowserStack: Email or phone of affected tester (no customers): Logs: https://stackoverflow.com/c/expensify/questions/4856 Expensify/Expensify Issue URL: Issue reported by: @cristipaval Slack conversation (hyperlinked to channel name): #Expensify Bugs
Action Performed:
- Have a non-default workspace with taxes enabled
- Go offline after a fresh sign-in (or clear cache from the Troubleshoot menu)
- Start creating an expense from Global Create and choose the workspace with taxes as the recipient
Expected Result:
Tax rate field displayed to choose
Actual Result:
The Tax Rate field is empty, and when you click on it, you get an empty list to pick from
Workaround:
Unknown
Platforms:
Select the officially supported platforms where the issue was reproduced:
- [ ] Android: App
- [ ] Android: mWeb Chrome
- [ ] iOS: App
- [ ] iOS: mWeb Safari
- [ ] iOS: mWeb Chrome
- [x] Windows: Chrome
- [x] MacOS: Chrome / Safari
- [ ] MacOS: Desktop
Platforms Tested:
On which of our officially supported platforms was this issue tested:- [ ] Android: App
- [ ] Android: mWeb Chrome
- [ ] iOS: App
- [ ] iOS: mWeb Safari
- [ ] iOS: mWeb Chrome
- [x] Windows: Chrome
- [ ] MacOS: Chrome / Safari
- [ ] MacOS: Desktop
Screenshots/Videos
Add any screenshot/video evidence
https://github.com/user-attachments/assets/38f13b21-3f86-4757-8c87-2581e934ce39
Triggered auto assignment to @isabelastisser (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.
Proposal
Please re-state the problem that we are trying to solve in this issue.
After signing in and going offline, creating an expense in a workspace with taxes enabled shows an empty Tax Rate list, so users cannot select a tax rate when offline.
What is the root cause of that problem?
The app never fetches tax rates right after login when a policy has tax tracking turned on, so the cached policy object lacks taxRates.taxes when the user later goes offline. The policy Onyx subscription only stores policies locally and does not request tax rates from the server for newly loaded policies https://github.com/Expensify/App/blob/bd180d78952816a042f7a763c99b021e784e92b0/src/libs/actions/Policy/Policy.ts#L180-L239
What changes do you think we should make in order to solve the problem?
Add a way to the policy Onyx subscription that auto-fetches tax rates the first time we see a policy with tax tracking enabled but no tax rates cached, while avoiding duplicate requests and skipping when offline:
const policiesRequestingTaxRates = new Set<string>();
function shouldFetchPolicyTaxRates(policy: OnyxEntry<Policy>): boolean {
return !!policy?.id && policy?.tax?.trackingEnabled && isEmptyObject(policy?.taxRates?.taxes ?? {});
}
function fetchPolicyTaxRatesIfNeeded(policyID: string | undefined, policy: OnyxEntry<Policy>) {
if (!policyID || policiesRequestingTaxRates.has(policyID) || NetworkStore.isOffline()) {
return;
}
if (!shouldFetchPolicyTaxRates(policy)) {
return;
}
policiesRequestingTaxRates.add(policyID);
openPolicyTaxesPage(policyID);
}
Onyx.connect({
key: ONYXKEYS.COLLECTION.POLICY,
callback: (val, key) => {
if (!key) {
return;
}
const policyID = val?.id ?? key.replace(ONYXKEYS.COLLECTION.POLICY, '');
if (val === null || val === undefined) {
policiesRequestingTaxRates.delete(policyID);
delete deprecatedAllPolicies[key];
return;
}
deprecatedAllPolicies[key] = val;
fetchPolicyTaxRatesIfNeeded(policyID, val);
},
});
This ensures tax rates are downloaded and cached during the initial online session, so the tax rate picker can show options even after going offline.
Proposal
Please re-state the problem that we are trying to solve in this issue.
When creating an expense offline (after a fresh sign-in or clearing cache) and selecting a workspace with taxes enabled as the recipient, the Tax Rate field is empty and clicking it shows an empty list with no tax rate options to choose from.
What is the root cause of that problem?
The Tax Rate picker builds its options from policy.taxRates.taxes stored in Onyx. The data flow is: TaxPicker → getTaxRatesSection() → transformedTaxRates() → reads from policy.taxRates.taxes.
After a fresh sign-in or cache clear, the workspace policy may have tax.trackingEnabled: true but taxRates hasn't been fetched and cached in Onyx yet. If the user goes offline before this data is available, the app cannot fetch it, so transformedTaxRates() returns an empty object and the picker has no options to display.
What changes do you think we should make in order to solve the problem?
When tax tracking is enabled but policy.taxRates (or policy.taxRates.taxes) is missing or empty, fall back to CONST.DEFAULT_TAX so the user can still select a tax rate offline.
Code changes:
Update two functions in App/src/libs/TransactionUtils/index.ts:
getDefaultTaxCode()- Add fallback logic to use default tax rates when policy tax rates are missing:
function getDefaultTaxCode(policy: OnyxEntry<Policy>, transaction: OnyxEntry<Transaction>, currency?: string | undefined): string | undefined {
const policyTaxRates = policy?.taxRates;
const shouldFallbackToDefaultTaxRates =
!!policy?.tax?.trackingEnabled && (!policyTaxRates || isEmptyObject(policyTaxRates) || isEmptyObject(policyTaxRates?.taxes ?? {}));
const taxRates = shouldFallbackToDefaultTaxRates
? {
...CONST.DEFAULT_TAX,
...policyTaxRates,
taxes: {
...CONST.DEFAULT_TAX.taxes,
...(policyTaxRates?.taxes ?? {}),
},
}
: policyTaxRates;
// ... rest of existing logic uses taxRates instead of policyTaxRates
}
transformedTaxRates()- Add the same fallback logic so the picker always has options:
function transformedTaxRates(policy: OnyxEntry<Policy> | undefined, transaction?: OnyxEntry<Transaction>): Record<string, TaxRate> {
const policyTaxRates = policy?.taxRates;
const shouldFallbackToDefaultTaxRates =
!!policy?.tax?.trackingEnabled && (!policyTaxRates || isEmptyObject(policyTaxRates) || isEmptyObject(policyTaxRates?.taxes ?? {}));
// When taxes are enabled but tax rates haven't been fetched/cached yet (common right after fresh sign-in / cache clear),
// fall back to the default tax rates so the user can still pick a tax rate offline.
const taxRates = shouldFallbackToDefaultTaxRates
? {
...CONST.DEFAULT_TAX,
...policyTaxRates,
taxes: {
...CONST.DEFAULT_TAX.taxes,
...(policyTaxRates?.taxes ?? {}),
},
}
: policyTaxRates;
// ... rest of existing logic uses taxRates instead of policyTaxRates
}
The fallback only activates when policy.tax.trackingEnabled is true and tax rates are missing/empty, ensuring we don't override actual workspace tax configurations.
What alternative solutions did you explore? (Optional)
- Fetch tax rates on-demand: Call the policy taxes page API when opening the picker. This fails offline and adds unnecessary network requests.
- Hide/lock the Tax Rate field: Disable the field until tax rates are downloaded. This blocks offline expense creation entirely, which is worse UX.
This should be internal
@cristipaval This happens when the app doesn't have the tax rates available. This can be fixed on FE, what do you think?
My BZ assignments are paused until March. Reassigning so I can prioritize the KYC squad issues.
Triggered auto assignment to @stephanieelliott (Bug), see https://stackoverflow.com/c/expensify/questions/14418 for more details. Please add this bug to a GH project, as outlined in the SO.
Couldn't get to it yet. I'll investigate it soon
@cristipaval @stephanieelliott this issue was created 2 weeks ago. Are we close to a solution? Let's make sure we're treating this as a top priority. Don't hesitate to create a thread in #expensify-open-source to align faster in real time. Thanks!
@cristipaval, @stephanieelliott Uh oh! This issue is overdue by 2 days. Don't forget to update your issues!