Skip to content

Commit

Permalink
Merge branch 'Expensify:main' into issue37153
Browse files Browse the repository at this point in the history
  • Loading branch information
allgandalf authored Mar 22, 2024
2 parents dc6f310 + 67acd7b commit 0e1b166
Show file tree
Hide file tree
Showing 77 changed files with 681 additions and 856 deletions.
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
versionCode 1001045501
versionName "1.4.55-1"
versionCode 1001045503
versionName "1.4.55-3"
}

flavorDimensions "default"
Expand Down
13 changes: 13 additions & 0 deletions docs/articles/expensify-classic/expenses/Export-expenses.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
title: Export expenses
description: Export expenses to a CSV
---
<div id="expensify-classic" markdown="1">

1. Click the **Expenses** tab.
2. Select the expenses you want to export by checking the box to the left of each expense or selecting them all.
3. Click **Export To** in the right corner and select either:
- **Default CSV**: Use Expensify’s default template
- **Create new CSV export layout**: Create your own custom CSV template

</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
title: Assign billing owner and payment account
description: Determine who will cover the cost of the workspace and link a payment method
---
<div id="expensify-classic" markdown="1">

The person who creates a workspace will automatically be responsible for the billing for that workspace. However, the existing billing owner can transfer the workspace’s billing ownership to any Admin on the workspace.

{% include info.html %}
There can only be one billing owner at a time. Assigning a new billing owner will automatically un-assign the existing billing owner. However, billing owners are also workspace admins by default, and the previous billing owner will remain a workspace admin unless manually updated.
{% include end-info.html %}

# Assign a new billing owner

To assign a new billing owner, **the person who will take over responsibility for the workspace billing must complete the following process**:

1. Hover over Settings, then click **Workspaces**.
2. Click the desired workspace name.
3. Under Workspace Overview, click **Take Over Billing**.

# Add or update payment account

Once you take over billing for a workspace, you must add a payment method to your account.

1. Hover over Settings, then click **Account**.
2. Click the **Payments** tab.
3. Scroll down to the Payment Details sections and click **Add Payment Card**.
4. Enter your credit or debit card information and click **Accept terms, add payment card, and pay $0.00** (the box will only show a balance if one is due).

</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
title: Create a group workspace
description: Create a workspace for your team's expense reports
---
<div id="expensify-classic" markdown="1">

A workspace is the set of rules, settings, and spending limits for expense reports in your organization. This includes the unique expense categories and tags, budgets, currency and tax settings, etc. that all workspace members will use. A workspace also defines the approval workflow for your employees, as well as the accounting connection if using an accounting software integration.

Here are a couple examples of when you’d want to create different workspaces:

- You have employees with expense reports in different currencies. For example, you may have a workspace for employees who live in the US and submit their reports in USD and a workspace for employees who live in Canada and submit in CAD.
- You want to limit specific groups of people to their own set of expense coding options (categories/tags) then they can separate their employees by Sales, Marketing, Support, etc.

To create a group workspace,

1. Hover over Settings, then click **Workspaces**.
2. Click the **Group** tab on the left.
3. Click **New Workspace**.
4. Enter the workspace name and select a workspace type.
- **Collect**: Ideal for small groups who only need basic features like expense approvals, reimbursement, corporate card management, and integration options.
- **Control**: For groups that need a deeper level of control and configurations, like multi-stage approval workflows, corporate card management, integrations, and more. This is the most popular option.
5. Set up your workspace details including the workspace name, expense rules, categories, and more.

</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
title: Set up your individual workspace
description: Capture your personal expenses
---
<div id="expensify-classic" markdown="1">

All Expensify accounts come with an individual workspace where you can track your personal expenses. If you want to connect your personal expenses to an accounting or travel integration, you can create a group workspace—even if you will be the only person in the group.

To set up your individual workspace,

1. Hover over Settings, then click **Workspaces**.
2. Click the **Individual** tab on the left.
3. Select the policy type that best fits your needs.
4. Set up your workspace details including the workspace name, expense rules, categories, and more.

{% include info.html %}
You can create multiple group workspaces, but you can only create one individual workspace.
{% include end-info.html %}

