Skip to content

Commit

Permalink
Merge branch 'main' into revert-54260-revert-54064-and-53396
Browse files Browse the repository at this point in the history
  • Loading branch information
ishpaul777 authored Dec 24, 2024
2 parents 0e97038 + dc784de commit d288477
Show file tree
Hide file tree
Showing 41 changed files with 1,535 additions and 182 deletions.
2 changes: 1 addition & 1 deletion Mobile-Expensify
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
versionCode 1009007706
versionName "9.0.77-6"
versionCode 1009007801
versionName "9.0.78-1"
// Supported language variants must be declared here to avoid from being removed during the compilation.
// This also helps us to not include unnecessary language variants in the APK.
resConfigs "en", "es"
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,14 @@ The three options for the date your report will export with are:
## Accounting Method

This dictates when reimbursable expenses will export, according to your preferred accounting method:
- Accrual: Out of pocket expenses will export immediately when the report is final approved
- Cash: Out of pocket expenses will export when paid via Expensify or marked as Reimbursed
- Accrual: Out-of-pocket expenses will export immediately when the report is final approved
- Cash: Out-of-pocket expenses will export when paid via Expensify or marked as Reimbursed

## Export Settings for Reimbursable Expenses

**Expense Reports:** Expensify transactions will export reimbursable expenses as expense reports by default, which will be posted to the payables account designated in NetSuite.

**Vendor Bills:** Expensify transactions export as vendor bills in NetSuite and will be mapped to the subsidiary associated with the corresponding workspace. Each report will be posted as payable to the vendor associated with the employee who submitted the report. You can also set an approval level in NetSuite for vendor bills.
**Vendor Bills:** Expensify transactions export as vendor bills in NetSuite and are mapped to the subsidiary associated with the corresponding workspace. Each report is posted as payable to the vendor associated with the employee who submitted it. You can also set an approval level in NetSuite for vendor bills.

**Journal Entries:** Expensify transactions that are set to export as journal entries in NetSuite will be mapped to the subsidiary associated with this workspace. All the transactions will be posted to the payable account specified in the workspace. You can also set an approval level in NetSuite for the journal entries.

Expand All @@ -63,7 +63,7 @@ This dictates when reimbursable expenses will export, according to your preferre
- Journal entry forms do not contain a customer column, so it is not possible to export customers or projects with this export option
- The credit line and header level classifications are pulled from the employee record

**Expense Reports:** To use the expense report option for your corporate card expenses, you will need to set up your default corporate cards in NetSuite.
**Expense Reports:** To use the expense report option for your corporate card expenses, you must set up your default corporate cards in NetSuite.

To use a default corporate card for non-reimbursable expenses, you must select the correct card on the employee records (for individual accounts) or the subsidiary record (If you use a non-one world account, the default is found in your accounting preferences).

Expand All @@ -87,6 +87,8 @@ When selecting the option to export non-reimbursable expenses as vendor bills, t

The Coding tab is where NetSuite information is configured in Expensify, which allows employees to code expenses and reports accurately. There are several coding options in NetSuite. Let’s go over each of those below.

![Insert alt text for accessibility here]({{site.url}}/assets/images/NetSuite_Configure_08.png){:width="100%"}

## Expense Categories

Expensify's integration with NetSuite automatically imports NetSuite Expense Categories as Categories in Expensify.
Expand Down Expand Up @@ -225,6 +227,8 @@ From there, you should see the values for the Custom Lists under the Tag or Repo

The NetSuite integration’s advanced configuration settings are accessed under **Settings > Workspaces > Group > _[Workspace Name]_ > Connections > NetSuite > Configure > Advanced tab**.

![Insert alt text for accessibility here]({{site.url}}/assets/images/NetSuite_Configure_09.png){:width="100%"}

Let’s review the different advanced settings and how they interact with the integration.

