Skip to content

Commit

Permalink
Merge branch 'main' into jasper-editDistanceRequests
Browse files Browse the repository at this point in the history
  • Loading branch information
tgolen committed Sep 28, 2023
2 parents ad21933 + d63e76f commit 64b5073
Show file tree
Hide file tree
Showing 32 changed files with 534 additions and 521 deletions.
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
multiDexEnabled rootProject.ext.multiDexEnabled
versionCode 1001037402
versionName "1.3.74-2"
versionCode 1001037403
versionName "1.3.74-3"
}

flavorDimensions "default"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,36 @@
---
title: Upload Receipts
description: Upload Receipts
title: Upload-Receipts.md
description: This article shows you all the ways that you can upload your receipts to Expensify!
---
## Resource Coming Soon!
<!-- The lines above are required by Jekyll to process the .md file -->

# About
Need to get paid? Check out this guide to see all the ways that you can upload your receipts to Expensify - whether it’s by SmartScanning them by forwarding via email or manually by taking a picture of a receipt, we’ll cover it here!

# How-to Upload Receipts
## SmartScan
The easiest way to upload your receipts to Expensify is to SmartScan them with Expensify’s mobile app or forward a receipt from your email inbox!

When you SmartScan a receipt, we’ll read the Merchant, Date and Amount of the transaction, create an expense, and add it to your Expensify account automatically. The best practice is to take a picture of the receipt at the time of purchase or forward it to your Expensify account from the point of sale system. If you have a credit card connected and you upload a receipt that matches a card expense, the SmartScanned receipt will automatically merge with the imported card expense instead.

## Email Receipts
To SmartScan a receipt on your mobile app, tap the green camera button, point and shoot! You can also forward your digital receipts (or photos of receipts) to [email protected] from the email address associated with your Expensify account, and they’ll be SmartScanned. This may take a few minutes because Expensify aims to have the most accurate OCR.

## Manually Upload
To upload receipts on the web, simply navigate to the Expenses page and click on **New Expense**. Select **Scan Receipt** and choose the file you would like to upload, or drag-and-drop your image directly into the Expenses page, and that will start the SmartScanning process!

# FAQ
## How do you SmartScan multiple receipts?
You can utilize the Rapid Fire Mode to quickly SmartScan multiple receipts at once!

To activate it, tap on the green camera button in the mobile app and then tap on the camera icon on the bottom right. When you see the little fire icon on the camera, Rapid Fire Mode has been activated - tap the camera icon again to disable Rapid Fire Mode.

## How do you create an expense from an email address that is different from your Expensify login?
You can email a receipt from a different email address by adding it as a Secondary Login to your Expensify account - this ensures that any receipts sent from this email to [email protected] will be associated with your current Expensify account.

Once that email address has been added as a Secondary Login, simply forward your receipt image or emails to [email protected].

## How do you crop or rotate a receipt image?
You can crop and rotate a receipt image on the web app, and you can only edit one expense at a time.

Navigate to your Expenses page and locate the expense whose receipt image you'd like to edit, then click the expense to open the Edit screen. If there is an image file associated with the receipt, you will see the Rotate and Crop buttons. Alternatively, you can also navigate to your Reports page, click on a report, and locate the individual expense.
Original file line number Diff line number Diff line change
@@ -1,5 +1,63 @@
---
title: Coming Soon
description: Coming Soon
title: User Roles
description: Each member has a role that defines what they can see and do in the workspace.
---
## Resource Coming Soon!

# Overview

This guide is for those who are part of a **Group Workspace**.

Each member has a role that defines what they can see and do in the workspace. Most members will have the role of "Employee."

# How to Manage User Roles

To find and edit the roles of group workspace members, go to **Settings > Workspaces > Group > [Your Specific Workspace Name] > Members > Workspace Members**

Here you'll see the list of members in your group workspace. To change their roles, click **Settings** next to the member’s name and choose the role that the member needs.

Next, let’s go over the various user roles that are available on a group workspace.

## The Employee Role