</div>
1 change: 1 addition & 0 deletions docs/redirects.csv
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,4 @@ https://help.expensify.com/articles/expensify-classic/expensify-billing/Individu
https://help.expensify.com/articles/expensify-classic/settings/Merge-Accounts,https://help.expensify.com/articles/expensify-classic/settings/account-settings/Merge-accounts
https://help.expensify.com/articles/expensify-classic/settings/Preferences,https://help.expensify.com/expensify-classic/hubs/settings/account-settings
https://help.expensify.com/articles/expensify-classic/getting-started/support/Your-Expensify-Account-Manager,https://use.expensify.com/support
https://help.expensify.com/articles/expensify-classic/settings/Copilot,https://help.expensify.com/expensify-classic/hubs/copilots-and-delegates/
2 changes: 1 addition & 1 deletion ios/NewExpensify/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>1.4.55.1</string>
<string>1.4.55.3</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
Expand Down
2 changes: 1 addition & 1 deletion ios/NewExpensifyTests/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,6 @@
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.4.55.1</string>
<string>1.4.55.3</string>
</dict>
</plist>
2 changes: 1 addition & 1 deletion ios/NotificationServiceExtension/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<key>CFBundleShortVersionString</key>
<string>1.4.55</string>
<key>CFBundleVersion</key>
<string>1.4.55.1</string>
<string>1.4.55.3</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
Expand Down
11 changes: 6 additions & 5 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "new.expensify",
"version": "1.4.55-1",
"version": "1.4.55-3",
"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 Expand Up @@ -121,7 +121,7 @@
"react": "18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-collapse": "^5.1.0",
"react-content-loader": "^6.1.0",
"react-content-loader": "^7.0.0",
"react-dom": "18.1.0",
"react-error-boundary": "^4.0.11",
"react-map-gl": "^7.1.3",
Expand Down
43 changes: 43 additions & 0 deletions patches/react-native-web+0.19.9+006+fixPointerEventDown.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
diff --git a/node_modules/react-native-web/dist/modules/useResponderEvents/ResponderSystem.js b/node_modules/react-native-web/dist/modules/useResponderEvents/ResponderSystem.js
index 0aec2d6..a71aec2 100644
--- a/node_modules/react-native-web/dist/modules/useResponderEvents/ResponderSystem.js
+++ b/node_modules/react-native-web/dist/modules/useResponderEvents/ResponderSystem.js
@@ -133,7 +133,7 @@ to return true:wantsResponderID| |

import createResponderEvent from './createResponderEvent';
import { isCancelish, isEndish, isMoveish, isScroll, isSelectionChange, isStartish } from './ResponderEventTypes';
-import { getLowestCommonAncestor, getResponderPaths, hasTargetTouches, hasValidSelection, isPrimaryPointerDown, setResponderId } from './utils';
+import { getLowestCommonAncestor, getResponderPaths, hasTargetTouches, hasValidSelection, isPrimaryOrSecondaryPointerDown, setResponderId } from './utils';
import { ResponderTouchHistoryStore } from './ResponderTouchHistoryStore';
import canUseDOM from '../canUseDom';

