Skip to content

Commit

Permalink
merge main
Browse files Browse the repository at this point in the history
  • Loading branch information
dominictb committed Sep 23, 2024
2 parents 8cc2e9d + 5e106f0 commit 05eec82
Show file tree
Hide file tree
Showing 64 changed files with 736 additions and 331 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ module.exports = {
'plugin:you-dont-need-lodash-underscore/all',
'plugin:prettier/recommended',
],
plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library', 'eslint-plugin-react-compiler'],
plugins: ['@typescript-eslint', 'jsdoc', 'you-dont-need-lodash-underscore', 'react-native-a11y', 'react', 'testing-library', 'eslint-plugin-react-compiler', 'lodash'],
ignorePatterns: ['lib/**'],
parser: '@typescript-eslint/parser',
parserOptions: {
Expand Down Expand Up @@ -231,6 +231,7 @@ module.exports = {
'you-dont-need-lodash-underscore/throttle': 'off',
// The suggested alternative (structuredClone) is not supported in Hermes:https://github.com/facebook/hermes/issues/684
'you-dont-need-lodash-underscore/clone-deep': 'off',
'lodash/import-scope': ['error', 'method'],
'prefer-regex-literals': 'off',
'valid-jsdoc': 'off',
'jsdoc/no-types': 'error',
Expand Down
1 change: 1 addition & 0 deletions .github/libs/promiseWhile.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line lodash/import-scope
import type {DebouncedFunc} from 'lodash';

/**
Expand Down
Binary file added assets/animations/BankVault.lottie
Binary file not shown.
Binary file added assets/animations/GenericEmptyState.lottie
Binary file not shown.
Binary file added assets/animations/TripsEmptyState.lottie
Binary file not shown.
45 changes: 45 additions & 0 deletions contributingGuides/STYLE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
- [Stateless components vs Pure Components vs Class based components vs Render Props](#stateless-components-vs-pure-components-vs-class-based-components-vs-render-props---when-to-use-what)
- [Use Refs Appropriately](#use-refs-appropriately)
- [Are we allowed to use [insert brand new React feature]?](#are-we-allowed-to-use-insert-brand-new-react-feature-why-or-why-not)
- [Handling Scroll Issues with Nested Lists in React Native](#handling-scroll-issues-with-nested-lists-in-react-native)
- [Wrong Approach (Using SelectionList)](#wrong-approach-using-selectionlist)
- [Correct Approach (Using SelectionList)](#correct-approach-using-selectionlist)
- [React Hooks: Frequently Asked Questions](#react-hooks-frequently-asked-questions)
- [Onyx Best Practices](#onyx-best-practices)
- [Collection Keys](#collection-keys)
Expand Down Expand Up @@ -1105,6 +1108,48 @@ There are several ways to use and declare refs and we prefer the [callback metho

We love React and learning about all the new features that are regularly being added to the API. However, we try to keep our organization's usage of React limited to the most stable set of features that React offers. We do this mainly for **consistency** and so our engineers don't have to spend extra time trying to figure out how everything is working. That said, if you aren't sure if we have adopted something, please ask us first.


## Handling Scroll Issues with Nested Lists in React Native

### Problem

When using `SelectionList` alongside other components (e.g., `Text`, `Button`), wrapping them inside a `ScrollView` can lead to alignment and performance issues. Additionally, using `ScrollView` with nested `FlatList` or `SectionList` causes the error:

> "VirtualizedLists should never be nested inside plain ScrollViews with the same orientation."

### Solution

The correct approach is avoid using `ScrollView`. You can add props like `listHeaderComponent` and `listFooterComponent` to add other components before or after the list while keeping the layout scrollable.

### Wrong Approach (Using `SelectionList`)

```jsx
<ScrollView>
<Text>Header Content</Text>
<SelectionList
sections={[{data}]}
ListItem={RadioListItem}
onSelectRow={handleSelect}
/>
<Button title="Submit" onPress={handleSubmit} />
</ScrollView>
```

### Correct Approach (Using `SelectionList`)

```jsx
<SelectionList
sections={[{item}]}
ListItem={RadioListItem}
onSelectRow={handleSelect}
listHeaderComponent={<Text>Header Content</Text>}
listFooterComponent={<Button title="Submit" onPress={handleSubmit} />}
/>
```

This ensures optimal performance and avoids layout issues.


## React Hooks: Frequently Asked Questions

### Are Hooks a Replacement for HOCs or Render Props?
Expand Down
6 changes: 6 additions & 0 deletions docs/_data/_routes.yml
Original file line number Diff line number Diff line change
Expand Up @@ -138,3 +138,9 @@ platforms:
title: Settings
icon: /assets/images/gears.svg
description: Manage profile settings and notifications.

- href: billing-and-subscriptions
title: Expensify Billing & Subscriptions
icon: /assets/images/subscription-annual.svg
description: Review Expensify's subscription options, plan types, and payment methods.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
title: Billing and Subscriptions
description: Coming soon
---

# Coming Soon
Binary file added docs/assets/images/addbankaccount_01.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/addbankaccount_02.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/addbankaccount_03.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions docs/new-expensify/hubs/billing-and-subscriptions/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
layout: default
title: Billing & Subscriptions
---

{% include hub.html %}
24 changes: 21 additions & 3 deletions package-lock.json

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

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@
"eslint-plugin-storybook": "^0.8.0",
"eslint-plugin-testing-library": "^6.2.2",
"eslint-plugin-you-dont-need-lodash-underscore": "^6.14.0",
"eslint-plugin-lodash": "^7.4.0",
"html-webpack-plugin": "^5.5.0",
"http-server": "^14.1.1",
"jest": "29.4.1",
Expand Down
6 changes: 4 additions & 2 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,9 @@ const CONST = {
},
// Multiplier for gyroscope animation in order to make it a bit more subtle
ANIMATION_GYROSCOPE_VALUE: 0.4,
ANIMATION_PAY_BUTTON_DURATION: 200,
ANIMATION_PAY_BUTTON_HIDE_DELAY: 1000,
ANIMATION_PAID_DURATION: 200,
ANIMATION_PAID_CHECKMARK_DELAY: 300,
ANIMATION_PAID_BUTTON_HIDE_DELAY: 1000,
BACKGROUND_IMAGE_TRANSITION_DURATION: 1000,
SCREEN_TRANSITION_END_TIMEOUT: 1000,
ARROW_HIDE_DELAY: 3000,
Expand Down Expand Up @@ -5764,6 +5765,7 @@ const CONST = {
ICON_HEIGHT: 160,

CATEGORIES_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories#import-custom-categories',
MEMBERS_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Invite-members-and-assign-roles#import-a-group-of-members',
TAGS_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-tags#import-a-spreadsheet-1',
},

Expand Down
8 changes: 8 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,14 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/members',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/members` as const,
},
WORKSPACE_MEMBERS_IMPORT: {
route: 'settings/workspaces/:policyID/members/import',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/members/import` as const,
},
WORKSPACE_MEMBERS_IMPORTED: {
route: 'settings/workspaces/:policyID/members/imported',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/members/imported` as const,
},
POLICY_ACCOUNTING: {
route: 'settings/workspaces/:policyID/accounting',
getRoute: (policyID: string, newConnectionName?: ConnectionName, integrationToDisconnect?: ConnectionName, shouldDisconnectIntegrationBeforeConnecting?: boolean) => {
Expand Down
2 changes: 2 additions & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,8 @@ const SCREENS = {
INVOICES_COMPANY_WEBSITE: 'Workspace_Invoices_Company_Website',
TRAVEL: 'Workspace_Travel',
MEMBERS: 'Workspace_Members',
MEMBERS_IMPORT: 'Members_Import',
MEMBERS_IMPORTED: 'Members_Imported',
INVITE: 'Workspace_Invite',
INVITE_MESSAGE: 'Workspace_Invite_Message',
CATEGORIES: 'Workspace_Categories',
Expand Down
7 changes: 1 addition & 6 deletions src/components/EmojiPicker/EmojiPickerMenu/index.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import lodashDebounce from 'lodash/debounce';
import React, {useCallback} from 'react';
import type {ForwardedRef} from 'react';
import {View} from 'react-native';
import {runOnUI, scrollTo} from 'react-native-reanimated';
import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
Expand Down Expand Up @@ -67,11 +66,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, r

const scrollToHeader = (headerIndex: number) => {
const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
runOnUI(() => {
'worklet';

scrollTo(emojiListRef, 0, calculatedOffset, true);
})();
emojiListRef.current?.scrollToOffset({offset: calculatedOffset, animated: true});
};

/**
Expand Down
7 changes: 2 additions & 5 deletions src/components/EmojiPicker/EmojiPickerMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import throttle from 'lodash/throttle';
import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import type {ForwardedRef} from 'react';
import {View} from 'react-native';
import {scrollTo} from 'react-native-reanimated';
import EmojiPickerMenuItem from '@components/EmojiPicker/EmojiPickerMenuItem';
import Text from '@components/Text';
import TextInput from '@components/TextInput';
Expand Down Expand Up @@ -114,9 +113,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, r
const filterEmojis = throttle((searchTerm: string) => {
const [normalizedSearchTerm, newFilteredEmojiList] = suggestEmojis(searchTerm);

if (emojiListRef.current) {
scrollTo(emojiListRef, 0, 0, false);
}
emojiListRef.current?.scrollToOffset({offset: 0, animated: false});
if (normalizedSearchTerm === '') {
// There are no headers when searching, so we need to re-make them sticky when there is no search term
setFilteredEmojis(allEmojis);
Expand Down Expand Up @@ -241,7 +238,7 @@ function EmojiPickerMenu({onEmojiSelected, activeEmoji}: EmojiPickerMenuProps, r
}

const calculatedOffset = Math.floor(headerIndex / CONST.EMOJI_NUM_PER_ROW) * CONST.EMOJI_PICKER_HEADER_HEIGHT;
scrollTo(emojiListRef, 0, calculatedOffset, true);
emojiListRef.current?.scrollToOffset({offset: calculatedOffset, animated: true});
},
[emojiListRef],
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {FlashList} from '@shopify/flash-list';
import {useCallback, useEffect, useMemo, useState} from 'react';
import {useAnimatedRef} from 'react-native-reanimated';
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
import emojis from '@assets/emojis';
import {useFrequentlyUsedEmojis} from '@components/OnyxProvider';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -10,7 +9,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions';
import * as EmojiUtils from '@libs/EmojiUtils';

const useEmojiPickerMenu = () => {
const emojiListRef = useAnimatedRef<FlashList<EmojiUtils.EmojiPickerListItem>>();
const emojiListRef = useRef<FlashList<EmojiUtils.EmojiPickerListItem>>(null);
const frequentlyUsedEmojis = useFrequentlyUsedEmojis();
// eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps
const allEmojis = useMemo(() => EmojiUtils.mergeEmojisWithFrequentlyUsedEmojis(emojis), [frequentlyUsedEmojis]);
Expand Down
8 changes: 6 additions & 2 deletions src/components/EmptyStateComponent/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Lottie from '@components/Lottie';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import VideoPlayer from '@components/VideoPlayer';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useThemeStyles from '@hooks/useThemeStyles';
import CONST from '@src/CONST';
import type {EmptyStateComponentProps, VideoLoadedEventType} from './types';
Expand All @@ -24,10 +25,12 @@ function EmptyStateComponent({
subtitle,
headerStyles,
headerContentStyles,
lottieWebViewStyles,
minModalHeight = 400,
}: EmptyStateComponentProps) {
const styles = useThemeStyles();
const [videoAspectRatio, setVideoAspectRatio] = useState(VIDEO_ASPECT_RATIO);
const {isSmallScreenWidth} = useResponsiveLayout();

const setAspectRatio = (event: VideoReadyForDisplayEvent | VideoLoadedEventType | undefined) => {
if (!event) {
Expand Down Expand Up @@ -63,6 +66,7 @@ function EmptyStateComponent({
autoPlay
loop
style={headerContentStyles}
webStyle={lottieWebViewStyles}
/>
);
case CONST.EMPTY_STATE_MEDIA.ILLUSTRATION:
Expand All @@ -75,7 +79,7 @@ function EmptyStateComponent({
default:
return null;
}
}, [headerMedia, headerMediaType, headerContentStyles, videoAspectRatio, styles.emptyStateVideo]);
}, [headerMedia, headerMediaType, headerContentStyles, videoAspectRatio, styles.emptyStateVideo, lottieWebViewStyles]);

return (
<ScrollView contentContainerStyle={[styles.emptyStateScrollView, {minHeight: minModalHeight}, containerStyles]}>
Expand All @@ -88,7 +92,7 @@ function EmptyStateComponent({
<View style={styles.emptyStateForeground}>
<View style={styles.emptyStateContent}>
<View style={[styles.emptyStateHeader(headerMediaType === CONST.EMPTY_STATE_MEDIA.ILLUSTRATION), headerStyles]}>{HeaderComponent}</View>
<View style={styles.p8}>
<View style={isSmallScreenWidth ? styles.p5 : styles.p8}>
<Text style={[styles.textAlignCenter, styles.textHeadlineH1, styles.mb2]}>{title}</Text>
<Text style={[styles.textAlignCenter, styles.textSupporting, styles.textNormal]}>{subtitle}</Text>
{!!buttonText && !!buttonAction && (
Expand Down
1 change: 1 addition & 0 deletions src/components/EmptyStateComponent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type SharedProps<T> = {
headerStyles?: StyleProp<ViewStyle>;
headerMediaType: T;
headerContentStyles?: StyleProp<ViewStyle & ImageStyle>;
lottieWebViewStyles?: React.CSSProperties | undefined;
minModalHeight?: number;
};

Expand Down
6 changes: 3 additions & 3 deletions src/components/ImportSpreadsheetColumns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type ImportSpreadsheetColumnsProps = {
learnMoreLink?: string;
};

function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles, errors, importFunction, isButtonLoading, learnMoreLink}: ImportSpreadsheetColumnsProps) {
function ImportSpreadsheetColumns({spreadsheetColumns, columnNames, columnRoles, errors, importFunction, isButtonLoading, learnMoreLink}: ImportSpreadsheetColumnsProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isOffline} = useNetwork();
Expand Down Expand Up @@ -101,6 +101,6 @@ function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles
);
}

ImportSpreeadsheetColumns.displayName = 'ImportSpreeadsheetColumns';
ImportSpreadsheetColumns.displayName = 'ImportSpreadsheetColumns';

export default ImportSpreeadsheetColumns;
export default ImportSpreadsheetColumns;
15 changes: 15 additions & 0 deletions src/components/LottieAnimations/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,21 @@ const DotLottieAnimations = {
w: 180,
h: 200,
},
GenericEmptyState: {
file: require<LottieViewProps['source']>('@assets/animations/GenericEmptyState.lottie'),
w: 375,
h: 240,
},
TripsEmptyState: {
file: require<LottieViewProps['source']>('@assets/animations/TripsEmptyState.lottie'),
w: 375,
h: 240,
},
BankVault: {
file: require<LottieViewProps['source']>('@assets/animations/BankVault.lottie'),
w: 375,
h: 240,
},
} satisfies Record<string, DotLottieAnimation>;

export default DotLottieAnimations;
7 changes: 6 additions & 1 deletion src/components/ReportActionItem/MoneyRequestView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import ReceiptEmptyState from '@components/ReceiptEmptyState';
import Switch from '@components/Switch';
import Text from '@components/Text';
import ViolationMessages from '@components/ViolationMessages';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import useNetwork from '@hooks/useNetwork';
import usePermissions from '@hooks/usePermissions';
Expand Down Expand Up @@ -175,6 +176,10 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals

const isAdmin = policy?.role === 'admin';
const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID;

const currentUserPersonalDetails = useCurrentUserPersonalDetails();
const isRequestor = currentUserPersonalDetails.accountID === parentReportAction?.actorAccountID;

// A flag for verifying that the current report is a sub-report of a workspace chat
// if the policy of the report is either Collect or Control, then this report must be tied to workspace chat
const isPolicyExpenseChat = ReportUtils.isReportInGroupPolicy(report);
Expand Down Expand Up @@ -357,7 +362,7 @@ function MoneyRequestView({report, shouldShowAnimatedBackground, readonly = fals

const isReceiptAllowed = !isPaidReport && !isInvoice;
const shouldShowReceiptEmptyState =
isReceiptAllowed && !hasReceipt && !isApproved && !isSettled && (canEditReceipt || isAdmin || isApprover) && (canEditReceipt || ReportUtils.isPaidGroupPolicy(report));
isReceiptAllowed && !hasReceipt && !isApproved && !isSettled && (canEditReceipt || isAdmin || isApprover || isRequestor) && (canEditReceipt || ReportUtils.isPaidGroupPolicy(report));

const [receiptImageViolations, receiptViolations] = useMemo(() => {
const imageViolations = [];
Expand Down
Loading

0 comments on commit 05eec82

Please sign in to comment.