## Auto Sync
Expand Down
12 changes: 6 additions & 6 deletions docs/articles/expensify-classic/domains/SAML-SSO.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Once the domain is verified, you can access the SSO settings by navigating to Se
**Below are instructions for setting up Expensify for specific SSO providers:**
- [Amazon Web Services (AWS SSO)](https://static.global.sso.amazonaws.com/app-202a715cb67cddd9/instructions/index.htm)
- [Google SAML](https://support.google.com/a/answer/7371682) (for GSuite, not Google SSO)
- [Microsoft Azure Active Directory](https://azure.microsoft.com/en-us/documentation/articles/active-directory-saas-expensify-tutorial/)
- [Microsoft Entra ID (formerly Azure Active Directory)](https://learn.microsoft.com/en-us/entra/identity/saas-apps/expensify-tutorial)
- [Okta](https://saml-doc.okta.com/SAML_Docs/How-to-Configure-SAML-2.0-for-Expensify.html)
- [OneLogin](https://onelogin.service-now.com/support?id=kb_article&sys_id=e44c9e52db187410fe39dde7489619ba)
- [Oracle Identity Cloud Service](https://docs.oracle.com/en/cloud/paas/identity-cloud/idcsc/expensify.html#Expensify)
Expand All @@ -39,13 +39,13 @@ The entityID for Expensify is https://expensify.com. Remember not to copy and pa
## Can you have multiple domains with only one entity ID?
Yes. Please send a message to the Concierge or your account manager, and we will enable the use of the same entity ID with multiple domains.

## How can I update the Microsoft Azure SSO Certificate?
## How can I update the Microsoft Entra ID SSO Certificate?
Expensify's SAML configuration doesn't support multiple active certificates. This means that if you create the new certification ahead of time without first removing the old one, the respective IDP will include two unique x509 certificates instead of one, and the connection will break. Should you need to access Expensify, switching back to the old certificate will continue to allow access while that certificate is still valid.

**To transfer from one Microsoft Azure certificate to another, please follow the below steps:**
1. In Azure Directory, create your new certificate.
2. In Azure Director, remove the old, expiring certificate.
3. In Azure Directory, activate the remaining certificate and get a new IDP for Expensify from it.
**To transfer from one Microsoft Entra certificate to another, please follow the below steps:**
1. In Microsoft Entra, create your new certificate.
2. In Microsoft Entra, remove the old, expiring certificate.
3. In Microsoft Entra, activate the remaining certificate and get a new IDP for Expensify from it.
4. In Expensify, replace the previous IDP with the new IDP.
5. Log in via SSO. If login continues to fail, write to Concierge for assistance.

Expand Down
4 changes: 2 additions & 2 deletions ios/NewExpensify/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>9.0.77</string>
<string>9.0.78</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
Expand All @@ -40,7 +40,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>9.0.77.6</string>
<string>9.0.78.1</string>
<key>FullStory</key>
<dict>
<key>OrgId</key>
Expand Down
4 changes: 2 additions & 2 deletions ios/NewExpensifyTests/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@
<key>CFBundlePackageType</key>
<string>BNDL</string>
<key>CFBundleShortVersionString</key>
<string>9.0.77</string>
<string>9.0.78</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>9.0.77.6</string>
<string>9.0.78.1</string>
</dict>
</plist>
4 changes: 2 additions & 2 deletions ios/NotificationServiceExtension/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundleShortVersionString</key>
<string>9.0.77</string>
<string>9.0.78</string>
<key>CFBundleVersion</key>
<string>9.0.77.6</string>
<string>9.0.78.1</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
"version": "9.0.77-6",
"version": "9.0.78-1",
"author": "Expensify, Inc.",
"homepage": "https://new.expensify.com",
"description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.",
Expand Down
4 changes: 4 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1152,6 +1152,7 @@ const CONST = {
UPDATE_TIME_RATE: 'POLICYCHANGELOG_UPDATE_TIME_RATE',
LEAVE_POLICY: 'POLICYCHANGELOG_LEAVE_POLICY',
CORPORATE_UPGRADE: 'POLICYCHANGELOG_CORPORATE_UPGRADE',
TEAM_DOWNGRADE: 'POLICYCHANGELOG_TEAM_DOWNGRADE',
},
ROOM_CHANGE_LOG: {
INVITE_TO_ROOM: 'INVITETOROOM',
Expand Down Expand Up @@ -1335,6 +1336,9 @@ const CONST = {
SEARCH_OPTION_LIST_DEBOUNCE_TIME: 300,
RESIZE_DEBOUNCE_TIME: 100,
UNREAD_UPDATE_DEBOUNCE_TIME: 300,
SEARCH_CONVERT_SEARCH_VALUES: 'search_convert_search_values',
SEARCH_MAKE_TREE: 'search_make_tree',
SEARCH_BUILD_TREE: 'search_build_tree',
SEARCH_FILTER_OPTIONS: 'search_filter_options',
USE_DEBOUNCED_STATE_DELAY: 300,
LIST_SCROLLING_DEBOUNCE_TIME: 200,
Expand Down
18 changes: 14 additions & 4 deletions src/components/Search/SearchRouter/SearchRouterList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {SearchQueryItem, SearchQueryListItemProps} from '@components/Select
import type {SectionListDataType, SelectionListHandle, UserListItemProps} from '@components/SelectionList/types';
import UserListItem from '@components/SelectionList/UserListItem';
import useActiveWorkspace from '@hooks/useActiveWorkspace';
import useFastSearchFromOptions from '@hooks/useFastSearchFromOptions';
import useLocalize from '@hooks/useLocalize';
import usePolicy from '@hooks/usePolicy';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
Expand Down Expand Up @@ -179,7 +180,7 @@ function SearchRouterList(
if (currentUser) {
autocompleteOptions.push({
name: currentUser.displayName ?? Str.removeSMSDomain(currentUser.login ?? ''),
accountID: currentUser.accountID?.toString() ?? '-1',
accountID: currentUser.accountID?.toString(),
});
}

Expand Down Expand Up @@ -382,21 +383,30 @@ function SearchRouterList(
};
});

/**
* Builds a suffix tree and returns a function to search in it.
*/
const filterOptions = useFastSearchFromOptions(searchOptions, {includeUserToInvite: true});

const recentReportsOptions = useMemo(() => {
if (autocompleteQueryValue.trim() === '') {
return searchOptions.recentReports.slice(0, 20);
}

Timing.start(CONST.TIMING.SEARCH_FILTER_OPTIONS);
const filteredOptions = OptionsListUtils.filterAndOrderOptions(searchOptions, autocompleteQueryValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true});
const filteredOptions = filterOptions(autocompleteQueryValue);
const orderedOptions = OptionsListUtils.combineOrderingOfReportsAndPersonalDetails(filteredOptions, autocompleteQueryValue, {
sortByReportTypeInSearch: true,
preferChatroomsOverThreads: true,
});
Timing.end(CONST.TIMING.SEARCH_FILTER_OPTIONS);

const reportOptions: OptionData[] = [...filteredOptions.recentReports, ...filteredOptions.personalDetails];
const reportOptions: OptionData[] = [...orderedOptions.recentReports, ...orderedOptions.personalDetails];
if (filteredOptions.userToInvite) {
reportOptions.push(filteredOptions.userToInvite);
}
return reportOptions.slice(0, 20);
}, [autocompleteQueryValue, searchOptions]);
}, [autocompleteQueryValue, filterOptions, searchOptions]);

useEffect(() => {
ReportUserActions.searchInServer(autocompleteQueryValue.trim());
Expand Down
113 changes: 113 additions & 0 deletions src/hooks/useFastSearchFromOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {useMemo} from 'react';
import FastSearch from '@libs/FastSearch';
import * as OptionsListUtils from '@libs/OptionsListUtils';

type AllOrSelectiveOptions = OptionsListUtils.ReportAndPersonalDetailOptions | OptionsListUtils.Options;

type Options = {
includeUserToInvite: boolean;
};

const emptyResult = {
personalDetails: [],
recentReports: [],
};

// You can either use this to search within report and personal details options
function useFastSearchFromOptions(
options: OptionsListUtils.ReportAndPersonalDetailOptions,
config?: {includeUserToInvite: false},
): (searchInput: string) => OptionsListUtils.ReportAndPersonalDetailOptions;
// Or you can use this to include the user invite option. This will require passing all options
function useFastSearchFromOptions(options: OptionsListUtils.Options, config?: {includeUserToInvite: true}): (searchInput: string) => OptionsListUtils.Options;

/**
* Hook for making options from OptionsListUtils searchable with FastSearch.
* Builds a suffix tree and returns a function to search in it.
*
* @example
* ```
* const options = OptionsListUtils.getSearchOptions(...);
* const filterOptions = useFastSearchFromOptions(options);
*/
function useFastSearchFromOptions(
options: OptionsListUtils.ReportAndPersonalDetailOptions | OptionsListUtils.Options,
{includeUserToInvite}: Options = {includeUserToInvite: false},
): (searchInput: string) => AllOrSelectiveOptions {
const findInSearchTree = useMemo(() => {
const fastSearch = FastSearch.createFastSearch([
{
data: options.personalDetails,
toSearchableString: (option) => {
const displayName = option.participantsList?.[0]?.displayName ?? '';
return [option.login ?? '', option.login !== displayName ? displayName : ''].join();
},
uniqueId: (option) => option.login,
},
{
data: options.recentReports,
toSearchableString: (option) => {
const searchStringForTree = [option.text ?? '', option.login ?? ''];

if (option.isThread) {
if (option.alternateText) {
searchStringForTree.push(option.alternateText);
}
} else if (!!option.isChatRoom || !!option.isPolicyExpenseChat) {
if (option.subtitle) {
searchStringForTree.push(option.subtitle);
}
}

return searchStringForTree.join();
},
},
]);

function search(searchInput: string): AllOrSelectiveOptions {
const searchWords = searchInput.split(' ').sort(); // asc sorted
const longestSearchWord = searchWords.at(searchWords.length - 1); // longest word is the last element
if (!longestSearchWord) {
return emptyResult;
}

// The user might separated words with spaces to do a search such as: "jo d" -> "john doe"
// With the suffix search tree you can only search for one word at a time. Its most efficient to search for the longest word,
// (as this will limit the results the most) and then afterwards run a quick filter on the results to see if the other words are present.
let [personalDetails, recentReports] = fastSearch.search(longestSearchWord);

if (searchWords.length > 1) {
personalDetails = personalDetails.filter((pd) => OptionsListUtils.isSearchStringMatch(searchInput, pd.text));
recentReports = recentReports.filter((rr) => OptionsListUtils.isSearchStringMatch(searchInput, rr.text));
}

if (includeUserToInvite && 'currentUserOption' in options) {
const userToInvite = OptionsListUtils.filterUserToInvite(
{
...options,
personalDetails,
recentReports,
},
searchInput,
);
return {
personalDetails,
recentReports,
userToInvite,
currentUserOption: options.currentUserOption,
};
}

return {
personalDetails,
recentReports,
};
}

return search;
}, [includeUserToInvite, options]);

return findInSearchTree;
}

export default useFastSearchFromOptions;
22 changes: 22 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,7 @@ const translations = {
drafts: 'Drafts',
finished: 'Finished',
upgrade: 'Upgrade',
downgradeWorkspace: 'Downgrade workspace',
companyID: 'Company ID',
userID: 'User ID',
disable: 'Disable',
Expand Down Expand Up @@ -4368,6 +4369,27 @@ const translations = {
},
},
},
downgrade: {
commonFeatures: {
title: 'Downgrade to the Collect plan',
note: 'If you downgrade, you’ll lose access to these features and more:',
benefits: {
note: 'For a full comparison of our plans, check out our',
pricingPage: 'pricing page',
confirm: 'Are you sure you want to downgrade and remove your configurations?',
warning: 'This cannot be undone.',
benefit1: 'Accounting connections (except QuickBooks Online and Xero)',
benefit2: 'Smart expense rules',
benefit3: 'Multi-level approval workflows',
benefit4: 'Enhanced security controls',
},
},
completed: {
headline: 'Your workspace has been downgraded',
description: 'You have other workspace on the Control plan. To be billed at the Collect rate, you must downgrade all workspaces.',
gotIt: 'Got it, thanks',
},
},
restrictedAction: {
restricted: 'Restricted',
actionsAreCurrentlyRestricted: ({workspaceName}: ActionsAreCurrentlyRestricted) => `Actions on the ${workspaceName} workspace are currently restricted`,
Expand Down
Loading

0 comments on commit d288477

Please sign in to comment.