@@ -225,7 +225,7 @@ function eventListener(domEvent) {
}
return;
}
- var isStartEvent = isStartish(eventType) && isPrimaryPointerDown(domEvent);
+ var isStartEvent = isStartish(eventType) && isPrimaryOrSecondaryPointerDown(domEvent);
var isMoveEvent = isMoveish(eventType);
var isEndEvent = isEndish(eventType);
var isScrollEvent = isScroll(eventType);
diff --git a/node_modules/react-native-web/dist/modules/useResponderEvents/utils.js b/node_modules/react-native-web/dist/modules/useResponderEvents/utils.js
index 7382cdd..d88f6c0 100644
--- a/node_modules/react-native-web/dist/modules/useResponderEvents/utils.js
+++ b/node_modules/react-native-web/dist/modules/useResponderEvents/utils.js
@@ -148,14 +148,14 @@ export function hasValidSelection(domEvent) {
/**
* Events are only valid if the primary button was used without specific modifier keys.
*/
-export function isPrimaryPointerDown(domEvent) {
+export function isPrimaryOrSecondaryPointerDown(domEvent) {
var altKey = domEvent.altKey,
button = domEvent.button,
buttons = domEvent.buttons,
ctrlKey = domEvent.ctrlKey,
type = domEvent.type;
var isTouch = type === 'touchstart' || type === 'touchmove';
- var isPrimaryMouseDown = type === 'mousedown' && (button === 0 || buttons === 1);
+ var isPrimaryMouseDown = type === 'mousedown' && (button === 0 || buttons === 1 || buttons === 2);
var isPrimaryMouseMove = type === 'mousemove' && buttons === 1;
var noModifiers = altKey === false && ctrlKey === false;
if (isTouch || isPrimaryMouseDown && noModifiers || isPrimaryMouseMove && noModifiers) {
1 change: 1 addition & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ const CONST = {
EXPORTEDTOQUICKBOOKS: 'EXPORTEDTOQUICKBOOKS', // OldDot Action
FORWARDED: 'FORWARDED', // OldDot Action
HOLD: 'HOLD',
HOLDCOMMENT: 'HOLDCOMMENT',
IOU: 'IOU',
INTEGRATIONSMESSAGE: 'INTEGRATIONSMESSAGE', // OldDot Action
MANAGERATTACHRECEIPT: 'MANAGERATTACHRECEIPT', // OldDot Action
Expand Down
4 changes: 1 addition & 3 deletions src/Expensify.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,10 @@ import Navigation from './libs/Navigation/Navigation';
import NavigationRoot from './libs/Navigation/NavigationRoot';
import NetworkConnection from './libs/NetworkConnection';
import PushNotification from './libs/Notification/PushNotification';
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import './libs/Notification/PushNotification/subscribePushNotification';
import StartupTimer from './libs/StartupTimer';
// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import UnreadIndicatorUpdater from './libs/UnreadIndicatorUpdater';
import './libs/UnreadIndicatorUpdater';
import Visibility from './libs/Visibility';
import ONYXKEYS from './ONYXKEYS';
import PopoverReportActionContextMenu from './pages/home/report/ContextMenu/PopoverReportActionContextMenu';
Expand Down
32 changes: 16 additions & 16 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ const ROUTES = {
},
REPORT_ATTACHMENTS: {
route: 'r/:reportID/attachment',
getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURI(source)}` as const,
getRoute: (reportID: string, source: string) => `r/${reportID}/attachment?source=${encodeURIComponent(source)}` as const,
},
REPORT_PARTICIPANTS: {
route: 'r/:reportID/participants',
Expand Down Expand Up @@ -304,13 +304,10 @@ const ROUTES = {
route: ':iouType/new/receipt/:reportID?',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/receipt/${reportID}` as const,
},
MONEY_REQUEST_DISTANCE: {
route: ':iouType/new/address/:reportID?',
getRoute: (iouType: string, reportID = '') => `${iouType}/new/address/${reportID}` as const,
},
MONEY_REQUEST_CREATE: {
route: 'create/:iouType/start/:transactionID/:reportID',
getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}` as const,
route: ':action/:iouType/start/:transactionID/:reportID',
getRoute: (action: ValueOf<typeof CONST.IOU.ACTION>, iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string) =>
`create/${iouType}/start/${transactionID}/${reportID}` as const,
},
MONEY_REQUEST_STEP_CONFIRMATION: {
route: 'create/:iouType/confirmation/:transactionID/:reportID',
Expand Down Expand Up @@ -352,9 +349,9 @@ const ROUTES = {
getUrlWithBackToParam(`${action}/${iouType}/description/${transactionID}/${reportID}${reportActionID ? `/${reportActionID}` : ''}`, backTo),
},
MONEY_REQUEST_STEP_DISTANCE: {
route: 'create/:iouType/distance/:transactionID/:reportID',
getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, backTo = '') =>
getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}`, backTo),
route: ':action/:iouType/distance/:transactionID/:reportID',
getRoute: (action: ValueOf<typeof CONST.IOU.ACTION>, iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string, backTo = '') =>
getUrlWithBackToParam(`${action}/${iouType}/distance/${transactionID}/${reportID}`, backTo),
},
MONEY_REQUEST_STEP_MERCHANT: {
route: ':action/:iouType/merchant/:transactionID/:reportID',
Expand Down Expand Up @@ -395,16 +392,19 @@ const ROUTES = {
getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, iouRequestType: ValueOf<typeof CONST.IOU.REQUEST_TYPE>) => `start/${iouType}/${iouRequestType}` as const,
},
MONEY_REQUEST_CREATE_TAB_DISTANCE: {
route: 'create/:iouType/start/:transactionID/:reportID/distance',
getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}/distance` as const,
route: ':action/:iouType/start/:transactionID/:reportID/distance',
getRoute: (action: ValueOf<typeof CONST.IOU.ACTION>, iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string) =>
`create/${iouType}/start/${transactionID}/${reportID}/distance` as const,
},
MONEY_REQUEST_CREATE_TAB_MANUAL: {
route: 'create/:iouType/start/:transactionID/:reportID/manual',
getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}/manual` as const,
route: ':action/:iouType/start/:transactionID/:reportID/manual',
getRoute: (action: ValueOf<typeof CONST.IOU.ACTION>, iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string) =>
`create/${iouType}/start/${transactionID}/${reportID}/manual` as const,
},
MONEY_REQUEST_CREATE_TAB_SCAN: {
route: 'create/:iouType/start/:transactionID/:reportID/scan',
getRoute: (iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string) => `create/${iouType}/start/${transactionID}/${reportID}/scan` as const,
route: ':action/:iouType/start/:transactionID/:reportID/scan',
getRoute: (action: ValueOf<typeof CONST.IOU.ACTION>, iouType: ValueOf<typeof CONST.IOU.TYPE>, transactionID: string, reportID: string) =>
`create/${iouType}/start/${transactionID}/${reportID}/scan` as const,
},

IOU_REQUEST: 'request/new',
Expand Down
1 change: 0 additions & 1 deletion src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,6 @@ const SCREENS = {
CURRENCY: 'Money_Request_Currency',
WAYPOINT: 'Money_Request_Waypoint',
EDIT_WAYPOINT: 'Money_Request_Edit_Waypoint',
DISTANCE: 'Money_Request_Distance',
RECEIPT: 'Money_Request_Receipt',
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,19 @@ import CONST from '@src/CONST';
function extractAttachmentsFromReport(parentReportAction, reportActions) {
const actions = [parentReportAction, ...ReportActionsUtils.getSortedReportActions(_.values(reportActions))];
const attachments = [];
// We handle duplicate image sources by considering the first instance as original. Selecting any duplicate
// and navigating back (<) shows the image preceding the first instance, not the selected duplicate's position.
const uniqueSources = new Set();

const htmlParser = new HtmlParser({
onopentag: (name, attribs) => {
if (name === 'video') {
const source = tryResolveUrlFromApiRoot(attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE]);
if (uniqueSources.has(source)) {
return;
}

uniqueSources.add(source);
const splittedUrl = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE].split('/');
attachments.unshift({
reportActionID: null,
Expand All @@ -35,7 +44,20 @@ function extractAttachmentsFromReport(parentReportAction, reportActions) {
if (name === 'img' && attribs.src) {
const expensifySource = attribs[CONST.ATTACHMENT_SOURCE_ATTRIBUTE];
const source = tryResolveUrlFromApiRoot(expensifySource || attribs.src);
const fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`);
if (uniqueSources.has(source)) {
return;
}

uniqueSources.add(source);
let fileName = attribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || FileUtils.getFileName(`${source}`);

// Public image URLs might lack a file extension in the source URL, without an extension our
// AttachmentView fails to recognize them as images and renders fallback content instead.
// We apply this small hack to add an image extension and ensure AttachmentView renders the image.
const fileInfo = FileUtils.splitExtensionFromFileName(fileName);
if (!fileInfo.fileExtension) {
fileName = `${fileInfo.fileName || 'image'}.jpg`;
}

// By iterating actions in chronological order and prepending each attachment
// we ensure correct order of attachments even across actions with multiple attachments.
Expand Down
Loading

0 comments on commit 0e1b166

Please sign in to comment.