- **What can they do:** Employees can only see their own expense reports or reports that have been submitted to or shared with them. They can't change settings or invite new users.
- **Who is it for:** Regular employees who only need to manage their own expenses, or managers who are reviewing expense reports for a few users but don’t need global visibility.
- **Approvers:** Members who approve expenses can either be Employees, Admins, or Workspace Auditors, depending on how much control they need.
- **Billable:** Employees are billable actors if they take actions on a report on your Group Workspace (including **SmartScanning** a receipt).

## Workspace Admin Role

- **What can they do:** Admins have full control. They can change settings, invite members, and view all reports. They can also process reimbursements if they have access to the company’s account.
- **Billing Owners:** Billing owners are Admins by default. **Workspace Admins** are assigned by the owner or another admin.
- **Billable:** Yes, if they perform actions like changing settings or inviting users. Just viewing reports is not billable.

## Workspace Auditor Role

- **What can they do:** Workspace Auditors can see all reports, make comments, and export them. They can also mark reports as reimbursed if they're the final approver.
- **Who is it for:** Accountants, bookkeepers, and internal or external audit agents who need to view but not edit workspace settings.
- **Billable:** Yes, if they perform any actions like commenting or exporting a report. Viewing alone doesn't incur a charge.

## Technical Contact

- **What can they do:** In case of connection issues, alerts go to the billing owner by default. You can set a technical contact if you want alerts to go to an IT administrator instead.
- **How to set one:** Go to **Settings > Workspaces > Group > [Workspace Name] > Connections > Technical Contact**.
- **Billable:** The technical contact doesn’t need to be a group workspace member and so is not counted towards your billable activity.

Note: running expense analytics from **Insights** follows the same rules. All the reports and data graphs you generate will be created based on the expense data you have access to.

# Deep Dive

## Expense Data Visibility

The amount of expense data you can see depends on your role within any group workspaces you're part of:

- **Employees:** Whether you're on a free or paid plan, if you're not approving expenses, you'll only see your own expenses.
- **Approvers:** If you approve expenses for your team and also submit your own, you can view both individual and team-wide expenses and analytics.
- **Admins:** Users with an admin role can see analytics and data for every expense report made by anyone on the workspace.

If you need to see more data, here are some options:

- **Become an Admin:** Check within your organization if you can be upgraded to an admin role in your group workspaces.
- **Become a Copilot:** Ask to be added as a **Copilot** to an existing admin account, which will allow you some additional viewing privileges.
- **Become an Approver:** You could also be added as an **Approver** in an existing workflow to view more data.


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.3.74.2</string>
<string>1.3.74.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.3.74.2</string>
<string>1.3.74.3</string>
</dict>
</plist>
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": "1.3.74-2",
"version": "1.3.74-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
6 changes: 6 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,6 +440,12 @@ const CONST = {
INTERNAL_DEV_EXPENSIFY_URL: 'https://www.expensify.com.dev',
STAGING_EXPENSIFY_URL: 'https://staging.expensify.com',
EXPENSIFY_URL: 'https://www.expensify.com',
BANK_ACCOUNT_PERSONAL_DOCUMENTATION_INFO_URL:
'https://community.expensify.com/discussion/6983/faq-why-do-i-need-to-provide-personal-documentation-when-setting-up-updating-my-bank-account',
PERSONAL_DATA_PROTECTION_INFO_URL: 'https://community.expensify.com/discussion/5677/deep-dive-security-how-expensify-protects-your-information',
ONFIDO_FACIAL_SCAN_POLICY_URL: 'https://onfido.com/facial-scan-policy-and-release/',
ONFIDO_PRIVACY_POLICY_URL: 'https://onfido.com/privacy/',
ONFIDO_TERMS_OF_SERVICE_URL: 'https://onfido.com/terms-of-service/',

// Use Environment.getEnvironmentURL to get the complete URL with port number
DEV_NEW_EXPENSIFY_URL: 'http://localhost:',
Expand Down
1 change: 1 addition & 0 deletions src/components/Form/FormProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ function FormProvider({validate, shouldValidateOnBlur, shouldValidateOnChange, c
onSubmit={submit}
inputRefs={inputRefs}
errors={errors}
enabledWhenOffline={enabledWhenOffline}
>
{children}
</FormWrapper>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText]
// costly invalidations and commits.
function BaseHTMLEngineProvider(props) {
// We need to memoize this prop to make it referentially stable.
const defaultTextProps = useMemo(() => ({selectable: props.textSelectable, allowFontScaling: false}), [props.textSelectable]);
const defaultTextProps = useMemo(() => ({selectable: props.textSelectable, allowFontScaling: false, textBreakStrategy: 'simple'}), [props.textSelectable]);

// We need to pass multiple system-specific fonts for emojis but
// we can't apply multiple fonts at once so we need to pass fallback fonts.
Expand Down
9 changes: 7 additions & 2 deletions src/components/MenuItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ const defaultProps = {
disabled: false,
isSelected: false,
subtitle: undefined,
subtitleTextStyle: {},
iconType: CONST.ICON_TYPE_ICON,
onPress: () => {},
onSecondaryInteraction: undefined,
Expand All @@ -76,6 +75,7 @@ const defaultProps = {
title: '',
numberOfLinesTitle: 1,
shouldGreyOutWhenDisabled: true,
error: '',
shouldRenderAsHTML: false,
};

Expand Down Expand Up @@ -276,6 +276,11 @@ const MenuItem = React.forwardRef((props, ref) => {
{props.description}
</Text>
)}
{Boolean(props.error) && (
<View style={[styles.mt1]}>
<Text style={[styles.textLabelError]}>{props.error}</Text>
</View>
)}
{Boolean(props.furtherDetails) && (
<View style={[styles.flexRow, styles.mt1, styles.alignItemsCenter]}>
<Icon
Expand Down Expand Up @@ -309,7 +314,7 @@ const MenuItem = React.forwardRef((props, ref) => {
{/* Since subtitle can be of type number, we should allow 0 to be shown */}
{(props.subtitle || props.subtitle === 0) && (
<View style={[styles.justifyContentCenter, styles.mr1]}>
<Text style={[props.subtitleTextStyle || styles.textLabelSupporting, props.style]}>{props.subtitle}</Text>
<Text style={[styles.textLabelSupporting, props.style]}>{props.subtitle}</Text>
</View>
)}
{!_.isEmpty(props.floatRightAvatars) && (
Expand Down
6 changes: 2 additions & 4 deletions src/components/ReportActionItem/MoneyRequestView.js
Original file line number Diff line number Diff line change
Expand Up @@ -167,8 +167,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should
shouldShowRightIcon={canEdit}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))}
brickRoadIndicator={hasErrors && transactionAmount === 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
subtitle={hasErrors && transactionAmount === 0 ? translate('common.error.enterAmount') : ''}
subtitleTextStyle={styles.textLabelError}
error={hasErrors && transactionAmount === 0 ? translate('common.error.enterAmount') : ''}
/>
</OfflineWithFeedback>
<OfflineWithFeedback pendingAction={getPendingFieldAction('pendingFields.comment')}>
Expand All @@ -193,8 +192,7 @@ function MoneyRequestView({report, betas, parentReport, policyCategories, should
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))}
brickRoadIndicator={hasErrors && transactionDate === '' ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
subtitle={hasErrors && transactionDate === '' ? translate('common.error.enterDate') : ''}
subtitleTextStyle={styles.textLabelError}
error={hasErrors && transactionDate === '' ? translate('common.error.enterDate') : ''}
/>
</OfflineWithFeedback>
{isDistanceRequest ? (
Expand Down
6 changes: 3 additions & 3 deletions src/components/menuItemPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,6 @@ const propTypes = {
/** A right-aligned subtitle for this menu option */
subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),

/** Style for the subtitle */
subtitleTextStyle: stylePropTypes,

/** Flag to choose between avatar image or an icon */
iconType: PropTypes.oneOf([CONST.ICON_TYPE_AVATAR, CONST.ICON_TYPE_ICON, CONST.ICON_TYPE_WORKSPACE]),

Expand Down Expand Up @@ -145,6 +142,9 @@ const propTypes = {
/** Should we grey out the menu item when it is disabled? */
shouldGreyOutWhenDisabled: PropTypes.bool,

/** Error to display below the title */
error: PropTypes.string,

/** Should render the content in HTML format */
shouldRenderAsHTML: PropTypes.bool,
};
Expand Down
41 changes: 16 additions & 25 deletions src/components/withKeyboardState.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/* eslint-disable react/no-unused-state */
import React, {forwardRef, createContext} from 'react';
import PropTypes from 'prop-types';
import React, {forwardRef, createContext, useEffect, useState} from 'react';
import {Keyboard} from 'react-native';
import PropTypes from 'prop-types';
import getComponentDisplayName from '../libs/getComponentDisplayName';

const KeyboardStateContext = createContext(null);
Expand All @@ -15,32 +14,24 @@ const keyboardStateProviderPropTypes = {
children: PropTypes.node.isRequired,
};

class KeyboardStateProvider extends React.Component {
constructor(props) {
super(props);

this.state = {
isKeyboardShown: false,
};
}

componentDidMount() {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
this.setState({isKeyboardShown: true});
function KeyboardStateProvider(props) {
const {children} = props;
const [isKeyboardShown, setIsKeyboardShown] = useState(false);
useEffect(() => {
const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
setIsKeyboardShown(true);
});
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
this.setState({isKeyboardShown: false});
const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
setIsKeyboardShown(false);
});
}

componentWillUnmount() {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
return () => {
keyboardDidShowListener.remove();
keyboardDidHideListener.remove();
};
}, []);

render() {
return <KeyboardStateContext.Provider value={this.state}>{this.props.children}</KeyboardStateContext.Provider>;
}
return <KeyboardStateContext.Provider value={{isKeyboardShown}}>{children}</KeyboardStateContext.Provider>;
}

KeyboardStateProvider.propTypes = keyboardStateProviderPropTypes;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import {useEffect, useState, useCallback} from 'react';
import {AccessibilityInfo} from 'react-native';
import _ from 'underscore';
import {AccessibilityInfo, LayoutChangeEvent} from 'react-native';
import moveAccessibilityFocus from './moveAccessibilityFocus';

const useScreenReaderStatus = () => {
type HitSlop = {x: number; y: number};

const useScreenReaderStatus = (): boolean => {
const [isScreenReaderEnabled, setIsScreenReaderEnabled] = useState(false);
useEffect(() => {
const subscription = AccessibilityInfo.addEventListener('screenReaderChanged', setIsScreenReaderEnabled);

return subscription && subscription.remove;
return () => {
subscription?.remove();
};
}, []);

return isScreenReaderEnabled;
};

const getHitSlopForSize = ({x, y}) => {
const getHitSlopForSize = ({x, y}: HitSlop) => {
/* according to https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/
the minimum tappable area is 44x44 points */
const minimumSize = 44;
const hitSlopVertical = _.max([minimumSize - x, 0]) / 2;
const hitSlopHorizontal = _.max([minimumSize - y, 0]) / 2;
const hitSlopVertical = Math.max(minimumSize - x, 0) / 2;
const hitSlopHorizontal = Math.max(minimumSize - y, 0) / 2;
return {
top: hitSlopVertical,
bottom: hitSlopVertical,
Expand All @@ -31,7 +34,7 @@ const getHitSlopForSize = ({x, y}) => {
const useAutoHitSlop = () => {
const [frameSize, setFrameSize] = useState({x: 0, y: 0});
const onLayout = useCallback(
(event) => {
(event: LayoutChangeEvent) => {
const {layout} = event.nativeEvent;
if (layout.width !== frameSize.x && layout.height !== frameSize.y) {
setFrameSize({x: layout.width, y: layout.height});
Expand Down
8 changes: 0 additions & 8 deletions src/libs/Accessibility/moveAccessibilityFocus/index.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {AccessibilityInfo} from 'react-native';
import MoveAccessibilityFocus from './types';

const moveAccessibilityFocus = (ref) => {
const moveAccessibilityFocus: MoveAccessibilityFocus = (ref) => {
if (!ref) {
return;
}

AccessibilityInfo.sendAccessibilityEvent(ref, 'focus');
};

Expand Down
10 changes: 10 additions & 0 deletions src/libs/Accessibility/moveAccessibilityFocus/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import MoveAccessibilityFocus from './types';

const moveAccessibilityFocus: MoveAccessibilityFocus = (ref) => {
if (!ref?.current) {
return;
}
ref.current.focus();
};

export default moveAccessibilityFocus;
Loading

0 comments on commit 64b5073

Please sign in to comment.